にゃみかんてっくろぐ

猫か百合を見守る壁になりたい

「インデックスが作成されていないカラムを WHERE 句に書かない」コーディング規約と、暗黙的なコンテキスト

一部フィクションを含むポエムです。

XX システム SQL コーディング規約

性能劣化の可能性があるため、インデックスが作成されていないカラムを WHERE 句に書かないこと。
(インデックスの作成有無はテーブル定義書を参照)

こうしたコーディング規約が ある ところは、 SQL 文を書くメンバが、概ね文法のみを知っていればよい、と言える。 誤解を恐れずに言えば、 メンバは RDBMS をあまり知らなくてよい

では、逆の場合、コーディング規約が ない ところを考えたい。

SQL 文を書くメンバは、 RDBMS を知っていて当然」、つまり暗黙的なコンテキストを設定していると言えるだろうか。必ずしもそうではないように感じる。いくつかのケースが思いつく。

暗黙的なコンテキストとして「みな知っていて当然」としているケース。 SQL 文を書くメンバは 当然 RDBMS の検索処理を知っており、適切な SQL 文を書くことができる、というもの。

「誰かが知っていて、ツッコミが入るはず」としているケース。たとえば、 SQL 文にアーキテクトやレビュアのチェックが入り、リリースまでに適切な SQL 文になるはずである、といったもの。

無秩序なケース。リリースして火を吹く可能性があるが、そのリスクを受け入れる、というもの。


「みな知っていて当然」は状態として最高のように思えるが、人間は全知全能の神ではないため「当然」というのはプレッシャーにもなりうる。一方で、無秩序なケースはプレッシャーを感じなかったとしても将来大きな損害を被る恐れがある。

いまのチーム、いまのプロジェクト、いまの現場は、どのケースだろうか。リスクとリターンを天秤に乗せたときに、ベターな状態だろうか。

Amazon ECS: curl コンテナを使ってタスク定義だけでモックサーバを設定する

3 行まとめ

  • API のモック化ツール WireMock には、動的にスタブ/モックを定義できる Admin API がある
  • ECS タスク定義に「 Admin API を叩く curl コンテナ」を加えて WireMock のスタブ/モック定義を行う
  • タスク定義だけ完結するので楽

タスク定義だけでモックサーバを設定する

テスト用のモックサーバの 1 つに WireMock がありますが、この WireMock には動的にスタブ/モックを定義できる Admin API があります (Stubbing - WireMock) 。

この Admin API を利用します。 Amazon ECS のタスク定義に「 WireMock コンテナ」に加えて「 Admin API を叩く curl コンテナ」を用意することで、 WireMock の設定が可能になります。

タスク定義例

ポイントは 2 点です。

  • WireMock が起動したあとに curl を叩くよう、ヘルスチェックと依存関係を設定
  • curl コンテナは実行後終了するので essential: false とする
{
    "containerDefinitions": [
        {
            "portMappings": [
                {
                    "hostPort": 8080,
                    "protocol": "tcp",
                    "containerPort": 8080
                }
            ],
            "cpu": 0,
            "image": "rodolpheche/wiremock:latest",
            "healthCheck": {
                "retries": 3,
                "command": [
                    "CMD-SHELL",
                    "curl -f http://localhost:8080/__admin/mappings || exit 1"
                ],
                "timeout": 5,
                "interval": 10,
                "startPeriod": 30
            },
            "essential": true,
            "name": "wiremock"
        },
        {
            "command": [
                "curl", "-X", "POST", "--data", "{ \"request\": { \"url\": \"/test\", \"method\": \"GET\" }, \"response\": { \"status\": 200, \"body\": \"Hello world!\" }}", "http://localhost:8080/__admin/mappings"
            ],
            "cpu": 0,
            "image": "curlimages/curl:latest",
            "dependsOn": [
                {
                    "containerName": "wiremock",
                    "condition": "HEALTHY"
                }
            ],
            "essential": false,
            "name": "curl"
        }
    ],
    "memory": "512",
    "family": "fargate-wiremock-with-curl",
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "networkMode": "awsvpc",
    "cpu": "256"
}

実行例

タスクを起動してしばらくすると、 curl コンテナは Admin API を叩いた後に終了します。

f:id:no_clock:20201001235224p:plain

GET /__admin/mappings で期待通り定義されていることが確認できます。また、 GET /test でも定義通りに WireMock がレスポンスを返却していることがわかります。

$ curl http://example.com:8080/__admin/mappings
{
  "mappings" : [ {
    "id" : "f8819ad0-32ca-408a-b248-d77a05767409",
    "request" : {
      "url" : "/test",
      "method" : "GET"
    },
    "response" : {
      "status" : 200,
      "body" : "Hello world!"
    },
    "uuid" : "f8819ad0-32ca-408a-b248-d77a05767409"
  } ],
  "meta" : {
    "total" : 1
  }
}
$ curl -v http://example.com:8080/test
(省略)
< HTTP/1.1 200 OK
< Matched-Stub-Id: f8819ad0-32ca-408a-b248-d77a05767409
< Vary: Accept-Encoding, User-Agent
< Transfer-Encoding: chunked
<
Hello world!

お手軽で再現性もある

実際に活用しましたが、ちょっとしたモックサーバを準備したいときにピッタリでした。

  • 設定ファイルを S3 からダウンロードしてマウントする、設定ファイルを含む自前の Docker イメージを作る、といった作業は不要
  • 手で Admin API を叩く場合と比較して、モック定義に再現性がある
  • タスク定義に閉じるため、管理するもの・考えるものが少なくて済む

参考

Traefik でお手軽に Docker コンテナの無停止 Blue/Green デプロイを実現した

3 行でまとめると

  • Traefik を使って Docker Compose だけで無停止デプロイを実現
  • docker-compose.yml に定義するだけ、他の設定ファイルは不要のお手軽さ
  • コンテナは graceful shutdown するようにしておく

docker-compose.yml

ざっくり作り上げた docker-compose.yml がこちらです。

gist.github.com

これだけで動きます。リクエストを投げながら nginx-blue, nginx-green のどちらか一方を落としてもエラーになることはありません。

f:id:no_clock:20200603225248p:plain
nginx-blue を停止してもエラーなくレスポンスし続けている様子

Traefik とは

Traefik はある種のリバースプロキシです。ただし、 HAProxy や nginx と違ってサービス検出機能が備わっているほか、環境変数メタデータ (Docker の場合は labels) で設定ができるのも特徴的です。

サービス検出機能と 仲良く なる必要はあるものの、設定の記述量が少なくて済みます。 Docker の場合は、 Traefik 自身が Docker デーモンにアクセス出来るようにします。

    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"

ちょっと気をつけたいところ

  1. 公式ドキュメントのみを頼ったほうが良いです。Google 検索で調べても情報は出てきますが、 frontendbackend といった v1.x 系列の設定を取り上げた記事がヒットして非常に混乱します。
  2. EntryPoints, Routers, Services と複数段定義する必要があり、とっつきにくい印象があるかもしれません。めちゃくちゃ端折って Nginx と対比すると下記の通りです、難しくないと思い込みましょう。
Traefik Nginx
EntryPoints listen ディレクティブ
Routers server ディレクティブ(ホスト名) と location ディレクティブ
Services upstream ディレクティブ

無停止デプロイする

さて、無停止デプロイをしましょう。ポイントは 2 点です。

  • コンテナが graceful shutdown *1 するか、リクエストが冪等であること。
    • traefik はクラウドロードバランサーサービスにあるような接続ドレインの機能を持ちません。コンテナ自身が受け付けたリクエストを正しく処理しきることが望ましく、それが出来ない場合はリクエストの処理を冪等にしておく必要があります。
  • Retry middleware を使う

冒頭の docker-compose.yml では、 nginx に SIGQUIT を投げることで graceful shutdown を実現しています。

    image: "nginx:alpine"
    stop_signal: SIGQUIT
    labels:
(略)
      - "traefik.http.middlewares.nginx-retry.retry.attempts=4"

healthcheck はなくても大丈夫

traefik には healthcheck 機能があり、これを使ったコンテナの切り離しも可能です。

    labels:
(略)
      - "traefik.http.services.nginx-bg.loadbalancer.healthcheck.path=/"
      - "traefik.http.services.nginx-bg.loadbalancer.healthcheck.interval=1s"
      - "traefik.http.services.nginx-bg.loadbalancer.healthcheck.timeout=1s"

ただし、 Retry middleware を使うことで下記の動きとなるため、 healthcheck を設定しなくても問題はありません。

  1. Listen をやめた停止中のコンテナにルーティング
  2. コネクションが確立出来ないため、即座にリトライ
  3. 実行中のコンテナにルーティング

メモリ使用量

かなり雑ですが、 1MB のファイルを 100 同時接続で計 10,000 リクエスト、 10GB 分の転送をさせました。

$ ab -n 10000 -c 100 -H "Host: localhost" http://localhost/
# 起動直後
$ docker stats
CONTAINER ID        NAME                    CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS
903a420aa79c        traefik_nginx-blue_1    0.00%               1.898MiB / 592.5MiB   0.32%               800B / 42B          6.21MB / 0B         2
bb10478ecb4b        traefik_traefik_1       0.03%               10.18MiB / 592.5MiB   1.72%               800B / 0B           46.3MB / 0B         6
7f123e90926c        traefik_nginx-green_1   0.00%               1.777MiB / 592.5MiB   0.30%               578B / 0B           348kB / 0B          2

# リクエスト後
$ docker stats
CONTAINER ID        NAME                    CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS
903a420aa79c        traefik_nginx-blue_1    0.00%               2.191MiB / 592.5MiB   0.37%               4.28MB / 5.21GB     7.27MB / 0B         2
bb10478ecb4b        traefik_traefik_1       0.00%               25.42MiB / 592.5MiB   4.29%               10.5GB / 10.5GB     46.8MB / 0B         19
7f123e90926c        traefik_nginx-green_1   0.00%               2.293MiB / 592.5MiB   0.39%               4.22MB / 5.22GB     1.4MB / 0B          2

nginx のメモリ使用量の少なさが際立ちますが、 traefik も 25MB に収まっています。実行中も 30MB を超えることはありませんでした。

参考

*1:明確な定義はないですが、ここでは、 Listen をやめ新たなリクエストを受け付けないようにした後、既存のリクエストを処理しきってから終了する振る舞いを指します。参考: Apache HTTP Server の停止と再起動 - Apache HTTP サーバ バージョン 2.4

個人開発サービスのログをぜんぶ CloudWatch Logs にまとめた

3 行でまとめると

  • ログを CloudWatch Logs に集約した
  • Metric filter でエラー数をメトリクス化した
  • CloudWatch Alarms ではなく Lambda でエラーを Slack 通知させるようにした

CloudWatch Logs にログを集約した

zenrei.nyamikan.net をすべて Docker 化した のに続いて、 P2P 地震情報にゃみかんの個人開発サービスのログを CloudWatch Logs に集約しました。

むかし

これまでは MonitXymon (旧 Hobbit ) を使ったログ監視を行っていました。

f:id:no_clock:20200531163107p:plain
旧構成

ログファイルとアラート対象のキーワードを指定するシンプルな仕組みでしたが、 長期運用するには少し面倒 でした。あと使っているものが古い 。

  • ディスクフル。気がつくとログがいっぱいになりがち。ログは残しておきたいので logrotate で消すわけにもいかず、定期的に退避する仕組みを用意するのも面倒で、手で退避させる作業を時々やっていた。
  • 再設定の手間Debian GNU/Linux の LTS リリースの度にサーバを構築し直しているが、 2 年に 1 回なのでいつも設定方法を忘れて四苦八苦していた。

いま

いまは CloudWatch へ集約した形にしました(その他、大部分を Docker コンテナ化してポータビリティを高めています)。

f:id:no_clock:20200531151932p:plain
新構成

ログ収集

CloudWatch エージェント もしくは "awslogs" Docker ログドライバ を用いて CloudWatch Logs にログを集約します。この際、サーバ起動時に logs.*.amazonaws.com が解決できない問題にハマりました。 Docker サービス起動前に 30 秒のスリープを入れる雑な方法で対処しています。

エラーの検出

エラー検出には、 CloudWatch のメトリクスフィルタを使用しています。特定のキーワードがあると、メトリクスがカウントされます。

f:id:no_clock:20200531171226p:plain
エラーがメトリクスでカウントされている様子

20 弱あるロググループを手で設定するのは大変なので、 AWS CDK (C#) でコード化しています(参考: AWS CDK for .NET: C# で CloudWatch Metric Filter を設定する - にゃみかんてっくろぐ)。

エラーの通知

ふつうはメトリクスを CloudWatch Alarms で監視させますが、今回は使いませんでした。 CloudWatch Events で Lambda をキックして Slack に通知させています。

  • アラームメトリクスあたり $0.1/month で、今後メトリクスを増やすとそこそこのお値段になってしまう。
  • 個人開発で Lambda を有効活用してみたい。

合理的というよりは 興味本位 の選択です。

コードは Gist に置いていますが、不慣れな JavaScript && 動けばいいやの精神なので可読性はお察しください → Notify CloudWatch Metrics to Slack (typewriter/metrics-to-slack.js)

f:id:no_clock:20200531172812p:plain
ゆかりさんが Slack にエラー発生を通知してくれている図

おねだん

気になるお値段はざっとこんな感じです。

サービス 月額料金 備考
CloudWatch Logs ログ収集 $0.76 無料枠 5GB/ 月 を超え、毎月 6GB ほどのログを収集している
CloudWatch Logs ログ保管 $0.00 無料枠 5GB/ 月 以下(まだ運用して 1 ヶ月ほどで蓄積がないため)
CloudWatch Metrics $0.00 無料枠 10 メトリクス / 月以下。メトリクスフィルタはデータポイントが発生しない部分は課金されない様子
CloudWatch Events $0.04 ? 無料枠なし。まだ請求に上がって来ていないが、カスタムイベント扱い・毎分実行の場合で計算
Lambda $0.00 実行回数 43,200 回(無料枠 1,000,000 回)、実行時間 3,240 GB 秒(無料枠 400,000 GB 秒)

感想

総合すると、そこそこ楽になりました。

  • よいところ
    • ディスクフルを気にしなくていい
    • 1 箇所に集約されている安心感
    • 「とりあえず CloudWatch Logs に流しちゃお」というデフォルトの選択肢が出来た
  • わるいところ
    • 雑に作った通知がエラー数だけで、エラー内容は見に行かないと行けない
  • どちらでもないところ
    • 毎月 6GB もログが発生していることに初めて気づいた

AWS CDK for .NET: C# で CloudWatch Metric Filter を設定する

.NET サポートあるじゃん」と思ったのでちょっと触ってみました。

手順とコード

  1. Getting Started With the AWS CDK - AWS Cloud Development Kit (AWS CDK) を参照して npm で AWS CDK をインストールする。
  2. Working with the AWS CDK in C# - AWS Cloud Development Kit (AWS CDK) を参照してプロジェクトを新規作成し、 NuGet から Amazon.CDK と Amazon.CDK.AWS.Logs パッケージを追加する。
  3. 書く。
  4. 先程のドキュメントや新規作成したプロジェクトの README.md に記載されている通り、 cdk deploy で適用する。
using Amazon.CDK;
using Amazon.CDK.AWS.Logs;

namespace P2PQuakeCDK
{
    public class P2PQuakeCDKStack : Stack
    {
        internal P2PQuakeCDKStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props)
        {
            new CfnMetricFilter(this, "www.p2pquake.net/p2pquake-server/error", new CfnMetricFilterProps()
            {
                FilterPattern = "ERROR",
                LogGroupName = "/vps/p2pquake.net/p2pquake-server",
                MetricTransformations = new CfnMetricFilter.IMetricTransformationProperty[]
                {
                    new CfnMetricFilter.MetricTransformationProperty()
                    {
                        MetricName = "www.p2pquake.net/p2pquake-server/error",
                        MetricNamespace = "app",
                        MetricValue = "1"
                    }
                }
            });
        }
    }
}

触ってみて

Node.js からは逃れられない

いきなり残念なお知らせですが、 .NET バインディングを使う場合でも Node.js からは逃れられません。

All CDK developers need to install Node.js (>= 10.3.0), even those working in languages other than TypeScript or JavaScript. The AWS CDK Toolkit (cdk command-line tool) and the AWS Construct Library are developed in TypeScript and run on Node.js.

Getting Started With the AWS CDK - AWS Cloud Development Kit (AWS CDK)

公式ドキュメントが若干怪しい

例えば CfnMetricFilter クラスのコンストラクタに投げ込む ICfnMetricFilterProps 型を見てみると、 MetricTransformations がなんと object 型になっています。

object MetricTransformations { get; }

Interface ICfnMetricFilterProps

コンソールでは、メトリクス名やメトリクス値を指定するはずの部分です。

f:id:no_clock:20200402000017p:plain
コンソールでの表示

困り果ててしまってアセンブリ ブラウザーで覗いてみると、 CfnMetricFilter.MetricTransformationProperty 型を投げ込めそうに見えます。実際、これで解決しました。

f:id:no_clock:20200402000613p:plain
アセンブリブラウザー ICfnMetricFilterProps

言語共通の API Reference に記載されている Type name も間違っているようです(ネストされていることが表現されていない)。

.NET Amazon.CDK.AWS.Logs.MetricTransformationProperty

interface MetricTransformationProperty · AWS CDK

コントリビューションチャンスか? ちょっと様子を伺ってみようと思います。

参考

ぜんぶ Docker コンテナにする (HTTPS+IPv6 対応 )

事例が何かの参考になればと思ったので記事にしました。

3行でまとめると

すべて Docker 化

個人開発のサービスを省力運用にするべく、 Zenrei (zenrei.nyamikan.net) をすべて Docker コンテナ化しました。

f:id:no_clock:20200308221840p:plain
構成図

docker-compose.yml とそれぞれの Dockerfile はリポジトリに公開しています。

nginx (host network)

https-portal の節で紹介します。

nginx (bridge network)

フロントエンドの静的ファイル配信と、サーバサイドアプリケーションへのリバースプロキシを担います。マルチステージビルドを使用し、 Vue.js の生成物のみを nginx に持ってきてイメージサイズを 23MB と小さくしています。

view/Dockerfile

FROM node:slim as builder
WORKDIR /app
ADD package*.json ./
RUN npm install
ADD . /app
RUN npm run build

FROM nginx:alpine
ADD nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /app/dist /app

api

サーバサイドアプリケーションです。 nginx と同様にマルチステージビルドとしていますが、日本語 WordNet の辞書を用いている関係でイメージサイズは 290MB あります。

aaaton/golem 辞書 (Git LFS) を取っている箇所は妥協しています( go build で Git LFS のデータがどうしても取得できなかった)。個人開発は動けばよかろうの精神であまり深追いしていません。

api/Dockerfile

FROM golang:latest as builder
WORKDIR /go/src
RUN apt-get update && apt-get install -y git-lfs && git lfs install --skip-repo
RUN curl http://compling.hss.ntu.edu.sg/wnja/data/1.1/wnjpn.db.gz > wnjpn.db.gz && gunzip wnjpn.db.gz
RUN mkdir -p /go/src && git clone https://github.com/aaaton/golem.git /go/src/golem
ADD . /go/src
RUN go build . && ls -l /go/src

FROM debian:buster-slim
WORKDIR /go
COPY --from=builder /go/src/api /go/src/wnjpn.db ./
CMD ["./api"]

mongo

ソースコード内の変数名の使用実績などを永続化している MongoDB です。こちらは公式イメージをそのまま使用しています。

https-portal

上記コンテナ群だけでは、 HTTPS で提供することはできません。そこで登場するのが https-portal です。 https-portal は、下記の機能を持った nginx のリバースプロキシ Docker コンテナです。

  • 環境変数による複数ドメインのシンプルなリバースプロキシ
  • Let's Encrypt を用いた https 化(証明書は自動取得・自動更新)

通常は nginx や certbot や cron をゴニョゴニョしなければなりませんが、これを使うと docker-compose.yml に十数行書くだけで済みます (Quick Start 参照 ) 。他のオーケストレーションツールを追加せずに Docker / Docker Compose で完結するのも嬉しいポイントです。

version: "3"

services:
  https-portal:
    image: steveltn/https-portal:latest
    network_mode: host
    ports:
      - "80:80"
      - "443:443"
    environment:
      DOMAINS: "zenrei.nyamikan.net -> http://127.0.0.1:8080"
      STAGE: "production"
      LISTEN_IPV6: "true"

IPv6 対応

https-portal の nginx が IPv4 でしかリッスンしていなかったため、 IPv6 でリッスンするようプルリクエストを出しました(マージ済み🎉)。

github.com

IPv6 と docker-proxy

…ただし、 IPv6 でリッスンしなくても、 IPv6 で通信を受け付けること自体は可能です。ブリッジネットワークでコンテナを起動してポートを開放すると、 IPv4 は NAT で、 IPv6 は docker-proxy による IPv4 への変換で、それぞれ通信がされるようです。

$ sudo iptables -L -n
Chain DOCKER (3 references)
target     prot opt source               destination
ACCEPT     tcp  --  0.0.0.0/0            172.18.0.2           tcp dpt:3001

$ sudo netstat -anp
稼働中のインターネット接続 (サーバと確立)
Proto 受信-Q 送信-Q 内部アドレス            外部アドレス            状態        PID/Program name
tcp6       0      0 :::3001                 :::*                    LISTEN      9181/docker-proxy

この状態で IPv6 通信を行うと、ブリッジアダプタの IPv4 アドレスが通信元になってしまい、それがアクセスログに記録されてしまいます。

172.18.0.1 - - [08/Mar/2020:04:56:33 +0000] "GET /favicon.ico HTTP/2.0" 200 6183

そこで、ホストネットワークでコンテナを起動しつつ、 IPv6 で nginx をリッスンさせることで、これを解消しました。

240f:<MASKED>:7194 - - [08/Mar/2020:07:01:14 +0000] "GET /favicon.ico HTTP/2.0" 200 6183

もう一度まとめると

参考

CRCチェックつきでAM2320の温湿度を読み取る (Raspberry Pi + Python 3)

温湿度センサモジュール AM2320の温湿度を取得するサンプルコードは数あれど、CRCチェックまでやっているコードが見当たりません。

ということで、データシートを見つつ書きました。

import smbus
import time

i2c = smbus.SMBus(1)
address = 0x5c

# Step one: Wake Sensor
try:
    i2c.write_i2c_block_data(address, 0x00, [])
except:
    pass
time.sleep(0.003)

# Step two: Send the read command
i2c.write_i2c_block_data(address, 0x03, [0x00, 0x04])

# Step three: To return the data read
time.sleep(0.015)
block = i2c.read_i2c_block_data(address, 0, 8)

# Check CRC
crc = 0xFFFF
for i in range(6):
  crc ^= block[i]
  for j in range(8):
    if (crc & 0x0001 == 1):
      crc >>= 1
      crc ^= 0xA001
    else:
      crc >>= 1

if block[7] << 8 | block[6] != crc:
  raise RuntimeError("CRC error")

# Print values
humidity = float(block[2] << 8 | block[3]) / 10
temperature = float(block[4] << 8 | block[5]) / 10

print(humidity, "%")
print(temperature, "°C")
$ python3 read_am2320.py
33.9 %
21.3 °C

参考