ECSにおけるログの扱い方


## 概要

ECS運用において、Dockerコンテナはデプロイの度に破棄・生成が繰り返されるため、アプリケーションが生成する各種ログはコンテナ内に保管しておくことができない。
すべてのログはイベントストリームとして扱い、外部ストレージに送る必要がある。
本ページではECSを運用していく中で発生したログの取り扱いに関する遍歴をまとめていく。

※このページは、[ECS運用のノウハウ](https://qiita.com/naomichi-y/items/d933867127f27524686a)からログの取り扱いに関する項目を抜き出して再編集したものです。

## Phase 1: CloudWatch Logsの導入

コンテナ運用を考慮したアプリケーション設計では、[The Twelve Factor App](https://12factor.net/)が参考となる。
この中の[Logs](https://12factor.net/logs)セクションではログの取り扱いで以下の記述がある。

> A twelve-factor* app never concerns itself with routing or storage of its output stream.
**It should not attempt to write to or manage logfiles. Instead, each running process writes its event stream, unbuffered, to stdout.**
During local development, the developer will view this stream in the foreground of their terminal to observe the app’s behavior.

全てのログはファイルへ書き込むのではなく、標準出力(STDOUT)に書き出す必要がある。

ECS運用当初はノウハウもなく右も左も分からない状況だったため、ログの永続化についてはあまり深く考えずAWSが標準で提供するCloudWatch Logs(`awslogs`ドライバ)を利用することにした。
CloudWatchにログを転送してしまえば、後はElasticsearchと連携し、ログの可視化まで比較的容易だったからである。

![Network (3).png](https://qiita-image-store.s3.amazonaws.com/0/27454/aa217dd6-4956-9fa7-6c12-b39be9d553b5.png)

しかし、実際に運用を始めてみるといくつかの問題が起きた。

* アプリケーションが生成する例外(スタックトレース)が行単位でストリームに流れるため、ログを追いづらい
    * Elasticsearchで[Multiline codec plugin](https://www.elastic.co/guide/en/logstash/current/plugins-codecs-multiline.html)プラグインの利用を検討したが、Elasticsearch Service自体がプラグインのインストールをサポートしていなかったので断念
    * EC2上にElasticsearchを構築することも考えたが、バックエンドサービスへの依存が高く、将来的にログドライバを変更する際の障壁と考え、導入を見送った
* 特定のキーワード・頻度に一致したエラーを検知することが難しい
    * CloudWatchのアラームだけでは条件一致の判定が出来ない

## Phase 2: ログクラスタの構築 (2017年2月〜)

前述の運用は数ヶ月で廃止し、代案として導入したのがFluentd経由でログをElasticsearchに流す方法である。
アプリケーションが将来的にスケールすることを考慮し、ログ用のクラスタを別途構築している。
この変更でFluentdがログ集約基盤となり、不正なログの検知や将来的なバックエンドサービスの切り替えが容易な構成となった。

![Network (4).png](https://qiita-image-store.s3.amazonaws.com/0/27454/13d2e9ee-3b3b-230f-6051-dd5e65d7e2e2.png)

### Fluentdの役割

アプリケーション例外は[Sentry](https://sentry.io/)で追跡しており、Sentryが検知できないエラー(ミドルウェアが標準出力や標準エラーに送信するログ)やLoggerが生成するメッセージをFluentdに送信。Fluentdはログをリアルタイムに監視し、一定期間内に閾値を超えた回数発生したメッセージをSlackに通知。すべてのログはS3とElasticsearchに並列で書き込みを行っている。

### ログの欠損問題

しばらく運用させていると、一部のログがElasticsearch・S3に書き込まれていないことに気付いた。

ログクラスタにはロードバランサとしてCLBを利用していたが、どうもCLBがアイドルタイムアウト直後の数リクエストでログが消失している。
試しにCLBを外してアプリケーションからログクラスタにアクセスすると問題は起きなかった。

![Network (1).png](https://qiita-image-store.s3.amazonaws.com/0/27454/21a73b15-9617-9ca6-c3eb-85c58e52f73e.png)

ログクラスタのECSコンテナインスタンスを調査したところ、`/var/log/docker`に次のようなログが残されていた。

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

同様の問題を[Issue](https://github.com/fluent/fluentd/issues/660#issuecomment-308334758])で見つけたが、ECSログドライバはKeepAliveの仕組みが無いため、アイドルタイムアウトの期間中にログの送信が無いとELBがコネクションを切断する仕様らしい。AWSサポートからも同様の回答を得られた。

## Phase 3: Weighted Routing (2017年5月〜)

CLBの代替策として、ログクラスタにはロードバランサを使わず、Route53のWeighted Routingでリクエストを分散することにした。

![Network (2).png](https://qiita-image-store.s3.amazonaws.com/0/27454/bbc6e6bf-06b9-4803-f256-6ee128d5f321.png)

仕組みとしては、ECS(EC2)の全プライベートIPをRoute 53のドメインに紐づけておくイメージである。
ただし、この方式ではログクラスタのスケールイン・アウトに合わせてRoute 53のレコードを更新する必要がある。
ここではオートスケールの更新をSNS経由でLambdaに検知させ、適宜レコードを更新する仕組みを取った。

## Phase 4: Service Discoveryの道入 (2018年10月〜)

2018年9月には[Service Discovery](https://aws.amazon.com/jp/blogs/news/amazon-ecs-service-discovery/)が東京リージョンにローンチされた。
ECSコンテナ間の通信をプライベートDNS(Route 53)で解決できる仕組みのため、インターナルなECSクラスタにELBを利用している場合はコスト削減にも繋がる。

![ECS-Service-Discovery-1024x865.png](https://qiita-image-store.s3.amazonaws.com/0/27454/b4942bc9-cec8-f9ac-783f-bf926b8a5c4e.png)

Service Discoveryを利用する場合、コンテナ自体の[ヘルスチェック](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_healthcheck)をタスク定義に追加する必要がある。
Fluentdが起動しているかの確認は、次のようなコードとなる。

```
CMD-SHELL,nc -zv localhost 24224 > /dev/null 2>&1 || exit 1
```

更にコンテナごとにENIが割り振られるため、タスクのネットワークモードを`awsvpc`に変更する。
例えば稼働タスク数を2にすると、Route 53上でも2つのAレコードが生成されることを確認できた。稼働数に合わせてAレコードが自動で増減する。
<img width="417" alt="Screen_Shot_2018-11-11_at_6_24_04.png" src="https://qiita-image-store.s3.amazonaws.com/0/27454/e8e14b38-0c9d-5e80-2fed-1b74393863f5.png">

ECS(Type: EC2)のPrivate IPsは3IP表示される(一番左はECSコンテナ自体のIP)。

<img width="462" alt="Screen_Shot_2018-11-11_at_6_27_41.png" src="https://qiita-image-store.s3.amazonaws.com/0/27454/7d789695-c92f-1445-0645-eb842df9afaa.png">

尚、[インスタンスタイプごとにENIの上限が決まっており](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/using-eni.html#AvailableIpPerENI)、例えばt2.mediumであれば3つまで(コンテナに割り当てられるのは2つ)となる。
t2.medium 1台の構成で3つ以上のタスクを指定すると、以下のようなエラーが発生することを確認できた。

```
service log was unable to place a task because no container instance met all of its requirements.
The closest matching container-instance XXX encountered error "RESOURCE:ENI".
For more information, see the Troubleshooting section.
```

※1: Service Discoveryの導入方法は[クラスメソッド](https://dev.classmethod.jp/cloud/aws/ecs-service-discovery/)の記事が分かりやすい
※2: インフラの構築にはTerraformを使っていたので、プライベートゾーンは[aws_route53_zone](https://www.terraform.io/docs/providers/aws/r/route53_zone.html)で作れば良いと思いきや、ECSコンソール上でDNSが見つからないと言われた。どうやら[aws_service_discovery_private_dns_namespace](https://www.terraform.io/docs/providers/aws/r/service_discovery_private_dns_namespace.html)で作成する必要があった
※3: 試しで作ったゾーンを削除しようとしたところ、サービスディスカバリに紐付いてるから削除できないとエラーが出た (クラスタからはサービスを削除済み)。ゾーンの削除は[delete-namespace API](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/delete-service.html)を使う必要があるみたい