tail my trail

作るのも使うのも、結局は、人なのだ

HAProxy & socat を使ったオンライン切り離しと Aurora Reader Endpoint における一考

f:id:uorat:20171201173258p:plain

前置き

随分前に RDS Read Replicas や Aurora Replica に対する振り分けを HAProxy で振り分ける方法を投稿した。

この後に、 Aurora Reader endpoint が実装され、 RDS for Aurora を使っている場合に限り、 readers への振り分けに HAProxy などのバランサーをかますことは必須ではなくなっている。 ただ、 Aurora Reader Endpoint も万能ではなく HAProxy を介しておくと小回りが利くことが多いので紹介しておく。

注意

ここで紹介している挙動は 2017 年 5 月時点の自プロダクトで見たものである。 環境固有の要因あり、かつ確認した挙動も古かったり見解が間違っているかもしれないので、あくまで参考程度に留めてほしい。

また、この記事を書いているちょうど今 re:Invent 2017 が開催中で、それに乗じて Aurora に関する Updates も多く出ている。 ちょっと前、2017年11月17日に Aurora Replica の Auto Scaling が可能 にもなっており、当時と今とでは結構状況が変わっている可能性が高い点を留意してほしい。

私と RDS for Aurora

今関わっているプロダクトは 2017 年の 5 月頃にバックエンド DB を RDS for MySQL から RDS for Aurora に移行した。

進め方は概ねデファクトだったと思う。

一通りのテストを終えたあとで、事前に移行元の RDS for MySQL Cluster から Aurora Read Replicas を生やしておき、HAProxy の weight で割合制御して本番トラフィックの参照系クエリの一部を流すようにして、まず様子を見た。 徐々に Aurora に散らすクエリの割合を増やしていき、最終的に 旧 Replicas の weight を 0 した時に まず RDS for MySQL Read Replicas を一掃した。 あとは Master を RDS for MySQL から Aurora writer に切り替える移行作業で、これは他のサービスアップデートのためのデプロイと兼ねて、サービスのピーク帯を避けて2時間程度のメンテナンス時間を設定し書き込み停止点を確保して進めた。

事前にステージング環境で入念なシミュレーションを行っていたこともあり、移行はあっけなく終わった。 細かいところを述べると、その後で本番トラフィックの洗礼を受け、幾つかのクエリチューニングが追加で必要となったりもした。Aurora のバッドノウハウである大量レコードのシーケンシャルリードを行うスロークエリが移行後に顕在化したりして、その手のクエリは抹消したりトランザクション分離レベルを変えたりした。

並列性に優れ、コンピューティングリソースを有効活用してくれる Aurora は RDS for MySQL 時代と比べて 最大で 5 倍のスループット が出るということだが、当然ワークロードによって性能向上率は変わる。 うちの場合だと Aurora の性能を存分に活かしきれるほどのクエリチューニングやアプリケーションリファクタをしまくったわけではないので スループットで言うと多少上がった程度で収まっている。 ただ、設計上 Replica Lag が起きにくいアーキテクチャとなっており、今までだと Read Replicas がサチった場合に Replica Lag が膨れるという副次リスクを問題視していたので、この恩恵を存分に受けることができた。これだけでも移行するメリットがあると感じている。

話が逸れるが、 Aurora Storage の Quorum Model についてまとまった解説がなされており、読むととても面白い。

Aurora における HAProxy の使いどころ

Reader Endpoint がなかった頃は結局 すべての Node Endpoints で振り分けざるを得なく、 また Writer に参照クエリを散らさないためには read_only (innodb_read_only) から識別するしか手がなかった。

以下、参考記事。

なので、このもやもや感を取っ払ってくれた Aurora Reader Endpoint はとても響いたリリースだった。

先程も述べたとおり、今は Aurora Reader endpoint があるので、参照クエリは Read Only なエンドポイントに向けてあげれば基本的には活性化している Aurora Replica にクエリが分散される。 各 Node の障害時は切り離されるし、 Writer 障害時に ある Reader が Writer に昇格すれば、その元 Reader には Reader endpoint 経由でクエリが流れてくることはなくなる。

ただ、いざ使ってみると必ずしも万能ではなく 2017 年 5 月当時に挙動を見る限りいくつか痒いところがあった。 なので、うちでは Reader Endpoint に身を委ねるのではなく、 HAProxy を引き続き間に挟むようにした。

背景と対処方法を紹介しよう。

1. Aurora readers の縮退時、しばらく削除した Reader Node にもクエリが振り分けられることがある

Cluster Endpoint や Reader Endpoint は DNS Endpoint で、ランダムに Reader Node IP を返却することで Readers の分散を実現している。 基本的には非活性な Node の IP は返さないし Reader から Writer に Failover した Node も返さない挙動だが、 DNS record となると DNS Cache や TTL が絡んでくるので、クエリを流すその瞬間に活性な Reader であることを保証してくれるものではない。 かつ、 Replica Node 削除時に実行中のクエリの終了を保証してくれるものでもないと思う。

うちの場合だと、Aurora 移行が一段落して性能もある程度恩恵を受けれるようになったので Aurora Readers を縮退 (削減) させるべく縮退時の挙動を当時見たのだが、 Reader を 幾つか Delete すると実際に何発か DB 接続エラーが出てしまった。

イレギュラーイベントな Writer 障害時の Failover であればまぁ仕方ないのだが、定型的なオペレーション時まで接続エラーが出るのは運用上ちょっと気持ちが悪い。 アプリケーションアラートが飛ぶので精神衛生上もよろしくない。

2. Replica Node の再起動をしたい時に困る

あまり頻度は高くないが、 Replica Node を再起動したい時がある。

例えば、DB Parameter Group を別のものに変更するような時。 特に、Aurora Replica の増設を画面 (RDS Console) で行った場合、Amazon Aurora DB クラスターの作成手順 に従って Cluster Indentifier を指定して Replica Node を追加することになるが、ここで DB Parameter Group を指定が出来ないのでデフォルトの DB Parameter Group (default.aurora) があたった形でReplica Node が起動してしまう。その場合、再起動を伴う DB Parameter Group の変更が必要となる。皆が AWS CLI や CloudFormation, Terraform などで操作していればこの点は困らないのだろうが。

また、Rebooting な Replica Node の IP を Reader Endpoint が返さないことを保証してくれるのか、当時ドキュメントを漁っても見あたらず、少なくとも当時軽く確認した限りでは再起動中の Replica の IP が変えるように見えた。 そして、いずれにしても再起動する以上、処理中のプロセスは kill される。

3. 回避策

なので、結局 HAProxy を間に挟んだ。ポイントは HAProxy の重み付けバランシングを利用するようにしつつ以下のように Backend Node を登録したこと。

  • 通常は HAProxy 経由で Reader Endpoint に流す
  • あわせて weight 0 な全 Node Endpoint を仕込んでおく
  • socat を使ってソケット越しで設定変更できるように操作レベルを admin にしておく

例えばHAProxy の設定はこんな感じ。

こうすると、通常は weight が 1 : 0 : 0 : 0 : 0 なので Reader Endpoint 以外には振り分けられない。 縮退や増設などの構成変更を行う場合は、HAProxy の weight をオンラインで変更して HAProxy から各 Node Endpoint に round-rogin させ、 一方で Reader Endpoint 経由で振り分けないようにする。 socat を使った重み変更なので HAProxy プロセスの再起動/再読込は不要、処理中の接続が中断されることもないのでオンラインで作業できる。

具体的にはこのような流れとなる。

  1. 継続利用する Node Endpoint の weight を 1 にする
  2. Reader Endpoint 経由でクエリが流れないように Reader Endpoint の weight を 0 にする
  3. (縮退する場合は)削除予定の Replica Node にクエリが流れてこないことを確認する
  4. 構成変更作業を進める
  5. 構成変更作業が終わったら、 Reader Endpoint の weight を 1 に戻す
  6. さらに、 Node Endpoint の weight を 0 に戻す

HAProxy Node がいくつもあると操作が面倒なので、Ansible のようなオーケストレーションツールを使うと比較的ラクにオペレーションできると思う。 以下、read4 を削除するためのオンライン weight 変更操作を Ansible で実行する例を載せておく。

最後に

この半年ほどこのオペレーションで、Aurora Replica の台数変更を何回か行っており上手く回っている。 時間も経ってしまったのでいい加減記事におこしておこうと思っていたところで、Aurora の Updates が立て続けに出てきた。

特に Aurora Replica の Auto Scaling サポートは大きい。 増強縮退を前提にしているのであればクエリを中断することもなく上手く処理中のタスクを吐き出してくれる気がするので、今回紹介したような HAProxy を挟む必要性は薄れるかもしれない。 まだ動作確認していないのであくまで希望的観測だが。

それにしても、今年も re:Invent 行きたかった。。

Amazon Web Services完全ソリューションガイド

Amazon Web Services完全ソリューションガイド

Lambda functions (Python) の 依存パッケージの保存場所を指定する

f:id:uorat:20170731120316p:plain

apex.run

Lambda w/ Apex 関連でもう一つ TIPs を備忘録しておく。

Function hooks のおさらい

Apex には Function hooks という機構があり、 Apex で管理する Lambda function のライフサイクルの中で、特定のステップで任意の shell commands を発火させることができる。

以下は Apex.run に記載されている Sample だ。

Shell commands を指定できるので、当然 golang に限らずなんでも実行できる。当然 pip install も可能。

hooks の種類は3種ある。

  • build
    • run before a function zip is built (use this to compile binaries or transform source)
  • deploy
    • run before a function is deployed (useful for testing, linting)
  • clean
    • run after a function is deployed (useful for cleaning up build artifacts)

なので、 Lamnbda Functions w/ Python の場合は以下のように build 時に pip install して functions を梱包して deploy する function.json と requirements.txt を用意することが多いと思う。

pip install したパッケージ群のインストール先を変えたい

ただ、上の方法だと function directory に各パッケージがインストールされてしまう。 function 開発時において重要なものは function.json, Lambda の handler となる python code, あとは依存モジュール記す requirements.txt 。 その他のファイル群が同層に展開されるのは美しくないし、 gitignore 依存パッケージを一つ一つ指定する形になり面倒。

$ tree -L 4
.
├── README.mkd
├── functions
│   └── helloworld-python
│       ├── certifi
│       ├── certifi-2017.7.27.1.dist-info
│       ├── chardet
│       ├── chardet-3.0.4.dist-info
│       ├── function.dev.json
│       ├── function.prd.json
│       ├── function.stg.json
│       ├── idna
│       ├── idna-2.5.dist-info
│       ├── main.py
│       ├── requests
│       ├── requests-2.18.2.dist-info
│       ├── requirements.txt
│       ├── urllib3
│       └── urllib3-1.22.dist-info
├── project.dev.json
├── project.prd.json
└── project.stg.json

例えばこうしたい。

$ tree -L 4
.
├── README.mkd
├── functions
│   └── helloworld-python
│       ├── function.dev.json
│       ├── function.prd.json
│       ├── function.stg.json
│       ├── main.py
│       ├── requirements.txt
│       └── site-packages
│           ├── certifi
│           ├── certifi-2017.7.27.1.dist-info
│           ├── chardet
│           ├── chardet-3.0.4.dist-info
│           ├── idna
│           ├── idna-2.5.dist-info
│           ├── requests
│           ├── requests-2.18.2.dist-info
│           ├── urllib3
│           └── urllib3-1.22.dist-info
├── project.dev.json
├── project.prd.json
└── project.stg.json

やりかた

ざっくり以下。

  1. function hooks を修正
  2. 環境変数 PYTHONPATH を指定

1. function hooks を修正

言うまでもないが、 pip install コマンドを微修正する。

- pip install -r requirements.txt -t ."
+ pip install -r requirements.txt -t ./site-packages"

2. 環境変数 PYTHONPATH を指定

site-packages を検索対象パスに追加する。

+  },
+   "environment": {
+       "PYTHONPATH": "/var/runtime:/var/task/site-packages"

両方をまとめるとこうなる。

これで apex build すれば、依存パッケージは ./site-packages/ 以下に保存されるようになり、かつ 依存パッケージにパスが通るようになる。

肝は

  • 依存パッケージの path を /var/task/site-packages とすること
  • /var/runtime も指定しておくこと

理由は以下に記す。

Lambda Functions を覗いてみる

pip install 時に指定した ./site-packages 以下のパッケージ群を検索対象するべくパスを通すために、展開先の挙動を把握しておく。

簡単な function を実行してみよう。

環境変数や実行ファイルのパスを除くだけのプログラムである。 この function のログはこんな感じとなる。

[INFO]  2017-08-31T04:29:53.519Z        08d61df6-8e05-11e7-a88f-4d9fe5cb6d15    $ENV: environ({'APEX_FUNCTION_NAME': 'python-sample','LAMBDA_FUNCTION_NAME': 'hello-apex_python-sample', 'PATH': '/var/lang/bin:/usr/local/bin:/usr/bin/:/bin', 'LANG': 'en_US.UTF-8', 'TZ': ':UTC', 'LD_LIBRARY_PATH': '/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib', 'LAMBDA_TASK_ROOT': '/var/task', 'LAMBDA_RUNTIME_DIR': '/var/runtime', 'AWS_REGION': 'ap-northeast-1', 'AWS_DEFAULT_REGION': 'ap-northeast-1', 'AWS_LAMBDA_LOG_GROUP_NAME': '/aws/lambda/hello-apex_python-sample', 'AWS_LAMBDA_LOG_STREAM_NAME': '2017/08/31/[12]xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'AWS_LAMBDA_FUNCTION_NAME': 'hello-apex_python-sample', 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE': '128', 'AWS_LAMBDA_FUNCTION_VERSION': '1', '_AWS_XRAY_DAEMON_ADDRESS': '169.254.79.2', '_AWS_XRAY_DAEMON_PORT': '2000', 'AWS_XRAY_DAEMON_ADDRESS': '169.254.79.2:2000', 'AWS_XRAY_CONTEXT_MISSING': 'LOG_ERROR', '_X_AMZN_TRACE_ID': 'Root=1-xxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx;Parent=xxxxxxxxxxxxxxxx;Sampled=0', 'AWS_EXECUTION_ENV': 'AWS_Lambda_python3.6', '_HANDLER': 'main.handle', 'PYTHONPATH': '/var/runtime', 'AWS_ACCESS_KEY_ID': 'ASIAXXXXXXXXXXXXXXXX', 'AWS_SECRET_ACCESS_KEY': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'AWS_SESSION_TOKEN': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx==', 'AWS_SECURITY_TOKEN': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=='})
[INFO]  2017-08-31T04:29:53.519Z        08d61df6-8e05-11e7-a88f-4d9fe5cb6d15    __file__ : /var/task/main.py
[INFO]  2017-08-31T04:29:53.519Z        08d61df6-8e05-11e7-a88f-4d9fe5cb6d15    $PYTHONPATH: /var/runtime
[INFO]  2017-08-31T04:29:53.519Z        08d61df6-8e05-11e7-a88f-4d9fe5cb6d15    sys.path: ['/var/task', '/var/runtime/awslambda', '/var/runtime', '/var/lang/lib/python36.zip', '/var/lang/lib/python3.6', '/var/lang/lib/python3.6/lib-dynload', '/var/lang/lib/python3.6/site-packages']
[INFO]  2017-08-31T04:29:53.519Z        08d61df6-8e05-11e7-a88f-4d9fe5cb6d15    list /var/task/: ['site-packages', 'function.dev.json', 'main.py', 'requirements.txt', 'function.stg.json']
[INFO]  2017-08-31T04:29:53.519Z        08d61df6-8e05-11e7-a88f-4d9fe5cb6d15    list /var/runtime/: ['s3transfer-0.1.10.dist-info', 'liblambdalog.so', 'dateutil', 'botocore', 's3transfer', 'boto3', 'six-1.10.0.dist-info', '__pycache__', 'botocore-1.5.89.dist-info', 'six.py', 'python_dateutil-2.6.1.dist-info', 'liblambdaio.so', 'liblambdaipc.so', 'boto3-1.4.4.dist-info', 'jmespath', 'awslambda', 'liblambdaruntime.so', 'docutils-0.13.1.dist-info', 'docutils', 'jmespath-0.9.3.dist-info']
[INFO]  2017-08-31T04:29:53.519Z        08d61df6-8e05-11e7-a88f-4d9fe5cb6d15    Received event: {
  "region": "ap-northeast-2"
}
END RequestId: 08d61df6-8e05-11e7-a88f-4d9fe5cb6d15
REPORT RequestId: 08d61df6-8e05-11e7-a88f-4d9fe5cb6d15  Duration: 17.21 ms      Billed Duration: 100 ms         Memory Size: 128 MB    Max Memory Used: 20 MB
null

ここから分かるのは、

  • handler となる main.py の path は /var/task/main.py である
  • /var/task に Upload した ZIP が展開される
  • $PYTHONPATH には /var/runtime が元々セットされている
  • /var/runtime には boto3 をはじめとした util 群 が存在する

つまり、 自分で pip install したパッケージの保存先 ./site-packages に path を通したければ PYTHONPATH に /var/task/site-packages を追加すれば良い。 ただ、デフォルトで PYTHONPATH にセットされている /var/runtime には boto3 をはじめとした util 群が含まれるので、このパスも有効にしておく。

なので、先程紹介した function.json のような形に行き着く。

Apex エントリー & 細かい TIPs

f:id:uorat:20170731120316p:plain

apex.run

今までも AWS Lambda は採用していましたが、アプリケーションエンジニアに必要な Policy を付与して、あとは各自よしなにで久しく運用していましたが、自分も本格的に使うようになり、デプロイや運用の効率を考え、今更ながら Apex を使い始めました。

kakakakakku さんの Apex エントリー に触発されたのが大きかった kakakakakku.hatenablog.com

使い始めると痛感するのですが、ホント Apex いいですよね。重すぎずライトな作りで柔軟性も高く、痒いところの大抵の場所に手が届く作りなので、 Serverless Framework を使うまでもないような軽量な functions の開発にはもってこいのフレームだと思います。

ハマるところ

上述の通り、軽量なスクリプティングAWS Lambda 上で行うような時にハマると思います。 例えば、

  • minutely hourly, daily で動かす軽量なスケジューラー
    • curlAPI 叩く程度のものなど
  • ちょっとした運用スクリプト
    • AMI や EBS snapshot 取得するようなバックアップ
    • 開発, 検証環境の up/down
    • EC2 や RDS のメンテナンスウィンドウの検知

軽量な処理だから LL で書いて Lambda Function で実行したいけど、Lambda Console でコピペ運用なんてしたくないし、コーディングからデプロイまで通貫して開発したいし、環境分離したいし、というようなニーズに応えてくれます。というか、基本的にこのレールに載っとくと余計なことを気にしたり実装せずにコアロジックに専念して楽に運用まで持っていけると思います。

代表的な特徴をあげると、

  • AWS Lambda がサポートしていない言語 (Golang) をサポート
  • Build や Build など各アクションに対する command hook をサポート
  • Project 単位, Function 単位で変数や設定値の管理, 分離が可能, Override も可能
  • Binary や Thirdparty Library の梱包が簡単
  • Dry-run 実行や Function の rollback, ログの参照など運用に必要なひととおりの機能あり

Golang サポートもうれしいですが、その他にあげたような痒いとことに手が届く機能は、素の AWS Lambda を使っていると辛みを感じるところなのでとても嬉しい限りです。

Install 〜 Hello World

Apex.run に丁寧に記載されているので、基本的にこれに準じれば特に困ることなく Hello World レベルは始められると思います。Completion (入力補完) も設定しておきましょう。

以下 Mac OS X w/ Homebrew の例です。

$ brew install bash_completion
$ vim /usr/local/etc/bash_completion.d/apex
$ cat /usr/local/etc/bash_completion.d/apex
_apex()  {
  COMPREPLY=()
  local cur="${COMP_WORDS[COMP_CWORD]}"
  local opts="$(apex autocomplete -- ${COMP_WORDS[@]:1})"
  COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
  return 0
}

complete -F _apex apex

$ exec $SHELL -l
$ apex
Apex version 0.15.0
$ apex <TAB>
--chdir      --iamrole    --region     delete       exec         init         logs         upgrade
--dry-run    --log-level  alias        deploy       help         invoke       metrics      version
--env        --profile    build        docs         infra        list         rollback

AWS CLI 使っている人であれば、大抵は $HOME/.aws/_config & credential を設定していると思いますので、その PROFILE を使えば apex init ですぐに始められると思います。 default 入れてない場合は export AWS_PROFILE=${YOUR_PROFILE_NAME}環境変数セットするか、 apex コマンドに --profile (-p) でプロファイル名渡してあげればいつもどおり実行できます。 あとは apex init を実行すると対話モードで Project 名など淡々と入力させられ、AWS上に Lambda 用の IAM Role & Policy と、手元にプロジェクトの雛形ディレクトリが出来上がります。

あとは、ドキュメント見てお作法に習いながら開発を進めていけばOKです。

Example

以下、乱雑なサンプルコードです。

指定した Region の AWSPublic CIDRs を取得するfunction 実装例 & 実行例です。

uorat/hello-apex

echo '{ "region": "ap-northeast-2"}' | apex invoke helloworld-python | jq .
[
  "13.124.0.0/16",
  "13.125.0.0/16",
  "52.78.0.0/16",
  "52.79.0.0/16",
  "52.92.0.0/20",
  "52.94.6.0/24",
  "52.94.198.64/28",
  "52.94.248.176/28",
  "52.95.111.0/24",
  "52.95.192.0/20",
  "52.95.252.0/24",
  "52.219.56.0/22",
  "52.219.60.0/23",
  "54.239.0.192/28",
  "54.239.116.0/22",
  "54.239.120.0/21",
  "52.92.0.0/20",
  "52.219.56.0/22",
  "52.219.60.0/23",
  "13.124.0.0/16",
  "13.125.0.0/16",
  "52.78.0.0/16",
  "52.79.0.0/16",
  "52.95.252.0/24",
  "52.78.247.128/26"
]

かなり柔軟ですが、幾つかの決まり事を以下列挙しときます。

Project (project.json)

その名の通りプロジェクトで、この中に Function を包含できます。プロジェクトの設定値は project.json で設定することが出来ます。このプロジェクトで管理される Function に適用される設定値となります。 代表的な設定は以下です。

  • name: プロジェクト名
  • description: 説明
  • runtime: 実行エンジン。AWS Lambda でネイティブサポートされているものも含めて以下を設定可能
    • java
    • python2.7
    • python3.6
    • nodejs4.3
    • nodejs4.3-edge (Lambda @Edge)
    • nodejs6.10
    • golang (any version)
    • clojure (any version)
    • rust-musl[^rust-runtime][^rust-linux-only] (any version)
    • rust-gnu[^rust-runtime][^rust-linux-only] (any version)
  • memory: メモリ
  • timeout: タイムアウト
  • role: Lambda Function に当てる IAM Role
  • profile: apex 実行時に使用する AWS Profile ($HOME/.aws/)
  • defaultEnvironment: このプロジェクトで使用するデフォルトの環境名 (※後述します)
  • environment: Function 実行時に渡す環境変数
  • nameTemplate: Apex で Lambda Function を作成する場合の Function 命名ルール. デフォルトは {{.Project.Name}}_{{.Function.Name}}

Function (function.json)

Lambda Function ごとに持たせたい設定値を記載します。project.json の設定値を override できます。 例えば、 Function の処理内容によって memory や timeout 値, 環境変数を変えるような使われ方が多いと思います。

Function 固有の設定値は handle , すなわち invoke (呼び出し ≒ 実行) 時の Event Handler 名です。 Apex で標準で使われる handler name は言語ごとに以下が標準値なので、変えたい場合はここに定義して変えましょう。

  • nodejs: index.handle (index.js file with handle exported function)
  • python: handle
  • java: lambda.Main::handler

知っておくと良いこと

幾つか TIPs を載せておきます。

Switch Role (AssumeRole) して apex 実行する方法

IAM Role を切り替えて運用する人は多いと思います。 特に、幾つも AWS アカウントを運用している人は、入り口の Credential だけ発行して、他のアカウントや Role に Switch して運用するのがデファクトだと思います。 このブログでも AWS CLI で Switch Role する方法を紹介したことがあります。

結論、Apex でも Switch できます。ただ、手法が AWS CLI のそれとは異なるので、以下で紹介しておきます。

AWS CLI の場合は、 $HOME/.aws/configrole_arn で切り替え先の IAM Role の ARN を入れた Profile を入れておけば良いのですが、

AWS CLI のprofileを簡単に切り替える (SwitchRole編) - tail my trail

残念ながら Apex は この role_arn は対応していないようで、 NoCredentialProviders error が出てしまいます。

$ echo '{ "region": "ap-northeast-2"}' | apex invoke helloworld-python | jq .
   ⨯ Error: function response: NoCredentialProviders: no valid providers in chain. Deprecated.
        For verbose messaging see aws.Config.CredentialsChainVerboseErrors

どうしたものかとApex のソースを眺めてみると、 AssumeRole や STS のロジックがあるわけですね。

github.com

ドキュメント Apex.run #via-iam-role にもサラッと書いてありました。

Via IAM Role

Using an IAM role can be achieved in two ways, via the AWS_ROLE environment variable or via a command line flag –iamrole. As with other Apex credential loading, the command line flag will supersede the environment variable.

Switch 先の IAM Role ARN を –iamrole で渡してあげれば良いです。つまり、これだけ。

$ apex -p uorat --iamrole arn:aws:iam::987654321098:role/lambda-maintener deploy

IAM Role ARN を入力するのが面倒であれば、環境変数 AWS_ROLE export してしまうだけで良いようです。

環境の分離方法

同じコードベースで 本番, 検証, 開発 と各環境で設定値を切り替えたい場合は、 Apex の environment 機構を使うと良いです。

.
├── README.mkd
├── functions
│   ├── function1
│   │   ├── function.dev.json
│   │   ├── function.prod.json
│   │   ├── function.stage.json
│   │   ├── main.py
│   │   └── requirements.txt
│   └── function2
│        ├── function.dev.json
│        ├── function.prod.json
│        ├── function.stage.json
│        ├── main.py
│        └── requirements.txt
├── project.dev.json
├── project.prod.json
└── project.stage.json

このようにすれば 環境毎に Lambda Function にあてる IAM Role を切り替えたり出来ます。 AWS アカウントを環境毎に分離していればこれだけで十分ですが、もし同一AWSアカウントで各環境を稼働させている場合は、これだけだと AWS Lambda 上の Function name が被ります。

環境毎に function name を変えたい場合は、 project_${ENV_NAME}.jsonnameTemplate をカスタマイズしてあげれば良いです。 デフォルトが {{.Project.Name}}_{{.Function.Name}} なので、これを例えば以下のように変えてあげれば、複数環境の Apex project を同一AWS アカウントで運用できます。

まとめ

Apex が日本で流行りはじめて1年以上経っており、とても今更感のある記事ですが、あらためて Apex 良いなと思ったので、ざっくりエントリー記事にしておきました。 軽量な Serverless scheduler 組もうとしている人はとりあえず Apex 使っておいて損はないと思います。

最後に、これ読んどけば間違いないかと。

Amazon ECSのためのコンテナスケジューラー Blox をDocker for Mac で動かしてみる

f:id:uorat:20170221203052p:plain

tl;dr

  • 遅ればせながら、昨年の re:Invent 2016 で発表された Amazon ECS のためのOSS Container Scheduler Blox を試してみた。
  • AWS 上で動かす事もできるし、手軽に Blox をローカルで動かすこともできる
  • 必要なリソースは CloudFormation Template や Docker compose が用意されているのでさくっと試せるよ you try it

Blox の概要と経緯

公式かつ丁寧な紹介があるので、詳しくはそちらを参照してください。

Blox – Amazon EC2 Container Serviceのための新しいオープンソーススケジューラ | Amazon Web Services ブログ

Blox はできることをシンプルに表わすと、以下の4点です。

  • ECS Cluster のイベントを 検知
  • イベント情報をもとに ECS クラスタの状態を追跡
  • REST APIクラスタの状態を取得
  • スケジューラーによりイベントトリガーで処理を実行

昨年の AWS のApplication Load Balancer の登場 を機に、Web API サーバのようなコンテナを単にロードバランサー配下に並べてしまえば事足りる用途であれば、 ECS の Service Task が Dynamic Port Mapping まで面倒見てくれるのでずいぶん楽になりました。 また、ECS の Service Task は主要どころのコンテナ配備方法を提供してくれています。(Spread, BinPack, One Task Per Host)

ただ、独自のスケジューリングや、他アプリケーションやコンポーネントへの連携など見越すと、所謂コンテナスケジューラー的なものが欲しくなります。

  • 起動/破棄するコンテナインスタンスで 1回だけ実行するタスク
    • Data Volume 内のファイルの退避とか
  • コンテナそれぞれの Endpoint (コンテナインスタンスのIP + Port Mapping) を把握しておきたいケース
    • ELB を介することが難しい持続接続型のプロトコルで待ち受けるもの (例えば RTMP とか)
    • HAProxy や LVS などでコンテナにバランシングしたい時

こうしたケースは ECS API (ListContainerInstances や DescribeContainerInstances) を定期的に呼び出して、ECS コンテナインスタンスやその内部に配置されたコンテナの状態を追っかける必要がありました。

そんな中、 2016/11/25 に Amazon ECSイベントストリームで、クラスタの状態を監視 することができるようになりました。CloudWatch Events と連携できるようになり、ECS Eventをトリガーに独自の処理を発火することができるようになりました。 さらにその直後、 2016/12/2 re:Invent 2016 の Keynote Day 2 で ECS イベントストリームを活用した Blox が発表され、この恩恵を手軽に受けることができるようになりました。

Blox - Open Source Tools for Amazon ECS

つまり、上に挙げたユースケースを実現するにあたり必要だったスケジューラーの実装が Blox を使えば楽になるということです。

Blox の仕組み

Blox の構成要素と役割は以下の3つです。

  • cluster-state-service (abbr. css)
    1. ECSイベントの監視および Blox DB (etcd) への保存
    2. Blox API
  • etcd
    1. ECSイベントを格納するDB (Key-Value Store)
  • scheduler
    1. ECSイベントをトリガーに処理を行うスケジューラー

それぞれかいつまんで紹介します。

cluster-state-service (abbr. css)

CloudWatch Events をもとにクラスタインスタンスやコンテナの情報を追跡し、Blox DB (etcd) に保存します。 また、API も提供しており、css を通じて etcd に保存されたECSクラスタの内部ステータスを取得することが出来ます。

以下、README.md より引用。

The cluster-state-service consumes events from a stream of all changes to containers and instances across your Amazon ECS clusters, persists the events in a local data store, and provides APIs (e.g., search, filter, list, etc.) that enable you to query the state of your cluster so you can respond to changes in real-time. The cluster-state-service utilizes etcd as the data store to track your Amazon ECS cluster state locally, and it also manages any drift in state by periodically reconciling state with Amazon ECS.

css 自身は SQS Queue を監視する仕組みとなっています。 以下のようなEvent Pattern を SQS Queue に流す CloudWatch Events Rule を用意しておくことで、css が ECS のイベントを拾うことができるようになります。

{
    "detail-type": [
        "ECS Task State Change",
        "ECS Container Instance State Change"
    ],
    "source": [
        "aws.ecs"
    ]
}

etcd

Blox DB には Docker 使いなら馴染みのある人が多い、Golang 製の分散KVS etcd が採用されています。css が取得したECSイベントはここに格納されます。

coreos/etcd: Distributed reliable key-value store for the most critical data of a distributed system

scheduler (daemon-scheduler)

css/etcd に流れてきたECSイベントをトリガーに、独自の処理を行うスケジューラーです。

Blox としてサポートしているスケジューラーは今は daemon-scheduler のみで、冒頭に挙げたコンテナインスタンスそれぞれに “1つずつ” 稼働するコンテナを稼働させるケースにマッチするスケジューラーです。

以下、README.md より引用。

The daemon-scheduler allows you to run exactly one task per host across all nodes in a cluster. It monitors the cluster state and launches tasks as new nodes join the cluster, and it is ideal for running monitoring agents, log collectors, etc. The daemon-scheduler can be used a reference for how to use the cluster-state-service to build custom scheduling logic.

daemon-scheduler の機能自体も便利ですが、daemon-scheduler の実装がカスタムスケジューラーのリファレンスとしても使えるということです。

Architecture

本家 から図を拝借します。

f:id:uorat:20170221203112p:plain

Blox 自体は Golang 製の3つのプロセスが動けばよいだけなので、極端な話 SQS Queue に通信できる環境であれば ローカルでもAWS上に立てたEC2上でもどこでも動きます。 さらに、Blox 自体の Docker compose や Docker Image が公開されているので、Blox の Docker コンテナをローカルで動かすことも出来ますし、Blox 自身を ECS で動かすことも出来ます。 さすが本家が開発しているOSSなだけあって、必要な CloudFormation テンプレートもバンドルされています。

blox/deploy at blox/blox | Github

Local の Docker for Mac 環境に Blox を 動かしてみる

前置きが大変長くなりましたが、今回の本題。

blox/deploy/README.md に紹介されている Local Installation を試してみます。

手順はざっくり以下の2点です。CloudFormation Template と Docker Compose が用意されているのでとても簡単。

  1. BloxGithub Repository を clone
  2. Blox が使用する AWS Resources を作成 (by CloudFormation)
    • SQS Queue
    • SQS Queue にイベントを流すための CloudWatch Events Rule
  3. Blox Components をローカルで動かす (by Docker Compose)
    • cluster-state-service (abbr. css)
    • etcd
    • daemon-scheduler

1. BloxGithub Repository を clone

uorat-mbp:~ uorat$ cd app/docker/
uorat-mbp:docker uorat$ git clone https://github.com/blox/blox.git
Cloning into 'blox'...
remote: Counting objects: 4984, done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 4984 (delta 0), reused 0 (delta 0), pack-reused 4979
Receiving objects: 100% (4984/4984), 3.94 MiB | 734.00 KiB/s, done.
Resolving deltas: 100% (2488/2488), done.
Checking connectivity... done.
uorat-mbp:~ uorat$ cd blox/

2. Blox が使用する AWS Resources を作成 (by CloudFormation)

CloudFormation Template が用意されているので、これを使うだけです。 ${BLOX_HOME}/deploy/docker/conf/cloudformation_template.json にあります。 region や profile を指定して実行すればOK

uorat-mbp:blox uorat$ aws --region ap-northeast-1 --profile uorat cloudformation create-stack --stack-name BloxLocal --template-body file://./deploy/docker/conf/cloudformation_template.json
{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/BloxLocal/abcdefgh-abcd-abcd-abcd-abcdefghijkl"
}

StackStatus が 完了するのを待つ。

uorat-mbp:blox uorat$ aws --region ap-northeast-1 --profile uorat cloudformation describe-stacks --stack-name BloxLocal | jq ".Stacks[] | .StackId, .Description, .StackName, .StackStatus"
"arn:aws:cloudformation:ap-northeast-1:123456789012:stack/BloxLocal/abcdefgh-abcd-abcd-abcd-abcdefghijkl"
"Template to deploy Blox framework locally"
"BloxLocal"
"CREATE_COMPLETE"

3. Blox Components をローカルで動かす (by Docker Compose)

docker-compose.yml が用意されているので、これを使えば良いです。

composeファイル内に、 AWS Region と Profile を指定する必要があるので、自分の環境にあわせて変更しておくこと。 docker-compose.yml を見ると分かるとおり、cssdaemon-scheduler も Docker ホストの $HOME/.aws を read-only でマウントしています。 AWS_PROFILE で指定したプロファイル名に対応する credential を使用して SQS や ECS と対話する仕掛けになっています。

README.md にも書いてあります。

  • Update the AWS_REGION value with the region of your ECS and SQS resources.
  • Update the AWS_PROFILE value with your profile name in ~/.aws/credentials. You can skip this step if you are using the default profile.
uorat-mbp:blox uorat$ cd deploy/docker/conf/
uorat-mbp:blox uorat$ vim docker-compose.yml

uorat-mbp:conf uorat$ git diff
diff --git a/deploy/docker/conf/docker-compose.yml b/deploy/docker/conf/docker-compose.yml
index b626ac2..7a16857 100644
--- a/deploy/docker/conf/docker-compose.yml
+++ b/deploy/docker/conf/docker-compose.yml
@@ -5,8 +5,8 @@ services:
     ports:
       - "2000:2000"
     environment:
-      AWS_REGION: "<region>"
-      AWS_PROFILE: "default"
+      AWS_REGION: "ap-northeast-1"
+      AWS_PROFILE: "uorat"
     command: [
       "--bind", "0.0.0.0:2000",
       "--css-endpoint", "css:3000",
@@ -25,8 +25,8 @@ services:
     ports:
       - "3000:3000"
     environment:
-      AWS_REGION: "<region>"
-      AWS_PROFILE: "default"
+      AWS_REGION: "ap-northeast-1"
+      AWS_PROFILE: "uorat"
     command: [
       "--bind", "0.0.0.0:3000",
       "--etcd-endpoint", "etcd:2379",

あとは docker-compose コマンドでコンテナを立ち上げるだけ。

uorat-mbp:conf uorat$ docker-compose up -d
Creating network "conf_default" with the default driver
Pulling etcd (quay.io/coreos/etcd:v3.0.15)...
v3.0.15: Pulling from coreos/etcd
3690ec4760f9: Pull complete
53b6b297c402: Pull complete
0ee7413b6e7d: Pull complete
0b6d568d289d: Pull complete
f79877e4a632: Pull complete
Digest: sha256:aed90a29fbe7ad0e6629b2ea5a290f5b6efb9b719cec97c756df13f1db3760bf
Status: Downloaded newer image for quay.io/coreos/etcd:v3.0.15
Pulling css (bloxoss/cluster-state-service:0.2.0)...
0.2.0: Pulling from bloxoss/cluster-state-service
2551f207d95c: Pull complete
559c70e440db: Pull complete
b778135d24c5: Pull complete
67ed40d0e0b3: Pull complete
Digest: sha256:63aebbcf800a64d51acb20d5d7ae2c261fb272fd6aa020c533d4721c22279114
Status: Downloaded newer image for bloxoss/cluster-state-service:0.2.0
Pulling scheduler (bloxoss/daemon-scheduler:0.2.0)...
0.2.0: Pulling from bloxoss/daemon-scheduler
a4d560910c77: Pull complete
bcfbf1954f9f: Pull complete
e199cf3f7828: Pull complete
1bb380f5b15e: Pull complete
Digest: sha256:4e3d0314ceb8ab62bc296c6f351d87ee89c21616f28f3a9bd425f7edc8281636
Status: Downloaded newer image for bloxoss/daemon-scheduler:0.2.0
Creating conf_etcd_1
Creating conf_css_1
Creating conf_scheduler_1

docker ps コマンドで確認。 css は 3000/tcpdaemon-scheduler は 2000/tcp で待ち受けます。

uorat-mbp:conf uorat$ docker-compose ps
      Name                    Command               State                Ports
--------------------------------------------------------------------------------------------
conf_css_1         /cluster-state-service --b ...   Up      0.0.0.0:3000->3000/tcp
conf_etcd_1        /usr/local/bin/etcd --data ...   Up      0.0.0.0:2379->2379/tcp, 2380/tcp
conf_scheduler_1   /daemon-scheduler --bind 0 ...   Up      0.0.0.0:2000->2000/tcp

なお、今回は既に ECS Cluster を稼働させているアカウントを使っていたので特にハマることなく3つのコンテナが稼働してくれましたが、 @smiyaguchi さんによると 先にECS で何かしらのクラスタが起動していないと css_1 の起動に失敗するそうです。

Bloxを使ってみました - smiyaguchi’s blog

また、ECSやSQSへのアクセス権限が足りなかったり、profile の指定が間違っていても同様に以下のようなエラーが出て css_1 がコケます。

css_1        | 2017-02-21T08:55:56Z [INFO] Reconciler loading tasks and instances
css_1        | 2017-02-21T09:00:58Z [CRITICAL] Error starting event stream handler: NoCredentialProviders: no valid providers in chain. Deprecated.
css_1        |  For verbose messaging see aws.Config.CredentialsChainVerboseErrors
css_1        | Failed to list ECS clusters.
css_1        | github.com/blox/blox/cluster-state-service/handler/reconcile/loader.clientWrapper.listClusters
css_1        |  /go/src/github.com/blox/blox/cluster-state-service/handler/reconcile/loader/ecs_wrapper.go:75
css_1        | github.com/blox/blox/cluster-state-service/handler/reconcile/loader.clientWrapper.ListAllClusters
css_1        |  /go/src/github.com/blox/blox/cluster-state-service/handler/reconcile/loader/ecs_wrapper.go:54

複数AWSアカウントを運用していると、Switch Role で切り替えられるように .aws を書いたりすると思いますが、

uorat.hatenablog.com

そのノリで profile name を指定しても 該当する credential が取得できずに css が落ちてしまうので気をつけてください。 (※しばらくハマった残念な人です。)

うまく動くとこんな感じで動きます。

uorat-mbp:conf uorat$ docker-compose logs css
Attaching to conf_css_1
css_1        | 2017-02-21T09:29:15Z [INFO] Reconciler loading tasks and instances
css_1        | 2017-02-21T09:29:15Z [INFO] Bootstrapping completed
css_1        | 2017-02-21T09:29:15Z [INFO] Starting to poll for events from SQS
css_1        | 2017-02-21T09:29:47Z [INFO] SQS attribute[ApproximateNumberOfMessages] = 0
css_1        | 2017-02-21T09:29:47Z [INFO] SQS attribute[ApproximateNumberOfMessagesDelayed] = 0
css_1        | 2017-02-21T09:29:47Z [INFO] SQS attribute[ApproximateNumberOfMessagesNotVisible] = 0
css_1        | 2017-02-21T09:30:17Z [INFO] SQS attribute[ApproximateNumberOfMessages] = 0
css_1        | 2017-02-21T09:30:17Z [INFO] SQS attribute[ApproximateNumberOfMessagesDelayed] = 0
css_1        | 2017-02-21T09:30:17Z [INFO] SQS attribute[ApproximateNumberOfMessagesNotVisible] = 0
css_1        | 2017-02-21T09:30:47Z [INFO] SQS attribute[ApproximateNumberOfMessagesDelayed] = 0
css_1        | 2017-02-21T09:30:47Z [INFO] SQS attribute[ApproximateNumberOfMessagesNotVisible] = 0
css_1        | 2017-02-21T09:30:47Z [INFO] SQS attribute[ApproximateNumberOfMessages] = 0

uorat-mbp:conf uorat$ docker-compose logs scheduler
Attaching to conf_scheduler_1
scheduler_1  | 2017-02-21T09:29:16Z [INFO] Started dispatcher

css 経由でECS Cluster の情報を参照してみる

css で提供されている REST API を使って ECS の情報を参照してみます。 APIcss の README.md に書いてあるとおり swagger.json にて一覧が確認できます。

After you launch the cluster-state-service, you can interact with and use the REST API by using the endpoint at port 3000. Identify the cluster-state-service container IP address and connect to port 3000. For more information about the API definitions, see the swagger specification.

例えば コンテナインスタンスの一覧を取得する場合は “http://localhost:3000/v1/instances” です。 以下 swagger.json より引用。

"/instances": {
  "get": {
    "description": "Lists all instances, after applying filters if any",
    "operationId": "ListInstances",
    "parameters": [
      {
        "name": "status",
        "in": "query",
        "description": "Status to filter instances by",
        "type": "string"
      },
      {
        "name": "cluster",
        "in": "query",
        "description": "Cluster name or ARN to filter instances by",
        "type": "string"
      }
    ],
    "responses": {
      "200": {
        "description": "List instances - success",
        "schema": {
          "$ref": "#/definitions/ContainerInstances"
        }
      },
      "400": {
        "description": "List instances - bad input",
        "schema": {
          "type": "string"
        }
      },
      "500": {
        "description": "List instances - unexpected error",
        "schema": {
          "type": "string"
        }
      }
    }
  }
},

試しに問い合わせしてみると、ECS API ライクに ECS Instance の情報が json で返ります。以下、jq で整形した例。

uorat-mbp:~ uorat$ curl http://localhost:3000/v1/instances 2>/dev/null | jq '.items[].entity | [.EC2InstanceID, .clusterARN, .containerInstanceARN, .status, .versionInfo.dockerVersion]'
[
  "i-xxxxxxxxxxxxxxxxx",
  "arn:aws:ecs:ap-northeast-1:123456789012:cluster/ecs-demo-php-simple-app-cluster",
  "arn:aws:ecs:ap-northeast-1:123456789012:container-instance/abcdefgh-abcd-abcd-abcd-hogehogehoge",
  "ACTIVE",
  "DockerVersion: 1.12.6"
]
[
  "i-yyyyyyyyyyyyyyyyy",
  "arn:aws:ecs:ap-northeast-1:123456789012:cluster/openrec-station-ecs-demo-php-simple-app-cluster",
  "arn:aws:ecs:ap-northeast-1:123456789012:container-instance/abcdefgh-abcd-abcd-abcd-fugafugafuga",
  "ACTIVE",
  "DockerVersion: 1.12.6"
]

同じように task もとれました。

daemon-scheduler を試す

長くなったので、次回にします。。

まとめ

BloxAmazon ECS のためのOSS Container Scheduler です。 今まで ECS では手の届かなかった(ユーザー任せになっていた)独自のスケジューリングが組み込みやすくなり、他アプリケーションやコンポーネント間の連携まで含めた所謂オーケストレーションが可能となるでしょう。

肝心の scheduler は今回は記載しきれなかったのでまたの機会にしますが、Blox の稼働に必要なものは全て Dockerize & Template 化されているので、さくっと試せます。 ローカルでも動くので、とりあえずカジュアルにお試しを。

AWS CLI のprofileを簡単に切り替える (SwitchRole編)

これまた意外と知らない人がちらほらいたので、書き留めておく。

tl;dr

$HOME/.aws/config で Switch Role な 設定をいれるだけ。

背景

Switch Role という機能を使うと、IAM User から特定のRole に切り替えることができる。

f:id:uorat:20170207160055p:plain

Management Console使ってると別アカウントの切り替えのためにログインし直すのは面倒だし、 アプリケーションのために AccessToken を発行しまくるのも管理上不便でリスクも増える(Token 漏洩すると悲惨)ので、自分はもっぱら IAM User は極力作成せず IAM Role に寄せるようにしている。

また、ログイン用の IAM User は ReadOnly にしておいて、操作が必要なときだけ パワフルな権限を有する Role に切り替えるなどしておくなどしておくと保険にもなる。

見た目もわかりやすいし。

f:id:uorat:20170207160253p:plain

で、今回のお題は、このSwitch Role を AWS CLI でどう扱うか。

やりかた

結構前に、AWS CLI の profile 切替方法を書いたが、

uorat.hatenablog.com

この応用でいける。

$HOME/.aws/config に、以下のように Switch 元の Profile と Switch 先のIAM Role の ARN を書いてあげれば良い。

[profile uorat-account-parent]
region = ap-northeast-1

[profile uorat-account-child-a]
role_arn = arn:aws:iam::123456789012:role/switchRoleReadOnly
source_profile = uorat-account-parent
region = ap-northeast-1

あとは、いつもどおり、aws コマンドを実行する時に –profile を指定すれば良いだけ。 source_profile で指定した IAM User で Switch Role され、Switch 先の Role での作業が可能となる。シンプル。

$ aws ec2 describe-vpcs --profile=uorat-account-parent
{
    "Vpcs": [
        {
            "VpcId": "vpc-xxxxxxxx",
            "InstanceTenancy": "default",
            "State": "available",
            "DhcpOptionsId": "dopt-xxxxxxxx",
            "CidrBlock": "172.31.0.0/16",
            "IsDefault": true
        }
    ]
}

$ aws ec2 describe-vpcs --profile=uorat-account-child-a
{
    "Vpcs": [
        {
            "VpcId": "vpc-yyyyyyyy",
            "InstanceTenancy": "default",
            "Tags": [
                {
                    "Value": "uorat-child-a",
                    "Key": "Name"
                }
            ],
            "State": "available",
            "DhcpOptionsId": "dopt-yyyyyyyy",
            "CidrBlock": "172.17.0.0/16",
            "IsDefault": false
        },
        {
            "VpcId": "vpc-zzzzzzzz",
            "InstanceTenancy": "default",
            "State": "available",
            "DhcpOptionsId": "dopt-yyyyyyyy",
            "CidrBlock": "172.31.0.0/16",
            "IsDefault": true
        }
    ]
}

この方法は公式ドキュメントにも書いてあるが、意外と知らない人が多かったので書き留めておきました。

参考: IAM ロールの切り替え(AWS Command Line Interface) - AWS Identity and Access Management

Docker for Mac 使っている場合の入力補完

f:id:uorat:20170131164247p:plain

tl:dr

Docker.app 内に bash_completion がバンドルされているので、それ使ってね。

背景

たいてい、以下のようなコマンドで completion 設定すると思うが、

curl -L https://raw.githubusercontent.com/docker/docker/master/contrib/completion/bash/docker > /etc/bash_completion.d/docker
curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose version --short)/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose
files=(docker-machine docker-machine-wrapper docker-machine-prompt)
for f in "${files[@]}"; do
  curl -L https://raw.githubusercontent.com/docker/machine/v$(docker-machine --version | tr -ds ',' ' ' | awk 'NR==1{print $(3)}')/contrib/completion/bash/$f.bash > `brew --prefix`/etc/bash_completion.d/$f
done

Docker for Mac の場合どうなんだろと気になって軽く調べた。

方法

またしてもググラビリティが低かったらしく、なかなかピンポイントな情報に出会えなかった。 結論、Docker for Mac の公式ドキュメントに書いてあるとおり、Docker.app 内にバンドルされているので、それを使おう。

Installing bash completion: Get started with Docker for Mac - Docker

If you are using bash completion, such as homebrew bash-completion on Mac bash completion scripts for the following commands may be found inside Docker.app, in the Contents/Resources/etc/ directory:

  • docker
  • docker-machine
  • docker-compose

homebrew で bash_completion 入れていれば、こんな感じで bash_completion.d/ ディレクトリに symlink 貼ってあげれば良い。

ln -s /Applications/Docker.app/Contents/Resources/etc/docker.bash-completion /usr/local/etc/bash_completion.d/docker
ln -s /Applications/Docker.app/Contents/Resources/etc/docker-machine.bash-completion /usr/local/etc/bash_completion.d/docker-machine
ln -s /Applications/Docker.app/Contents/Resources/etc/docker-compose.bash-completion /usr/local/etc/bash_completion.d/docker-compose

実行例

コマンド補完の例

$ docker [TAB]
attach     create     export     import     login      pause      push       run        stack      system     version
build      deploy     help       info       logout     plugin     rename     save       start      tag        volume
commit     diff       history    inspect    logs       port       restart    search     stats      top        wait
container  events     image      kill       network    ps         rm         secret     stop       unpause
cp         exec       images     load       node       pull       rmi        service    swarm      update

パラメーター補完の例

# 以下のコンテナがある状態で

$ docker ps -a
CONTAINER ID        IMAGE                           COMMAND                  CREATED              STATUS              PORTS                NAMES
dbd529912c30        uorat/ecs-demo-php-simple-app   "/usr/sbin/apache2..."   About a minute ago   Up About a minute   0.0.0.0:80->80/tcp   hopeful_kalam

# "docker stop" まで打って TAB 押すと
$ docker stop [TAB]
↓
$ docker stop hopeful_kalam

# もう一つコンテナ立ち上げる
$ docker run -d -p81:80 [TAB]
alpine                                          uorat/ecs-demo-php-simple-app         ubuntu
alpine:latest                                   uorat/ecs-demo-php-simple-app:latest  ubuntu:12.04

$ docker run -d -p81:80 uorat/ecs-demo-php-simple-app
01a8b01975c07667574042bced6ba14d9056f87b43053b46e1abd6a29800fa6f

# 確認
$ docker ps
CONTAINER ID        IMAGE                           COMMAND                  CREATED             STATUS              PORTS                NAMES
01a8b01975c0        uorat/ecs-demo-php-simple-app   "/usr/sbin/apache2..."   2 minutes ago       Up 2 minutes        0.0.0.0:81->80/tcp   elastic_chandrasekhar
dbd529912c30        uorat/ecs-demo-php-simple-app   "/usr/sbin/apache2..."   5 minutes ago       Up 5 minutes        0.0.0.0:80->80/tcp   hopeful_kalam

# "docker stop" まで打って TAB 押すと
$ docker stop [TAB]
elastic_chandrasekhar  hopeful_kalam

# 止める
$ docker stop [TAB]
elastic_chandrasekhar  hopeful_kalam
$ docker stop hopeful_kalam
hopeful_kalam

# 止まってるコンテナを再度立ち上げる
$ docker start [TAB]
↓
$ docker start hopeful_kalam ← ちゃんと止まってるコンテナだけフィルタリングされる

ちゃんちゃん。

Docker for Mac v1.13 でディスクイメージの自動圧縮に対応されたのであげておくべし

f:id:uorat:20170131164247p:plain

tl;dr

「Docker for Mac 使ってる場合のimage群の保存場所と掃除方法」というタイトルでブログに書き殴っておこうと思ったら、実は Docker for Mac v1.13 から自動圧縮に対応していたというオチ。

Docker for Mac 使いは 今すぐ v 1.13 に上げよう。

背景

Docker および Amazon ECS 素振り中の身。 数年前に 軽く触った程度であまり詳しくないけど所用でコンテナを本番運用することになりそうなので、浦島太郎状態から抜け出すべくキャッチアップしつつ、ついでに開発機のMac OS X のDocker環境を Docker Toolbox から Docker for Mac に移行していた。

ゴミが散らかっていたのでコンテナやイメージを掃除していたら、なかなか開放されず困ったちゃんだったので色々調べていた。

Where are Docker images?

通常Linux をDocker ホストに使ってる場合は、 /var/lib/docker にイメージが保存される。 けど、Docker for Mac 入れたMac OS Xに /var/lib/docker が見当たらなかったので、気になってファイル漁ってみた。 そもそも Docker コマンドってどこにあるんだろう。

$ which docker
/usr/local/bin/docker
$ ll /usr/local/bin/docker
lrwxr-xr-x  1 uorat  Users  66  1 30 17:01 /usr/local/bin/docker -> /Users/uorat/Library/Group Containers/group.com.docker/bin/docker

$HOME/Library/Group Containers にあるらしい。 $HOME/Library/Containers というディレクトリもあったので、ここを掘り下げて見てみると

$ ll -h ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/
total 1998512
drwxr-xr-x  13 uorat  Users   442B  1 30 17:01 .
drwxr-xr-x  22 uorat  Users   748B  1 30 17:01 ..
-rw-r--r--   1 uorat  Users   976M  1 31 14:04 Docker.qcow2
-rw-r--r--   1 uorat  Users    64K  1 30 17:01 console-ring
-rw-r--r--   1 uorat  Users    64K  1 30 16:45 console-ring.0
-rw-r--r--   1 uorat  Users     3B  1 30 17:01 hypervisor.pid
-rw-r--r--   1 uorat  Users     0B  1 30 16:45 lock
drwxr-xr-x   2 uorat  Users    68B  1 30 16:45 log
-rw-r--r--   1 uorat  Users    17B  1 30 17:01 mac.0
-rw-r--r--   1 uorat  Users    36B  1 30 16:45 nic1.uuid
-rw-r--r--   1 uorat  Users     3B  1 30 17:01 pid
-rw-r--r--   1 uorat  Users    94B  1 30 17:01 syslog
lrwxr-xr-x   1 uorat  Users    12B  1 30 17:01 tty -> /dev/ttys000

更新日やファイルサイズ見る限りなんかそれっぽいのがある。 qcow2 という拡張子見る限り、QEMUのイメージファイルらしい。内部的にQEMU使ってるのか。

フォーラム漁ってみる

Dockerのフォーラム見てみると、似たようなスレッドがあってまさにこのファイルがdefault指定されていると言及されていた。

Where are images stored on Mac OS X? - Docker for Mac - Docker Forums

It depends on the driver being used, but the default is: $HOME/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/Docker.qcow2

Try it

ごりごり docker rmi で不要なイメージ消してみる。 元々1GBくらいに育ってたのですっきり。

$ docker images prune
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
alpine              latest              70c557e50ed6        11 months ago       4.8 MB

では、実体のファイル見てみると、

$ ll -h Docker.qcow2
-rw-r--r--  1 uorat  Users   976M  1 31 14:45 Docker.qcow2

減らないうぇーい。

Docker for Mac の仕組み

Mac OS X の xhyve 使って Linux 仮想マシン立ち上げている

QEMUファイルがあることから推察できるとおり、Docker for Mac も所詮は仮想マシン上にContainer立ち上げる仕組みのよう。 昔使ってた Docker Toolbox はVirtualBoxバックグラウンドで使って実現していたわけだけども、Docker for Mac は 内部的には xhyve という Mac OS X の Hypervisor 使って Dockerホスト用のLinux を立ち上げているらしい。

引用: The Docker Platform

DOCKER FOR MAC An integrated, easy-to-deploy environment for building, assembling, and shipping applications from a Mac, Docker for Mac is a native Mac application architected from scratch, with a native user interface and auto-update capability, deeply integrated with OS X native virtualization, Hypervisor Framework, networking and file system, making it faster and more reliable than previous ways of getting Docker on a Mac.

引用: Docker for Mac and Windows Beta: the simplest way to use Docker on your laptop - Docker Blog

Faster and more reliable: no more VirtualBox! The Docker engine is running in an Alpine Linux distribution on top of an xhyve Virtual Machine on Mac OS X or on a Hyper-V VM on Windows, and that VM is managed by the Docker application. You don’t need docker-machine to run Docker for Mac and Windows.

なので、screen tty するとLinuxに入れる。

$ cd ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/
$ screen tty

...

 * Running system containerd ... [ ok ]
 * Running system containers ... binfmt rng-tools [ ok ]
 * Configuring host settings from database ... [ ok ]
 * Setting up proxy port service ... [ ok ]
 * Starting Docker ... [ ok ]
 * Starting chronyd ... [ ok ]
 * Checking system state ...
✓ Drive found: sda
✓ Drive mounted: /dev/sda1 on /var type ext4 (rw,relatime,data=ordered)
✓ Network connected:           inet addr:192.168.65.2  Bcast:192.168.65.7  Mask:255.255.255.248
✓ Process transfused running
✓ Process dockerd running: dockerd --pidfile=/run/docker.pid -H unix:///var/run/docker.sock --swarm-default-advertise-addr=eth0 --userland-proxy-path /usr/bin/slirp-proxy --debug --experimental --storage-driver aufs
✓ Process containerd running: docker-containerd -l unix:///var/run/docker/libcontainerd/docker-containerd.sock --metrics-interval=0 --start-timeout 2m --state-dir /var/run/docker/libcontainerd/containerd --shim docker-containerd-shim --runtime docker-runc --debug
✓ Docker daemon working: 1.13.0
✓ Diagnostics server running: /usr/bin/diagnostics-server -vsock
✓ System containerd server running: /usr/bin/containerd
✓ System containerd working
 * Starting Hyper-V daemon: hv_kvp_daemon ... [ ok ]
 * Starting Hyper-V daemon: hv_vss_daemon ... [ ok ]
 * Adjusting oom killer settings ... [ ok ]

Welcome to Moby

                        ##         .
                  ## ## ##        ==

/ # hostname
moby

/ # cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.5.0
PRETTY_NAME="Alpine Linux v3.5"
HOME_URL="http://alpinelinux.org"
BUG_REPORT_URL="http://bugs.alpinelinux.org"

/ # (Ctl-a + D)

Mac OS XQEMUディスクイメージをリサイズする方法 (従来)

ググってみたら、まさに同じようなことを言及している記事を発見。 ここでは qemu で仮想ディスクをリサイズする方法が紹介されている。KVM触ってた頃を思い出す。

Docker for macでDocker.qcow2というファイルが肥大化する件 | GENDOSU@NET

Github Issue でも同じ案内を見つけた。DockerホストとなるLinuxサーバにログインして /var/tmpfile を0埋めした後で、Mac OS X上の Docker.qcow2 ファイルを qemu-img コマンドで convert しろというもの。

Docker.qcow2 never shrinks - disk space usage leak in docker for mac · Issue #371 · docker/for-mac

試してみたらいつの間にか勝手に圧縮されていた

結論、ブログタイトル & この見出しのような挙動をした。 一応途中までやったこと書き記しておく。

従来の圧縮方法の下準備

まずは brewqemu 入れた。

$ brew install qemu

リサイズする前に、念のため Linux仮想マシン上に無駄なファイルがないか確認。 上の記事に良いこと書いてあるので、ここにも転機させて頂く。

変換をかける前に、考慮する部分があり。通常ディスク上でファイルを削除した場合にはインデックス上で削除したという情報を管理して、実体は消えていない状態ですが、通常の場合だとそこに上書きすることになるので、容量があるという見え方になりますが、仮想ディスクはそのインデックス上では削除されているけれど、実体がある場合も、ディスク容量として消費していることになります。この削除されているけれど実体がある部分を綺麗にしてから変換をすることで、効率よくファイルを小さくする事が出来ます。

で、Docker for Mac の Preferances を変更する。これを機に自動起動 (Start Docker when you log in) を無効にしておいた。使わないのに無駄にデーモン立ち上がってるの嫌いだから使うときだけ起動するようにする。

で、Docker for Mac 再起動。

圧縮しようとしたら…

ここでddコマンドによる0埋めや、qemu-imgコマンドでリサイズかける前に、今一度 ディスクイメージのファイルサイズを測ってみたところ

$ ll -h Docker.qcow2
-rw-r--r--  1 uorat  Users   113M  1 31 15:30 Docker.qcow2

あら、減っとる。。

Docker for Mac v1.13 で qcow2 ファイルの圧縮に対応していた

2017年1月31日時点のstable版 v1.13 で対応したらしい。 上記の Github Issues を読み進めていくと、 compaction に対応する旨が言及されており、無事 v1.13 に組み込まれリリースされていた。

経緯は以下あたりから読み進めてみてほしい。

Docker.qcow2 never shrinks - disk space usage leak in docker for mac · Issue #371 · docker/for-mac

ドキュメントを探してみると、確かに記載あった。ググラビリティが低かったらしく、最初にこれを見つけられなかったのが無念。

How do I reduce the size of Docker.qcow2?: Frequently asked questions (FAQ) - Docker

以下引用する。

In Docker 1.13 there is preliminary support for “TRIM” to non-destructively free space on the host. First free space within the Docker.qcow2 by removing unneeded containers and images with the following commands:

  • docker ps -a: list all containers
  • docker image ls: list all images
  • docker system prune: (new in 1.13): deletes all stopped containers, all volumes not used by at least one container and all images without at least one referring container.

Note the Docker.qcow2 will not shrink in size immediately. In 1.13 a background cron job runs fstrim every 15 minutes. If the space needs to be reclaimed sooner, run this command: docker run –rm -it –privileged –pid=host walkerlee/nsenter -t 1 -m -u -i -n fstrim /var

厳密には即時圧縮というわけではなく、15分毎に走るcron jobで実現しているらしい。 ただ、以前の手法と比べると大きな改善だし、これで十分だと思う。

結論

Docker for Mac v1.13 に上げると幸せになれます。