stks56's tech blog

Keep it simple, stupid.

ログファイルからをバッチ処理を行う非コンテナアプリケーションをECSでサイドカーパターンとService Discoveryを使ってコンテナ内ロギングをする

概要

Dockerコンテナでアプリケーション構築するメリットは皆さんご周知ではあると思いますが、EC2でアプリケーションを公開していてECSなどのコンテナ基盤に乗り換えた時に、EBSやEFS等のlinuxファイルシステムに依存している部分をコンテナ上でどうするかという問題に直面します。

今回はその問題のうち、アクセスログファイル等を集計してDBに入れたいというありがちな問題をService DiscoveryとFluentdコンテナのサイドカーパターンで解決する手法を実践します。

そもそもなぜコンテナ内のファイルシステムに保持してはいけないか

Dockerfile 記述のベスト・プラクティスによると

Dockerfile が定義するイメージによって生成されるコンテナは、できる限り "はかないもの"(ephemeral)と考えておくべきです。 "はかない" という語を使うのは、コンテナが停止、破棄されて、すぐに新たなものが作り出されるからです。 最小限の構成や設定があれば稼動できます。

とあり、更にGCPコンテナ運用のベストプラクティスによると、

初めてコンテナを試している場合、従来のサーバーのようにコンテナを扱わないでください。たとえば、実行中のコンテナ内でアプリケーションを更新したり、脆弱性が発生したときに実行中のコンテナにパッチを適用したりしたくなるかもしれません。

コンテナは基本的に、このような処理に対応するように設計されていません。コンテナは、ステートレスかつ不変になるように設計されています。

コンテナは基本的に、このような処理に対応するように設計されていません。コンテナは、ステートレスかつ不変になるように設計されています。 とあり、コンテナ内では状態を持たない = ステートレス であることがかなり推奨されています。 コンテナ内で作られたファイルをコンテナ内で持ってしまうと、コンテナが破棄され次世代のコンテナが立ち上がった際に生成したファイルが無いので実質欠損するという事態が起こりえます。 これを回避するためにデータストアに格納することが推奨されていますが、今回はDockerの永続化Volumeを使ってデータを退避させます。

コンテナ基盤を使って運用するときの普通のロギング

ECS、kubernetesなどのコンテナ基盤を使う場合、Dockerの標準ログドライバーを使うことが推奨されています。 主に、docker logskubectl logsで見れる標準出力のログをどこかに集約させるという手法です。 大体各種クラウドにはコンテナ基盤に付随してロギングを集約させるマネージドサービスが搭載されていると思うのでそれを使ってデータストアに入れるパターが王道だと思います。 ただ、これらはアプリケーション側でログ出力の整備を行わなければ成立しません。今回はその整備にコストがかかる場合、既存のロギングシステムを残しつつ、コンテナ環境でどう再現するかの手法です。

構成図

f:id:stks56:20201207210426p:plain

コンポーネント

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としてタスクの重複を許さないようにします。

f:id:stks56:20201207210635p:plain

それと、今回はタスクのnetworkmodeはawsvpcに設定します。

続いてサービス検出の設定ですが、名前空間名は好みで変えても良いですが、慣習としてxxx.local xxx.internalという名前空間が使われやすいです。 サービスの検出名も好みで良いですが、分かりやすくfluentdのコンテナ名にします。

f:id:stks56:20201207211159p:plain

private DNSレコードは今回はAレコードを作成するようにします。

f:id:stks56:20201207211308p:plain

注意点としてタスクのnetworkmodeがawsvpcじゃない場合Aレコードが作成できないと思います。 代替としてSRVレコードが用意されていますがこちらは動的ポートマッピングを使用する為、既存のログプラグインで対応できない可能性があります。 awsvpcはENIをアタッチされてるのでAレコードからportを指定して接続ができるようになります。 ENIをアタッチする関係上インスタンスを制限されるデメリットもありますが、ネットワークパフォーマンスの観点からもECS全体でawsvpcが推奨されます。

以上でサービスを構築するとService Discoveryによるprivate DNSでのコンテナアクセスが可能になります。

f:id:stks56:20201207211003p:plain

タスク上で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処理が正常に走れば完成です。