ログファイルからをバッチ処理を行う非コンテナアプリケーションをECSでサイドカーパターンとService Discoveryを使ってコンテナ内ロギングをする
概要
Dockerコンテナでアプリケーション構築するメリットは皆さんご周知ではあると思いますが、EC2でアプリケーションを公開していてECSなどのコンテナ基盤に乗り換えた時に、EBSやEFS等のlinuxファイルシステムに依存している部分をコンテナ上でどうするかという問題に直面します。
今回はその問題のうち、アクセスログファイル等を集計してDBに入れたいというありがちな問題をService DiscoveryとFluentdコンテナのサイドカーパターンで解決する手法を実践します。
そもそもなぜコンテナ内のファイルシステムに保持してはいけないか
Dockerfile が定義するイメージによって生成されるコンテナは、できる限り "はかないもの"(ephemeral)と考えておくべきです。 "はかない" という語を使うのは、コンテナが停止、破棄されて、すぐに新たなものが作り出されるからです。 最小限の構成や設定があれば稼動できます。
とあり、更にGCPのコンテナ運用のベストプラクティスによると、
初めてコンテナを試している場合、従来のサーバーのようにコンテナを扱わないでください。たとえば、実行中のコンテナ内でアプリケーションを更新したり、脆弱性が発生したときに実行中のコンテナにパッチを適用したりしたくなるかもしれません。
コンテナは基本的に、このような処理に対応するように設計されていません。コンテナは、ステートレスかつ不変になるように設計されています。
コンテナは基本的に、このような処理に対応するように設計されていません。コンテナは、ステートレスかつ不変になるように設計されています。 とあり、コンテナ内では状態を持たない = ステートレス であることがかなり推奨されています。 コンテナ内で作られたファイルをコンテナ内で持ってしまうと、コンテナが破棄され次世代のコンテナが立ち上がった際に生成したファイルが無いので実質欠損するという事態が起こりえます。 これを回避するためにデータストアに格納することが推奨されていますが、今回はDockerの永続化Volumeを使ってデータを退避させます。
コンテナ基盤を使って運用するときの普通のロギング
ECS、kubernetesなどのコンテナ基盤を使う場合、Dockerの標準ログドライバーを使うことが推奨されています。
主に、docker logs
やkubectl logs
で見れる標準出力のログをどこかに集約させるという手法です。
大体各種クラウドにはコンテナ基盤に付随してロギングを集約させるマネージドサービスが搭載されていると思うのでそれを使ってデータストアに入れるパターが王道だと思います。
ただ、これらはアプリケーション側でログ出力の整備を行わなければ成立しません。今回はその整備にコストがかかる場合、既存のロギングシステムを残しつつ、コンテナ環境でどう再現するかの手法です。
構成図
コンポーネント
- ECS
- サービス検出(Service Discovery)
- nginx, app, batch
- Fluentd公式イメージ
- RDBやその他データストア
App Serviceの構築
メインのアプリケーションコンテナは複数コンテナ存在し、Fluentdのログコンテナは単一コンテナとしてログを集約させます。 App Serviceは通常通りnginx containerからunix socketsを介してapp containerに接続します。これに関してはこちらの記事を参照して構築していきます。 https://qiita.com/na-o-ys/items/1a863419e1f6c3063ace App Serviceに関してはもう構築できている前提で話を進めていきます。
Log Serviceの構築
Log Serviceですがこちらは別サービスになっているのでscalingはせず、単一サービスとして構築します。 まず、Log Service用のタスクはport mappingは指定せず、Dockerfileでコンテナ側でのみportを開いてください。
"portMappings": [], "networkMode": "awsvpc",
単一サービスですのでタスクの数は1、最小ヘルス率は0、最大率は100としてタスクの重複を許さないようにします。
それと、今回はタスクのnetworkmodeはawsvpc
に設定します。
続いてサービス検出の設定ですが、名前空間名は好みで変えても良いですが、慣習としてxxx.local
xxx.internal
という名前空間が使われやすいです。
サービスの検出名も好みで良いですが、分かりやすくfluentdのコンテナ名にします。
private DNSレコードは今回はAレコードを作成するようにします。
注意点としてタスクのnetworkmodeがawsvpc
じゃない場合Aレコードが作成できないと思います。
代替としてSRVレコードが用意されていますがこちらは動的ポートマッピングを使用する為、既存のログプラグインで対応できない可能性があります。
awsvpc
はENIをアタッチされてるのでAレコードからportを指定して接続ができるようになります。
ENIをアタッチする関係上インスタンスを制限されるデメリットもありますが、ネットワークパフォーマンスの観点からもECS全体でawsvpc
が推奨されます。
以上でサービスを構築するとService Discoveryによるprivate DNSでのコンテナアクセスが可能になります。
タスク上でport mappingせずともdnsとportを指定して通信ができるようになったかと思います
root@21454022290b:/# dig fluentd.local ; <<>> DiG 9.11.4-P2-RedHat-9.11.4-9.P2.amzn2.0.4 <<>> fluentd.local ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 15 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;fluentd.local. IN A ;; ANSWER SECTION: fluentd.local. 10 IN A 172.31.0.54 ;; Query time: 1 msec ;; SERVER: 172.31.0.2#53(172.31.0.2) ;; WHEN: 木 7月 09 20:15:59 UTC 2020 ;; MSG SIZE rcvd: 60
ログプラグインでもportを指定して接続ができるようになっているかと思います。
host: fluentd.local port: 24224
これでログ集約サーバーの出来上がりです。
サイドカーパターンでログファイルを処理する
fluentdにより生成されたログファイルを用いてbatch containerで処理したい場合、サイドカーパターンでvolumeを共有して永続化する必要があります。
まずfluentdのDockerfileでvolumeのpathを指定してあげます。
VOLUME /fluentd/log/xxx
注意点はコンテナ内の実行ユーザーによってはvolumeディレクトリに書き込めない場合があるので、適宜書き込み権限を追加してください。
続いて、batchをサイドカーパターンでvolume共有したタスクを用意します。
[ { "name": "fluentd", "image": "xxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/xxx-fluentd:e6767fd9", "cpu": 400, "memoryReservation": 400, "essential": true }, { "name": "batch", "image": "xxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/xxx-batch:e6767fd9", "cpu": 400, "memoryReservation": 400, "essential": true, "volumesFrom": [ { "readOnly": false, "sourceContainer": "fluentd" } ], "dependsOn": [ { "containerName": "fluentd", "condition": "START" } ] } ]
ポイントはvolumesFrom
を指定してあげて書き込み処理をしたい場合はreadOnly: false
にすることです。
念のため、もしfluentd containerの起動が遅れた状態でにバッチ処理が走らないようにdependsOn
で起動を待つ指定もしておきます。
また、batchコンテナ内のユーザーにも書き込み権限を追加するようにしてください。
今回自分が実践したbatchはcronで回すタイプだったのでcron -f
で定期実行していますが、何かしらのトリガーで無事ログファイルのbatch処理が正常に走れば完成です。