自由帳

@_nibral の技術ブログ

RustでActivityPubサーバを書いていてハマったところ

ここ半年くらいMisskeyの某サーバに定住していて、裏で動いている仕組みが気になったので勉強がてらRustでActivityPubのサーバを書いた。といってもガチのアプリケーションではなく、複数のBOTユーザを抱えてフォロワーにノートを配信するだけの片方向な感じのやつ。

実装を進めていくと何回かどうやったらいいのかわからなくてハマったので備忘録として残す。

github.com

HTTP SignatureのDigestの求め方

sha256base64を使う。SHA-256のハッシュ値を2文字ずつに分割して16進数として解釈し、BASE64エンコードすればOK。なんかもうちょっとうまいやり方があるような気もする。

fn http_digest(data: String) -> String {
    let sha256_hash = digest(data);
    let binaries = sha256_hash.chars()
        .collect::<Vec<char>>()
        .chunks(2)
        .map(|c| c.iter().collect::<String>())
        .map(|hex| u8::from_str_radix(&hex, 16).unwrap())
        .collect::<Vec<u8>>();
    return general_purpose::STANDARD.encode(binaries);
}

HTTPリクエストの署名

Signatureヘッダに含める署名は以下で求めることができる。実際にはユーザごとの秘密鍵をDBに保存しておいて、都度必要な鍵を取り出して署名することになると思う。

fn http_sign(data: &[u8]) -> String {
    // load pem
    let private_key_str = r#"-----BEGIN RSA PRIVATE KEY-----
........
-----END RSA PRIVATE KEY-----"#;
    let private_key = PKey::private_key_from_pem(private_key_str.as_bytes()).unwrap();

    // signing by rsa-sha256
    let mut signer = Signer::new(MessageDigest::sha256(), &private_key).unwrap();
    signer.set_rsa_padding(Padding::PKCS1).unwrap();
    let mut len = signer.len().unwrap();
    let mut buf = vec![0; len];
    len = signer.sign_oneshot(&mut buf, data).unwrap();
    buf.truncate(len);
    let signature = general_purpose::STANDARD.encode(buf);

    return signature;
}

Actix Webに非同期でデータを渡す

Dependency InjectionとかでActix Webのサーバを起動するときにデータを渡しておき、リクエストハンドラの引数として受け取りたいケース。公式をはじめとして見つけやすいサンプルコードでは App:new().app_data() を使っているが、この方法はasync/awaitに対応していない。代わりに App::new().data_factory() を使う。

リクエストハンドラ側の受け取り方は一緒。

 App::new()
    .data_factory(|| {
        async {
            <ここにawaitな処理を書く>
        }
    })

日本語配列でBluetoothマルチペアリングができるキーボード「Sunrise70」を作った

日本語配列で複数台のPCにBluetooth接続できるキーボードが欲しくなったが、既にある製品や自作キットだと矢印キーが横並びになってたり分割前提だったりしてどれも微妙に納得いかない感じだったのでイチから作った。スイッチは軽くて静かなのが気に入ったのでKailh BOX Silent Pink。

初めてのキーボードで70キーなので名前は「Sunrise70」。いつかend gameにたどり着いたらSunsetの名をつけたい。無刻印にしてMidnightとかも面白いかも。

特徴

  • 使い慣れた日本語配列Majestouch 交換用キーキャップセットを使えば刻印と入力が一致するので安心。
  • コントローラはスイッチサイエンスのISP1807搭載Microボードを採用。USB Type-Cと技適の通ったBLEが使えるマイコンの中では比較的安価で入手性が良い。ピン配置はPro Micro互換なので他のマイコンも一応使える。
  • PCとの接続はBLEのみ、複数台の同時ペアリングが可能で接続先はホットキーで切り替え。電源はUSBから取るので電池切れの心配なし。
  • スペースキーを2.25u x2にすることでスタビライザーの入手性を確保*1しつつ、好みに応じてShiftなどの割り当ても可能に。
  • 基板とケースのサイズ、ネジの位置はサリチル酸さんのGL516互換。
  • 光らない。

基板とスイッチプレート

KiCadで設計してJLCPCBに作ってもらった。ガーバーデータをアップロードして発注したところ長いキーのスイッチ穴とスタビライザー穴の間隔が狭すぎるという指摘が来たので、細い部分をなくして一つの大きな穴に変更した。基板の方は特に指摘事項もなく動作も一発OK。

こんな感じで直すところを指示してくれる。

ケース

サリチル酸さんの公開しているデータをそのまま3Dプリンタで出力。Ender-3だと大きさの問題で1回では出力できないので、2分割して出力してから接着した。微妙に地面から浮いてる部分があったりエッジが面取りされてたりして印刷中にベッドから浮きやすいので、出力するときはラフトかブリムをつけた方が良さそう。

ファームウェア

QMKが使ってるマイコンに対応していないのでArduino IDEで自作。BLEまわりの処理はAdafruitが公開しているライブラリを参考にしつつ、スイッチが押されたら定義済みのHIDキーコードを送るようにすればOKだった。400行ないくらいだけどブログに貼ると長いのでGistに。

Firmware for Sunrise70 keyboard · GitHub

材料費

  • 基板とスイッチプレート x5セット: $65.4(≒8,500円)
    • 基板: $16.5
    • スイッチプレート: $32.1 (穴が多くて追加料金)
    • 送料: $15.8 (Standard Global Direct Line)
    • PayPal手数料: $1
  • ISP1807搭載Microボード x1: 3,300円
  • Kailh BOX Slient Pink x70: 5,390円
  • キーソケット x70: 1,309円
  • Majestouch キーキャップセット x1セット: 2,691円
  • 2uスタビライザー x4: 880円
  • ダイオード(1N4148W) x80: 200円

これ以外にピンヘッダ・M2のネジ・ゴム足・長めのUSBケーブルなどを買っていることを考えると、1台作るのにおよそ16,000円かかったらしい。1/3はスイッチの値段なのでこんなもんかという感じ。

基板とスイッチプレートがあと4セット余ってるので、欲しい人がいたら原価でお譲りします。@_nibralまでDMください。

作ってみての感想

この記事はSunrise70で書きました。(これが書いてみたかった)

*1:日本語配列で一般的な4.5uのスタビライザーは手に入りにくい

ECS + FargateでgRPCを動かす

gRPCでリクエストを受けるアプリをECS + Fargateで動かしつつ、ちゃんと負荷分散するために調べたことのメモ。


2021/2/22 追記

ALBがgRPCをサポートしたのでこの記事の内容は不要になった。

aws.amazon.com


先に結論

リバースプロキシとしてenvoyを走らせて、ECS Service Discovery経由でつなぐのが良さそう。インターネットからの通信を受けたいならALBではなくNLBを使う。

検証用プログラム

gRPCでUUIDを返す。UUIDはサーバの起動時に1回だけ生成するので、UUIDの値を比較すれば負荷分散ができているかがわかる。

github.com

hello.proto

syntax = "proto3";

package hello;

option go_package = ".;main";

message HelloRequest {

}

message HelloReply {
  string msg = 1;
}

service Hello {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

server.go

package main

import (
    "context"
    "github.com/google/uuid"
    "google.golang.org/grpc"
    "log"
    "net"
)

type Service struct {
    id string
}

func (service *Service) SayHello(ctx context.Context, message *HelloRequest) (*HelloReply, error) {
    return &HelloReply{
        Msg: "Hello from " + service.id,
    }, nil
}

func main() {
    port, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalln(err)
    }
    server := grpc.NewServer()
    helloService := &Service{
        id: uuid.New().String(),
    }
    RegisterHelloServer(server, helloService)
    err = server.Serve(port)
    if err != nil {
        log.Fatalln(err)
    }
}

client.go

package main

import (
    "context"
    "flag"
    "fmt"
    "google.golang.org/grpc"
    "log"
)

func main() {
    flag.Parse()
    conn, err := grpc.Dial(flag.Arg(0), grpc.WithInsecure())
    if err != nil {
        log.Fatalln(err)
    }
    defer conn.Close()
    client := NewHelloClient(conn)
    msg := &HelloRequest{}
    res, err := client.SayHello(context.TODO(), msg)
    if err != nil {
        fmt.Printf("error::%#v \n", err)
    }
    fmt.Println(res.Msg)
}

ECSで動かしたいのでDockerfileも用意した。単にgo buildをするだけでなく、マルチステージビルドにしてdockerイメージを軽くするとかAlpine Linuxglibcを入れるとかもやっている。

FROM golang:latest as builder

WORKDIR /go/src/grpc_sample
COPY server.go hello.proto ./

RUN apt update \
    && apt install -y protobuf-compiler \
    && go get google.golang.org/grpc \
    && go get github.com/golang/protobuf/protoc-gen-go
RUN protoc --go_out=plugins=grpc:. hello.proto

RUN go get -v ./...
RUN go build -o server server.go hello.pb.go

FROM alpine:latest

COPY --from=builder /go/src/grpc_sample/server server

RUN apk update && apk add libc6-compat

EXPOSE 50051

ビルドしたdockerイメージをECRにプッシュしたらECSの検証に移る。

ECSとロードバランサーとgRPC

ECSの負荷分散だとApplication Load Balancer(ALB)が使われることが多いが、ALBのターゲット側はHTTP/1.1にしか対応していないためgRPC(HTTP/2)では使えない。

f:id:nibral:20200906172522p:plain
ALB構成

ではどうするかというと、ALBの代わりにNetwork Load Balancer(NLB)を使う。NLBはレイヤー4(TCP)で処理を行うので、gRPCの通信も問題なく通過できる。これで一見良さそうに見えるが、設定を進めてみるといくつかの問題点が判明した。

  • HTTP/2は1つのTCPコネクションを長く使うため、適切な負荷分散が行われない可能性がある(らしい)
    • 特定のコンテナに負荷が偏る
    • 今回使用したサンプルプログラムでは1回1回通信を張るので確認できず
  • セキュリティグループの設定ができない
    • コンテナにはクライアントの送信元IPがそのまま届くので 0.0.0.0/0 を許可するしかない
  • SSLの終端ができない
    • NLBより後ろでSSLの処理を行う必要がある
    • ACMで発行した無料のSSL証明書が使えない

f:id:nibral:20200906172533p:plain
NLB構成

セキュリティグループは頑張ってIPを設定する、SSLはいったん諦めるとして、負荷分散はちゃんとやりたいということでたどり着いた構成が以下。

NLBとサーバの間にEnvoyというプロキシを挟む。envoyからサーバへ通信するためには各サーバコンテナのIPを知る必要があるので、ECS Service Discoveryを使って server.grpc.local がコンテナのIPを返すようにしておく。

f:id:nibral:20200906173807p:plain
envoy + ECS Service Discovery構成

NLBのDNS名に対してクライアントからリクエストした様子。c9cf2091- で始まるUUIDと 62aa1e41- のUUIDが返ってきているのでちゃんと動いているはず。

f:id:nibral:20200906175206p:plain
サーバ2台で負荷分散

envoy.yaml

envoy 1.16.0-dev-0b24c6で動作確認。envoy API v3。ネット上で見つかる記事だとv2の記述が多いので、調べるときは記事の日付を見た方が良い。

admin:
  access_log_path: '/dev/null'
  address:
    socket_address:
      address: 127.0.0.1
      port_value: 9901
static_resources:
  listeners:
    - name: listner_0
      address:
        socket_address:
          protocol: TCP
          address: 0.0.0.0
          port_value: 5000
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http
                codec_type: AUTO
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: backend
                      domains:
                        - '*'
                      routes:
                        - match:
                            prefix: '/'
                          route:
                            cluster: grpc_sample
                access_log:
                  - name: envoy.access_loggers.file
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
                      path: '/dev/stdout'
                http_filters:
                  - name: envoy.filters.http.router
  clusters:
    - name: grpc_sample
      connect_timeout: 0.25s
      type: LOGICAL_DNS
      lb_policy: ROUND_ROBIN
      http2_protocol_options: {}
      health_checks:
        - timeout: 5s
          interval: 10s
          unhealthy_threshold: 2
          healthy_threshold: 2
          tcp_health_check: {}
      load_assignment:
        cluster_name: grpc_sample
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: server.grpc.local
                      port_value: 50051

Amazon Auroraで"Unknown MySQL error"が出る

DjangoチュートリアルをDockerで動かしていて、DBにAmazon Aurora (MySQL 5.7)を使おうとしたらERROR 2000 (HY000): Unknown MySQL errorが出た話。

docs.djangoproject.com

先に結論

クエリキャッシュを無効にする (query_cache_type = 0) と解決する。

環境

  • masOS Catalina 10.15.2
  • docker desktop community 2.1.0.5 (40693)

問題のSQLクエリ

トップページ用にQuestionの一覧を取得する。

SELECT `polls_question`.`id`, `polls_question`.`question_text`, `polls_question`.`pub_date` FROM `polls_question` ORDER BY `polls_question`.`pub_date` DESC LIMIT 5;

調査

手元のMac + brewで入れたMySQL 8.0のクライアント → OK

% mysql -V
mysql  Ver 8.0.18 for osx10.15 on x86_64 (Homebrew)
% 
% mysql -uadmin -p -h <Auroraのクラスターエンドポイント> -D mysite_db
Enter password: 
mysql> SELECT `polls_question`.`id`, `polls_question`.`question_text`, `polls_question`.`pub_date` FROM `polls_question` ORDER BY `polls_question`.`pub_date` DESC LIMIT 5;
+----+---------------+----------------------------+
| id | question_text | pub_date                   |
+----+---------------+----------------------------+
|  1 | What's up?    | 2020-01-16 07:45:11.757886 |
+----+---------------+----------------------------+
1 row in set (0.02 sec)

Alpine LinuxのDockerコンテナ + mariadb-client(10.4.10) → ERROR 2000 (HY000): Unknown MySQL error

% docker run -it --rm alpine:latest /bin/ash
/ # apk update
/ # apk add mariadb-client
/ # 
/ # mysql -V
mysql  Ver 15.1 Distrib 10.4.10-MariaDB, for Linux (x86_64) using readline 5.1
/ # 
/ # mysql -uadmin -p -h <Auroraクラスターのエンドポイント> -D mysite_db
Enter password: 
MySQL [mysite_db]> SELECT `polls_question`.`id`, `polls_question`.`question_text`, `polls_question`.`pub_date` FROM `polls_question` ORDER BY `polls_question`.`pub_date` DESC LIMIT 5;
ERROR 2000 (HY000): Unknown MySQL error

クエリの一部(DESC LIMIT 5)を消すとエラーコードすら出なくなるが、何度か再実行すると正常な結果が返ってきたりして不安定。

MySQL [mysite_db]> SELECT `polls_question`.`id`, `polls_question`.`question_text`, `polls_question`.`pub_date` FROM `polls_question` ORDER BY `polls_question`.`pub_date`;
ERROR: 
MySQL [mysite_db]> SELECT `polls_question`.`id`, `polls_question`.`question_text`, `polls_question`.`pub_date` FROM `polls_question` ORDER BY `polls_question`.`pub_date`;
ERROR: 
MySQL [mysite_db]> SELECT `polls_question`.`id`, `polls_question`.`question_text`, `polls_question`.`pub_date` FROM `polls_question` ORDER BY `polls_question`.`pub_date`;
+----+---------------+----------------------------+
| id | question_text | pub_date                   |
+----+---------------+----------------------------+
|  1 | What's up?    | 2020-01-16 07:45:11.757886 |
+----+---------------+----------------------------+
1 row in set (0.000 sec)

MySQL [mysite_db]> 
MySQL [mysite_db]> SELECT `polls_question`.`id`, `polls_question`.`question_text`, `polls_question`.`pub_date` FROM `polls_question` ORDER BY `polls_question`.`pub_date` DESC LIMIT 5;
+----+---------------+----------------------------+
| id | question_text | pub_date                   |
+----+---------------+----------------------------+
|  1 | What's up?    | 2020-01-16 07:45:11.757886 |
+----+---------------+----------------------------+
1 row in set (0.000 sec)

UbuntuのDockerコンテナ + mariadb-client(10.1.43) → ERROR 2027 (HY000): Malformed packet

% docker run -it --rm ubuntu:latest /bin/bash        
root@cc00c89acc69:/# apk update
root@cc00c89acc69:/# apk upgrade
root@cc00c89acc69:/# apk install mariadb-client
root@cc00c89acc69:/# 
root@cc00c89acc69:/# mysql -V
mysql  Ver 15.1 Distrib 10.1.43-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2
root@cc00c89acc69:/# 
root@cc00c89acc69:/# mysql -uadmin -p -h <Auroraクラスターのエンドポイント> -D mysite_db
Enter password: 
MySQL [mysite_db]> SELECT `polls_question`.`id`, `polls_question`.`question_text`, `polls_question`.`pub_date` FROM `polls_question` ORDER BY `polls_question`.`pub_date` DESC LIMIT 5;
ERROR 2027 (HY000): Malformed packet

解決

"Unknown MySQL error"では有益な情報にたどり着けなかったので"Malformed packet"で調べたところ、クエリキャッシュを切ったら直るという情報を発見。

stackoverflow.com

Auroraのパラメータグループを変更したあと、Alpineのコンテナで確認。

/ # mysql -uadmin -p -h <Auroraクラスターのエンドポイント> -D mysite_db
Enter password: 
MySQL [mysite_db]> SHOW VARIABLES LIKE '%query_cache_type%';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| query_cache_type | OFF   |
+------------------+-------+
1 row in set (0.019 sec)

MySQL [mysite_db]> 
MySQL [mysite_db]> SELECT `polls_question`.`id`, `polls_question`.`question_text`, `polls_question`.`pub_date` FROM `polls_question` ORDER BY `polls_question`.`pub_date` DESC LIMIT 5;
+----+---------------+----------------------------+
| id | question_text | pub_date                   |
+----+---------------+----------------------------+
|  1 | What's up?    | 2020-01-16 07:45:11.757886 |
+----+---------------+----------------------------+
1 row in set (0.013 sec)

直った。少なからずパフォーマンスは落ちると思われる。

Auroraが変なレスポンスを返しているのか、MariaDBのクライアントがちゃんと解釈できてないのか、それ以外のところが悪いのか、よくわからない。

i3-9100Fで自宅PCを組んだ

メモリとSSDが安くなってたので勢いで。

構成

まず、いままで使っていたPCの構成がこちら。Netflixで映画を見たりETS2でヨーロッパを走り回る分には特に問題ないが、組んでから4年半が経過してるのでそろそろ入れ替え時。

パーツ 型番
CPU Intel Core i5-4460
CPUクーラー サイズ 虎徹 (SCKTT-1000)
メモリ Patriot DDR3-1600 4GB x2 (PSD38G1600KH)
マザーボード ASUS H97-PRO
GPU MSI GTX 1060 AERO ITX 6G OC
SSD OCZ Arc 100 240GB
HDD WD Green 2TB (WD20EZRX)
ケース Fractal Design Define R5
電源 ENERMAX Platimax EPM750AWT

で、新しいマシンの構成がこちら。CPU/メモリ/マザーの3点セットとSSD以外は流用した。

パーツ 型番
CPU Intel Core i3-9100F
CPUクーラー サイズ 虎徹 (SCKTT-1000)
メモリ CFD Crucial DDR4-2666 8GB x2 (W4U2666CM-8G)
マザーボード ASRock B365M Pro4
GPU MSI GTX 1060 AERO ITX 6G OC
SSD WD Blue SN500 NVMe M.2 SSD 500GB (WDS500G1B0C)
ケース Fractal Design Define R5
電源 ENERMAX Platimax EPM750AWT

現行のi3は4コア4スレッドかつターボブーストも効く*1のでかつてのi5より性能が良い。グラボがあるので内蔵GPUなしのFモデルにしたのが今回のポイント。

組み立て

数年ぶりにケースをサイドパネルをオープン。Define R5は空気を取り入れるところにフィルタがついているので、ケース内のホコリは少なめ。

マザーボードから外してグリスをきれいにしたi5-4460。グリスの塗りなおしはしなかったので、表面の刻印を見たのは組んだ日とバラす日の2回だけ。

i3-9100F。ヒートスプレッダの形状が少し変わった。

マザーボードの説明書に従ってCPUクーラーを先に付けたら、M.2のヒートシンク取り付けがやりにくい。

マザーボードが一回り小さくなったのと、HDDがなくなったのとでケース内部がスッキリした。この写真を見ていて気付いたが、ケースファンが吸気x2 排気x1の構成なのでケース内が陽圧になっていてホコリが少なかったのかもしれない。

ベンチマーク

FF14ベンチは「非常に快適」判定。

f:id:nibral:20191227173310p:plain

CPUに100%負荷をかけても40℃ちょっとで安定している。(室温20℃)

f:id:nibral:20191227173320p:plain

お手頃価格とはいえさすがNVMeという速度。

f:id:nibral:20191227173330p:plain

まとめ

トータル3万円ちょっとでPCの世代交代ができてよかった。

*1:9100Fは最大4.2GHz

オフィスにCisco Meraki MR33を導入してみた

NTTから貸し出されるホームゲートウェイ(RS-500KI)の無線LANがどうにも不安定なので、ちゃんとしたアクセスポイントを導入した話。

Cisco Merakiとは

数年前にCiscoが買収したプロダクトで、無線LANのアクセスポイントやスイッチ、ファイヤーウォールなどを揃える。設定は全てクラウド上で一元管理されているのが特徴で、一般的なネットワーク機器のようにそれぞれの設定画面にログインして設定する必要がない。

www.publickey1.jp

Merakiシリーズはエンタープライズ向けなので一般消費者向けの小売りはされておらず、代理店経由で購入する必要がある。より小規模なネットワーク向けにMeraki Goというシリーズもあり、機能面では本家Merakiに及ばないものの、こちらはAmazonで買える。

無償検証用アクセスポイント

2019/10現在、Ciscoが提供するオンラインセミナー(ウェビナー)を受講すると検証用としてアクセスポイントがもらえるキャンペーンを展開している。

法人向けのためか多少のやりとりが必要なので、簡単に手順を。

まず、Merakiオンデマンドウェビナーのサイトにアクセスして「イントロダクション : クラウド管理型IT パワフルなITをよりシンプルに」の「視聴する」をクリック。

merakiresources.cisco.com

申し込みフォームが表示されるのですべて入力して「送信」。セミナー動画(1時間くらい)が再生できるようになるのでちゃんと見る。見終わると再度メールアドレスの入力フォームが表示されるので、メールアドレスを入力。

f:id:nibral:20191017115905j:plain

正常に受け付けられると、「アクセスポイントの送付先住所をMerakiの営業に送ってね」というメールが来る。

f:id:nibral:20191017120226j:plain

指定されたアドレスにアクセスポイントの送付を希望する旨と送付先をメール。ほどなくして相手方から電話があり、ネットワークの規模やアクセスポイントの使い方を聞かれたので素直に答える。問題なさそうだと判断されると(?)アクセスポイントが発送される。

今回はMR33というアクセスポイントと3年分のサブスクリプションがもらえることになった。定価で考えると機器代が99,800円、サブスクリプションが39,900円なのでかなり太っ腹。

発送されると「Your Meraki AP has shipped - xxxxxxxxx」というメールが来るので、大切に保存しておく。

外観

エンタープライズ向けらしくシンプルな箱。

MR33本体のほか、壁掛け用の台座と金具類が付属。

接続口はLANと電源のみ。底面が丸みを帯びていて端のほうは隙間ができるので、フラットでない普通のLANケーブルでも問題ない。

大きさはティッシュ箱と同じか一回り小さいくらい。構造的に縦置きはできず、平置きか壁掛けのどちらかを選ぶ必要がある。

小さいほうの箱にはACアダプターが入っていた。後で調べたところ、MR33はPoE給電が基本なのでACアダプターは別売とのこと。電話で「フレッツ光のホームゲートウェイ使ってます」という話をしたので、気を使ってくれたのかも。

初期設定

アクセスポイントのネットワーク設定

前述したようにMerakiのアクセスポイントはクラウド上で設定を行う。逆に言うとアクセスポイントがインターネットにつながっていないと何もできない。

DHCPIPアドレスが配られているネットワークであればLANケーブルと電源をつなぐだけでよいが、静的IPを振る場合はいったんアクセスポイントに直接接続して設定を変える必要がある。詳細な手順は公式の設置ガイドを参照のこと。ステータスLEDが緑になればOK。

www.cisco.com

アクセスポイントを設置したら、以降はMerakiのWebサイトで設定を行う。

Meraki ダッシュボードアカウントの作成

MerakiのWebサイトにアクセスして右上の「Login」をクリック。

f:id:nibral:20191017124347j:plain

アカウントを作成するので「Create an account」。

f:id:nibral:20191017124418j:plain

リージョンは Asia を選択して「Next」。

f:id:nibral:20191017124503j:plain

続いてアカウントの情報を入力し「Create account」。

f:id:nibral:20191017125255j:plain

入力したメールアドレスに「Cisco Meraki Email Verification」というメールが届くので、本文中のURLを開いてメールアドレスの検証を行う。

f:id:nibral:20191017125402j:plain

バイスとライセンスの登録

ダッシュボードを開くとWelcome的な画面が表示されるので「Register Meraki devices」を選択して「Next」。

f:id:nibral:20191017144047j:plain

ネットワーク名とネットワークに参加させるデバイスの選択画面。Merakiにおけるネットワーク機器の管理は 組織 → ネットワーク → デバイス という階層構造になっている。

  • ABC株式会社 (組織)
    • 東京オフィス (ネットワーク)
    • 大阪オフィス (ネットワーク)

のようなイメージ(たぶん)。また、ネットワークに所属していないデバイスは インベントリ にプールされる。

f:id:nibral:20191017144744j:plain

インベントリにデバイスを追加する際は、購入した際のオーダー番号か製品記載のシリアルナンバーを使う。今回は1台だけなのでシリアルナンバーを入力して「Add devices」した。

f:id:nibral:20191017144936j:plain

ネットワーク名を入力し、追加したMR33にチェックを入れたら「Create network」。

f:id:nibral:20191017145039j:plain

正常にネットワークが作成されるとネットワークに所属しているアクセスポイントの一覧画面が表示されるが、この段階ではデバイスを登録しただけでサブスクリプションが有効になっていない。

サブスクリプションを有効にするには「Your Meraki AP has shipped - xxxxxxxxx」のメールに記載のURLをクリックする。サブスクリプションが有効になると、左側のメニュー Organization → License info のLicense statusが Ok になる。

f:id:nibral:20191017145623j:plain

これで無線LANアクセスポイント1台 x 3年のライセンスが有効になった。

MR33の面白いところ

電波状態モニターの表示が細かい

各チャンネルの混雑具合と時間軸での変化が色分けで表示される。ほぼリアルタイムで更新されるので、チャンネルを割り当てるときの参考にするもよし、スピードテストを走らせて色が赤くなるのを楽しむのもよし。

f:id:nibral:20191017152627j:plain

NATとファイアウォールがついてる

IP割り当てをNAT modeにすると、アクセスポイントがDHCPサーバとして動作して配下の端末に10.0.0.0/8のIPアドレスを配るようになり、無線LANクライアント間での通信が行えなくなる。

f:id:nibral:20191017153003p:plain

ファイアウォールでは、IPアドレスやポートを指定して通信を拒否できる。デフォルトでは有線LAN側端末へのアクセスを拒否する設定なので、NASにアクセスする場合などは許可する必要がある。

f:id:nibral:20191017152954j:plain

2つの設定をうまく組み合わせるとゲスト用のネットワークのセキュリティを高められる。

ゲスト用SSIDにスプラッシュページが出せる

インターネットに出る前に「ご来社ありがとうございます」的なページを出したり、認証を求めることができる。試してはいないが、ページの種類をBillingにして課金制にすることもできるようだ。

f:id:nibral:20191017152937p:plain

導入して1週間、今のところは安定して動作しているしネットワークの管理もやりやすいと思う。家庭用の無線LANルータが3,000円ちょっとで買える時代ではあるが、エンタープライズ向けはいろいろ設定できて楽しい。

AWS SAMでDefaultAuthorizerとCORSが共存できるようになった

半年ほど前に以下のような記事を書いたが、その後のアップデートでDefaultAuthorizerとCORSが問題なく共存できるようになった。

nibral.hateblo.jp

関係しそうなところ

  • AWS::Serverless::ApiのAuthプロパティに「AddDefaultAuthorizerToCorsPreflight」が追加され、CORSのプリフライトリクエストにDefaultAuthorizerを適用するかどうかを指定できるようになった (AWS SAM v1.13~)
    • 以前テストをパスできなくて取り下げられていたもの

github.com

  • AWS::Serverless::FunctionのAuthプロパティに「NONE」を指定して、Authrorizerを無効にできるようになった (AWS SAM v1.15~)

github.com

  • 「sam local start-api」がCORSに対応した (AWS SAM CLI v0.21.0~)
    • Authorizerには直接関係しないが、ローカルでプリフライトリクエストの動作確認ができるようになった

github.com

設定例

以下のような template.yml を作成すると、

  • GET /
    • CognitoAuthorizer
  • GET /public
    • Authorizerなし
  • OPTIONS / および OPTIONS /public
    • Authorizerなし (プリフライトリクエスト成功)

という動作が実現できる。AWS SAM CLI v0.22.0で確認。

Resources:
  MyApi:
    Type: AWS::Serverless::Api
    Properties:
      Auth:
        DefaultAuthorizer: CognitoAuthorizer
        AddDefaultAuthorizerToCorsPreflight: false
        Authorizers:
          CognitoAuthorizer:
            UserPoolArn: <Cognito UserPool ARN>
      Cors:
        AllowOrigin: "'*'"

  MyFunction1:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.handler
      CodeUri: src/
      Events:
        GetIndex:
          Type: Api
          Properties:
            Path: /
            Method: get

  MyPublicFunction1:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.handler
      CodeUri: src/
      Events:
        GetPublic:
          Type: Api
          Properties:
            Path: /public
            Method: get
            Auth:
              Authorizer: 'NONE'

もともとやりたかったことができるようになったので満足。