1
/
5

ECS運用のノウハウ(20180618更新)

【ECSデプロイツールを公開しました】

https://qiita.com/naomichi-y/items/fee7720fdadd443a0aa0


【概要】

ECSで本番運用を始めて早半年。ノウハウが溜まってきたので公開していきます。


【設計】

■基本方針

基盤を設計する上で次のキーワードを意識した。

Immutable infrastructure

  • 一度構築したサーバは設定の変更を行わない
  • デプロイの度に新しいインフラを構築し、既存のインフラは都度破棄する

Infrastructure as Code (IaC)

  • インフラの構成をコードで管理
  • オーケストレーションツールの利用

Serverless architecture

  • 非常駐型プロセスはイベントごとにコンテナを作成
  • 冗長化の設計が不要

アプリケーションレイヤに関して言えば、Twelve Factor Appも参考になる。コンテナ技術とも親和性が高い。


■ECSとBeanstalk Multi-container Dockerの違い

以前に記事を書いたので、詳しくは下記参照。

Beanstalk Multi-container Dockerは、ECSを抽象化してRDSやログ管理の機能を合わせて提供してくれる。ボタンを何度か押すだけでRubyやNode.jsのアプリケーションが起動してしまう。
一見楽に見えるが、ブラックボックスな部分もありトラブルシュートでハマりやすいので、素直にECSを使った方が良いと思う。


■ALBを使う

ECSでロードバランサを利用する場合、CLB(Classic Load Balancer)かALB(Application Load Balancer)を選択できるが、特別な理由がない限りALBを利用するべきである。
ALBはURLベースのルーティングやHTTP/2のサポート、パフォーマンスの向上など様々なメリットが挙げられるが、ECSを使う上での最大のメリットは動的ポートマッピングがサポートされたことである。
動的ポートマッピングを使うことで、1ホストに対し複数のタスク(例えば複数のNginx)を稼働させることが可能となり、ECSクラスタのリソースを有効活用することが可能となる。

※1: ALBの監視方式はHTTP/HTTPSのため、TCPポートが必要となるミドルウェアは現状ALBを利用できない。
※2: 2017年9月、TCPによる高スループットを実現したNetwork Load Balancerが追加された。詳しくはクラスメソッドの記事辺りを参考に。HTTP/HTTPSはALB、TCPはNLBを利用することで、今後CLBを利用する機会は無くなってくるものと考えられる。


■アプリケーションの設定は環境変数で管理

Twelve Factor Appでも述べられてるが、アプリケーションの設定は環境変数で管理している。
ECSのタスク定義パラメータとして環境変数を定義し、パスワードやシークレットキーなど、秘匿化が必要な値に関してはKMSで暗号化。CIによってECSにデプロイが走るタイミングで復号化を行っている。


■ログドライバ

ECSにおいてコンテナはデプロイの度に破棄・生成されるため、アプリケーションを始めとする各種ログはコンテナの内部に置くことはできない。ログはイベントストリームとして扱い、コンテナとは別のストレージで保管する必要がある。

今回はログの永続化・可視化を考慮した上で、AWSが提供するElasticsearch Service(Kibana)を採用することにした。
ECSは標準でCloudWatch Logsをサポートしているため、当初は素直にawslogsドライバを利用していた。CloudWatchに転送してしまえば、Elasticsearch Serviceへのストリーミングも容易だったからである。

しかし、Railsで開発したアプリケーションは例外をスタックトレースで出力し、改行単位でストリームに流されるためログの閲覧やエラー検知が非常に不便なものだった。

Multiline codec plugin等のプラグインを使えば複数行で構成されるメッセージを1行に集約できるが、AWS(Elasticsearch Service)ではプラグインのインストールがサポートされていない。

EC2にElasticsearchを構築することも一瞬考えたが、Elasticsearchへの依存度が高く、将来的にログドライバを変更する際の弊害になると考えて止めた。

その後考案したのがFluentd経由でログをElasticsearch Serviceに流す方法。この手法であればFluentdでメッセージの集約や通知もできるし、将来的にログドライバを変更することも比較的容易となる。


■ジョブスケジューリング

アプリケーションをコンテナで運用する際、スケジュールで定期実行したい処理はどのように実現するべきか。
いくつか方法はあるが、1つの手段としてLambdaのスケジュールイベントからタスクを叩く方法がある(Run task)。この方法でも問題はないが、最近(2017年6月)になってECSにScheduled Taskという機能が追加されており、Lambdaに置き換えて利用可能となった。Cron形式もサポートしているので非常に使いやすい。


【運用】

■ECSで設定可能なパラメータ

ECSコンテナインスタンスにはコンテナエージェントが常駐しており、パラメータを変更することでECSの動作を調整できる。設定ファイルの場所は /etc/ecs/ecs.config。
変更する可能性が高いパラメータは下表の通り。他にも様々なパラメータが存在する。

パラメータ変更後はエージェントの再起動が必要。

$ sudo stop ecs
$ sudo start ecs

クラスタのスケールアウトを考慮し、ecs.configはUserDataに定義しておくと良い。

以下はfluentdを有効にしたUserDataの記述例。

#!/bin/bash
echo ECS_CLUSTER=sandbox >> /etc/ecs/ecs.config
echo ECS_AVAILABLE_LOGGING_DRIVERS=["fluentd\"] >> /etc/ecs/ecs.config


■CPUリソースの制限

現状ECSにおいてCPUリソースの制限を設定することはできない(docker runの--cpu-quotaオプションがサポートされていない)。
タスク定義パラメータcpuは、docker runの--cpu-sharesにマッピングされるもので、CPUの優先度を決定するオプションである。従って、あるコンテナがCPUを食いつぶしてしまうと、他のコンテナにも影響が出てしまう。
尚、Docker 1.13からは直感的にCPUリソースを制限ができる--cpusオプションが追加されている。是非ECSにも取り入れて欲しい。


■ユーティリティ

実際に利用しているツールを紹介。


■ルートボリューム・Dockerボリュームのディスク拡張

ECSコンテナインスタンスは自動で2つのボリュームを作成する。1つはOS領域(/dev/xvda8GB)、もう1つがDocker領域(/dev/xvdcz 22GB)である。
クラスタ作成時にDocker領域のサイズを変更することはできるが、OS領域は項目が見当たらず変更が出来ないように見える。

どこから設定するかというと、一度空のクラスタを作成し、EC2マネージメントコンソールからインスタンスを作成する必要がある。

また、既存ECSコンテナインスタンスのOS領域を拡張したい場合は、EC2マネージメントコンソールのEBS項目から変更可能。スケールアウトを考慮し、Auto scallingのLaunch Configurationも忘れずに更新しておく必要がある。

補足となるが、Docker領域はOS上にマウントされていないため、ECSコンテナインスタンス上からdf等のコマンドで領域を確認することはできない。


■デプロイ

ECSのデプロイツールは色々ある。

※下2つは私が開発したツールです。genova は ecs_deployer をベースとしたデプロイ管理マネージャとなってます。


■デプロイ方式

  • コマンド実行形式のデプロイ
  • GitHubのPushを検知した自動デプロイ
  • Slackを利用したインタラクティブデプロイ


■デプロイフロー

ECSへのデプロイフローは次の通り。

1. リポジトリ・タスクの取得
2. イメージのビルド
* タグにGitHubのコミットID、デプロイ日時を追加
3. ECRへのプッシュ
4. タスクの更新
5. 不要なイメージの削除
* ECRは1リポジトリ辺り最大1,000のイメージを保管できる
6. サービスの更新
7. タスクの入れ替えを監視
* コンテナの異常終了も検知
8. Slackにデプロイ完了通知を送信


■デプロイパフォーマンスの改善

ALBを利用している場合、デプロイ時のコンテナの入れ替えに時間がかかることがある。
これはELBが登録解除プロセスを実行する前に300秒待機して、リクエストの完了を待つために起きている。アプリケーションの特性によってはこの時間を短くすることで、デプロイのパフォーマンスを改善することができる。

この設定の変更により、6~7分かかっていたデプロイが2分程度に改善された。


■ログの分類

ECSのログを分類してみた。


■サーバレス化

ECSから少し話が逸れるが、インフラの運用・保守コストを下げるため、Lambda(Node.js)による監視の自動化を進めている。各種バックアップからシステムの異常検知・通知までをすべてコード化することで、サービスのスケールアウトに耐えうる構成が容易に構築できるようになる。
ECS+Lambdaを使ったコンテナ運用に切り替えてから、EC2の構築が必要となるのは踏み台くらいだった。


■クラスタから特定のインスタンスを外す

クラスタから特定のインスタンスを外したい場合は、インスタンスのドレイニングが必要となる。ドレイニング状態となったインスタンスには新しいタスクが配置されなくなり、別のインスタンスにタスクが配置される。
ドレイニングを行わずインスタンスを削除した場合、インスタンス上のコンテナが削除され、アプリケーションへの接続が遮断される点に注意したい。

インスタンスのドレイニングはクラスタページのECS Instancesタブから設定できる。

ドレイニングが終わると、対象インスタンス上のタスク数は 0 となる。

ドレイニングの自動化についてはAWSの記事が参考となる。


■インスタンスを減らす

クラスタ上のインスタンスを増やすのは簡単で、コンソールから実行する場合は Scale ECS Instances を実行すれば良い。

同様にインスタンスを減らす場合は Desired number of instances の数を減らせば良いのだが、そのまま実行してしまうと稼働中のコンテナが落ちてしまうことがある。
今まで何となく「ドレイニングしたインスタンスが外される」と思ってたが、決してそんなことはなく、どのインスタンスが落ちるかは制御できないものらしい。

で、どうすれば良いかというと、例えば次のような対応が必要となる。

  1. 削除対象インスタンスをドレイニング
  2. 削除されたくないインスタンスに対し、Set Scale In Protection (削除保護) を有効化
  3. インスタンス数を減らす

2 の削除保護は、EC2の削除保護ではなく、オートスケールグループ側の設定が必要であることに注意する。


■リザーブドインスタンスの購入

ECSはEC2でクラスタリングを構成するため、年間利用(1年または3年)が確定しているのであればリザーブドインスタンスを購入することで2~5割程度の割引を受けることができる。


【トラブルシュート】

■ログドライバにfluentdを使うとログの欠損が起きる

ログドライバの項に書いた通り、アプリケーションログはFluentd経由でElasticsearchに流していたが、一部のログが転送されないことに気付いた。
構成的にはアプリケーションクラスタからログクラスタ(CLB)を経由してログを流していたが、どうもCLBのアイドルタイムアウト経過後の最初のログ数件でロストが生じている。試しにCLBを外してみるとロストは起きない。

ログクラスタ(ECSコンテナインスタンスの/var/log/docker)には次のようなログが残っていた。

time="2017-08-24T11:23:55.152541218Z" level=error msg="Failed to log msg \"...\" for logger fluentd: write tcp *.*.*.*:36756->*.*.*.*:24224: write: broken pipe"
3) time="2017-08-24T11:23:57.172518425Z" level=error msg="Failed to log msg \"...\" for logger fluentd: fluent#send: can't send logs, client is reconnecting"

同様の問題をIssueで見つけたが、どうも現状のECSログドライバはKeepAliveの仕組みが無いため、アイドルタイムアウトの期間中にログの送信が無いとELBが切断してしまうらしい(AWSサポートにも問い合わせた)。

という訳でログクラスタにはCLBを使わず、Route53のWeighted Routingでリクエストを分散することにした。

尚、この方式ではログクラスタのスケールイン・アウトに合わせてRoute 53のレコードを更新する必要がある。
ここではオートスケールの更新をSNS経由でLambdaに検知させ、適宜レコードを更新する仕組みを取った。


■コンテナの起動が失敗し続け、ディスクフルが発生する

ECSはタスクの起動が失敗すると数十秒間隔でリトライを実施する。この時コンテナがDockerボリュームを使用していると、ECSコンテナエージェントによるクリーンアップが間に合わず、ディスクフルが発生することがあった(ECSコンテナインスタンスの/var/lib/docker/volumesにボリュームが残り続けてしまう)。
この問題を回避するには、ECSコンテナインスタンスのOS領域(※1)を拡張するか、コンテナクリーンアップの間隔を調整する必要がある。
コンテナを削除する間隔はECS_ENGINE_TASK_CLEANUP_WAIT_DURATIONパラメータを使うと良い。

※1: DockerボリュームはDocker領域ではなく、OS領域に保存される。OS領域の容量はデフォルトで8GBなので注意が必要。

また、どういう訳か稀に古いボリュームが削除されず残り続けてしまうことがあった。そんな時は次のコマンドでボリュームを削除しておく。

# コンテナから参照されていないボリュームの確認
docker volume ls -f dangling=true

# 未参照ボリュームの削除
docker volume rm $(docker volume ls -q -f dangling=true)


■ECSがELBに紐付くタイミング

DockerfileのCMDでスクリプトを実行するケースは多々あると思うが、コンテナはCMDが実行された直後にELBに紐付いてしまうので注意が必要となる。

bundle exec rake assets:precompile

時間のかかる処理は素直にDockerfile内で実行した方が良い。


■ecs-agent.logに"WARN messages when no Tasks are scheduled"という警告が出力される

ECSコンテナインスタンスの/var/log/ecs-agent.logにWARN messages when no Tasks are scheduledという警告が頻出していた。Issueにも症例が報告されている。
この問題は既知の不具合のようで、ECSのAMIにamzn-ami-2017.03.d-amazon-ecs-optimized - ami-e4657283を使うことで解決した(AMI amzn-ami-2016.09.f-amazon-ecs-optimized - ami-c393d6a4で問題が起こることを確認済み)。


■コンテナ起動時にディスク容量不足のエラーが出てコンテナごと落ちる

CannotPullContainerError: failed to register layer: devmapper: Thin Pool has 2862 free data blocks which is less than minimum required 4449 free data blocks. Create more free space in thin pool or use dm.min_free_space option to change behavior

docker info でDockerの情報を確認する。

$ docker info
Containers: 5
Running: 5
Paused: 0
Stopped: 0
Images: 6
Server Version: 17.12.0-ce
Storage Driver: devicemapper
Pool Name: docker-docker--pool
Pool Blocksize: 524.3kB
Base Device Size: 10.74GB
Backing Filesystem: ext4
Udev Sync Supported: true
Data Space Used: 21.84GB
Data Space Total: 23.33GB
Data Space Available: 1.491GB
...

ECSはデフォルトで22GBのDockerイメージ格納領域を作るが、Data Space Available の値を見ると 1.49GB しかない。

次のコマンドを実行して容量を増やす。

# 実行中でないコンテナを削除
$ docker rm $(docker ps -aq)

# 未使用のイメージを削除
$ docker rmi $(docker images -q)

# 確認
$ docker info
Containers: 5
Running: 5
Paused: 0
Stopped: 0
Images: 3
Server Version: 17.12.0-ce
Storage Driver: devicemapper
Pool Name: docker-docker--pool
Pool Blocksize: 524.3kB
Base Device Size: 10.74GB
Backing Filesystem: ext4
Udev Sync Supported: true
Data Space Used: 20.54GB
Data Space Total: 23.33GB
Data Space Available: 2.79GB

1.49GB→2.79GB と少し増えた。

続けてコンテナ内で使用されていないデータブロックを削除してみる。

$ sudo sh -c "docker ps -q | xargs docker inspect --format='{{ .State.Pid }}' | xargs -IZ fstrim /proc/Z/root/"

$ docker info
Containers: 5
Running: 5
Paused: 0
Stopped: 0
Images: 3
Server Version: 17.12.0-ce
Storage Driver: devicemapper
Pool Name: docker-docker--pool
Pool Blocksize: 524.3kB
Base Device Size: 10.74GB
Backing Filesystem: ext4
Udev Sync Supported: true
Data Space Used: 10.43GB
Data Space Total: 23.33GB
Data Space Available: 12.9GB

2.79GB→12.9GB まで増えた。

根本解決としては、Dockerに割り当てるストレージ容量を増やすか、古いイメージの削除間隔を短くすれば良さそう。

ストレージ設定

株式会社pringでは一緒に働く仲間を募集しています
3 いいね!
3 いいね!
同じタグの記事
今週のランキング