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

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

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

# そもそもなぜコンテナ内のファイルシステムに保持してはいけないか
[Dockerfile 記述のベスト・プラクティス](https://docs.docker.jp/engine/userguide/eng-image/dockerfile_best-practices.html?highlight=%E3%83%99%E3%82%B9%E3%83%88%E3%83%97%E3%83%A9%E3%82%AF%E3%83%86%E3%82%A3%E3%82%B9)によると
> Dockerfile が定義するイメージによって生成されるコンテナは、できる限り "はかないもの"（ephemeral）と考えておくべきです。 "はかない" という語を使うのは、コンテナが停止、破棄されて、すぐに新たなものが作り出されるからです。 最小限の構成や設定があれば稼動できます。

とあり、更にGCPの[コンテナ運用のベストプラクティス](https://cloud.google.com/solutions/best-practices-for-operating-containers?hl=ja#ensure_that_your_containers_are_stateless_and_immutable)によると、
> 初めてコンテナを試している場合、従来のサーバーのようにコンテナを扱わないでください。たとえば、実行中のコンテナ内でアプリケーションを更新したり、脆弱性が発生したときに実行中のコンテナにパッチを適用したりしたくなるかもしれません。

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

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

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

# 構成図
![ecs-logging.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353528/d2c4c24a-9603-ec80-4abc-bf4678f38a62.png)


# コンポーネント
- ECS
- サービス検出(Service Discovery)
- nginx, app, batch
- [Fluentd公式イメージ](https://hub.docker.com/r/fluent/fluentd/)
- RDBやその他データストア

# App Serviceの構築
メインのアプリケーションコンテナは複数コンテナ存在し、Fluentdのログコンテナは単一コンテナとしてログを集約させます。
App Serviceは通常通りnginx containerからunix socketsを介してapp containerに接続します。これに関しては[こちらの記事](https://qiita.com/na-o-ys/items/1a863419e1f6c3063ace)を参照して構築していきます。
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としてタスクの重複を許さないようにします。
![スクリーンショット 2020-07-10 4.20.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353528/23174a0a-d01b-79df-e877-020c154fd11a.png)
それと、今回はタスクのnetworkmodeは`awsvpc`に設定してください。

続いてサービス検出の設定ですが、名前空間名は好みで変えても良いですが、慣習として`xxx.local` `xxx.internal`という名前空間が使われやすいです。
サービスの検出名も好みで良いですが、分かりやすくfluentdのコンテナ名にします。
![スクリーンショット 2020-07-10 4.37.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353528/6643cdaf-26b0-711d-37ba-2762d089bd5c.png)

private DNSレコードは今回はAレコードを作成してください。
![スクリーンショット 2020-07-10 4.37.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353528/2a46f087-8cd3-f28f-cdab-8a8ae1c4a6c7.png)
注意点としてタスクのnetworkmodeが`awsvpc`じゃない場合Aレコードが作成できないと思います。
代替としてSRVレコードが用意されていますがこちらは動的ポートマッピングを使用する為、既存のログプラグインで対応できない可能性があります。
`awsvpc`はENIをアタッチされてるのでAレコードからportを指定して接続ができるようになります。
ENIをアタッチする関係上インスタンスを制限されるデメリットもありますが、ネットワークパフォーマンスの観点からもECS全体で`awsvpc`が推奨されます。

以上でサービスを構築するとService Discoveryによるprivate DNSでのコンテナアクセスが可能になります。
![スクリーンショット 2020-07-10 5.05.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353528/923f8a7d-4c0b-d277-87f0-b6122b16d55a.png)

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