AWS Elastic Beanstalk の Multicontainer Docker 環境（ECS）で log-driver=fluentd を使う

# 2020年10月追記
どうやら、デフォルトでも log-driver=fluentd が使えるようになったみたいです（動作未確認）

https://github.com/aws/amazon-ecs-init/pull/151

もし、動作確認などされた方がいらっしゃれば、コメントいただけると助かります。

# この記事は何か
この記事は AWS Elastic Beanstalk（以下 EB）の Multicontainer Docker 環境で、log-driver=fluentd を使う方法を紹介します。

EB の Multicontainer Docker 環境（ECS）では、デフォルトで log-driver=fluentd が利用できません。
現時点で安全に(※1) log-driver=fluentd を利用する方法は、カスタム AMI を作成してそれを利用する方法しかなさそうです。（他に方法がありましたらお知らせください）

この記事では、この問題と log-driver に fluentd を利用できるようにする方法を説明します。

(※1):
ネット上では ecs-agent を再起動する方法が紹介されていますが、後述する理由で安全な方法とは言えません。

# log-driver=fluentd 使えない問題について
カスタム AMI の作成方法について知りたい方はこの項目はスキップして「対策」から読んでください。

## 前提
EB の Multicontainer Docker 環境で log-driver に fluentd を使いたい場合があります。

例として nginx コンテナを EB にデプロイし、fluentd を使ってログを収集したいと仮定します。
このような場合、log-driver を利用しない最初の Dockerrun.aws.json は次のようになるでしょう。

```json:Dockerrun.aws.json
{
  "AWSEBDockerrunVersion": 2,
  "containerDefinitions": [
    {
      "name": "nginx",
      "image": "nginx",
      "essential": true,
      "memory": 128,
      "portMappings": [
        {
          "hostPort": 80,
          "containerPort": 80
        }
      ]
    }
  ]
}
```

この Dockerrun.aws.json は問題なく EB にデプロイできます。

## log-driver=fluentd の利用
ここで log-driver を使って fluentd にログが転送されるように指定します。

nginx イメージはデフォルトでは stdout にログが出力されるようになっています。(ref: [nginxinc/docker-nginx](https://github.com/nginxinc/docker-nginx/blob/master/mainline/alpine/Dockerfile#L136))
そこで、nginx コンテナの他に fluentd コンテナを起動するようにし、そのコンテナにログを転送するように log-driver を指定します。

```json:Dockerrun.aws.json
{
  "AWSEBDockerrunVersion": 2,
  "containerDefinitions": [
    {
      "name": "nginx",
      "image": "nginx",
      "essential": true,
      "memory": 128,
      "portMappings": [
        {
          "hostPort": 80,
          "containerPort": 80
        }
      ],
      "logConfiguration": {
        "logDriver": "fluentd",
        "options": {
          "fluentd-address": "localhost:24224",
          "tag": "nginx"
        }
      }
    },
    {
      "name": "fluentd",
      "image": "fluent/fluentd",
      "memory": 512,
      "portMappings": [
        {
          "hostPort": 24224,
          "containerPort": 24224
        }
      ]
    }
  ]
}
```


しかし、この Dockerruns.aws.json を EB にデプロイすると、次のようなエラーが発生してデプロイできません。

```text:デプロイエラー
Encountered error starting new ECS task: { "failures": [ { "reason": "ATTRIBUTE", "arn": "****" } ], "tasks": [] }
```

ECS がデフォルトで使える log-driver は次のものに限るからです。

- json-file
- syslog
- awslogs
- none

より具体的には ecs-agent の起動スクリプトである ecs-init 内のデフォルト指定に fluentd が含まれていないのです。(ref: [aws/amazon-ecs-init](https://github.com/aws/amazon-ecs-init/blob/master/ecs-init/docker/docker.go#L203))


## どうするか
デフォルトで利用できる log-driver は先ほどの値のみですが、設定ファイルを使ってこれらを変更することが可能です。

/etc/ecs/ecs.config というパスにファイルを作成し、ECS_AVAILABLE_LOGGING_DRIVERS=["hoge"] と指定すると、利用可能な log-driver の値を変更することができます。(ref: [Amazon ECS コンテナエージェントの設定](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-agent-config.html))

結果として fluentd を利用可能にするためには、次のような内容のファイルを /etc/ecs/ecs.config に作成すればよさそうです。

```text:/etc/ecs/ecs.config
ECS_AVAILABLE_LOGGING_DRIVERS=["json-file","syslog","awslogs","fluentd","none"]
```

## どうやって /etc/ecs/ecs.config を設定するか
問題は「この設定ファイルをどのタイミングで作成し、適用するか」です。

/etc/ecs/ecs.config は ecs-agent が起動するタイミングでしか読み込まれないので、案として次のようなものが考えられます。

1. /etc/ecs/ecs.config を作成し、ecs-agent を再起動する
2. /etc/ecs/ecs.config を作成し、インスタンスを再起動する
3. インスタンスを起動時から /etc/ecs/ecs.config が存在するようにする

### 1. /etc/ecs/ecs.config を作成し、ecs-agent を再起動させる
「/etc/ecs/ecs.config を作成し、ecs-agent を再起動させる」という案について考えます。

このアイディアの実現方法として

> .ebextensions の config を使って /etc/ecs/ecs.config の生成と ecs-agent の再起動を行う

という手法が Stackoverflow で紹介されています。

この手法を用いると当然、デプロイ中に ecs-agent が再起動するようになりますが、その結果、

> アプリケーションのソースコードが展開される /var/app/current は以前のバージョンであるにも関わらず、ecs-agent が再起動され、新しいバージョン ecs task が起動して新しいバージョンのコンテナが起動する

というバージョン不整合状態になるタイミングが存在するようになります。
これは、場合によっては非常に分かりにくいエラーを引き起こします。（筆者はこのエラーにはまりました）

EB において ecs-agent を再起動するのは、__非常に危険であるためおすすめできません__。

### 2. /etc/ecs/ecs.config を作成し、インスタンスを再起動する
インスタンスを生成したあと、ssh などで /etc/ecs/ecs.config を作成し、インスタンスを reboot する、という手法について考えます。

EB ではインスタンスを入れ替えることが多いため、インスタンスになるべく状態を持たせたくありません。

設定を変更しインスタンスが入れかわると、新しいインスタンスには /etc/ecs/ecs.config が存在しないためアプリケーションが正しくデプロイされません。
その都度、/etc/ecs/ecs.config を作成しなおすのは現実的ではないので、これはあまり良い方法ではありません。

### 3. インスタンスを起動時から /etc/ecs/ecs.config が存在するようにする
結果として残るのは「インスタンスを起動時から /etc/ecs/ecs.config が存在するようにする」という方法です。

具体的には、

> ECS_AVAILABLE_LOGGING_DRIVERS を設定した /etc/ecs/ecs.config を持っている Multicontainer Docker 用のカスタム AMI を作成し、EB でそれを利用する

という方法です。

ただし、この方法にも以下のデメリットがあります。

- カスタム AMI を管理しなければならい
- 新しい AMI が公開されたら、都度カスタム AMI を作らなければならない

しかし、現時点ではこの方法がリスクが少なくベストな方法だと思われます。

### 番外: ecs-init のデフォルト値に fluentd が含まれるようにする 
デフォルトで fluentd が使えればこのような問題は発生しません。
そのため、ecs-init のデフォルト値に fluentd が含まれているのが最高です。

ecs-init のリポジトリにそのような PR が存在しています。

[Add fluentd to default available logging drivers by amake · Pull Request #151 · aws/amazon-ecs-init](https://github.com/aws/amazon-ecs-init/pull/151)

しかし、残念ながら放置されたままの状態です。。。

# 対策
ここからは実際に /etc/ecs/ecs.config を持つカスタム AMI を作成して、EB で利用する方法について説明します。

## カスタム AMI の作成
EB において カスタム AMI を作成する方法は公式ドキュメントにも存在します。

[カスタム Amazon マシンイメージ（AMI）の作成](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/using-features.customenv.html)

これに従って、AMI を作成して EB に適用します。

### ベースの AMI を特定する
EB の Multicontainer Docker で利用されるベースとなる AMI を特定します。

まず、Multicontainer Docker 環境で何も指定せず環境を作成します。
その後、[設定] ⇒ [インスタンス] の AMI の値をメモします。

<img src="https://qiita-image-store.s3.amazonaws.com/0/41604/5243ef42-7211-c563-2f63-faae2cab7fb4.png" width=400>

これがベースの AMI となります。

### ベースの AMI を選択する
[EC2 Console] から EC2 の作成フローに入ります。

[インスタンスの作成] ⇒ [Community AMI] を選択し、先ほどの AMI の値を入力して出てきた AMI を選択します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/41604/4746dc71-2e5c-7c4f-55ce-a6be8b47f255.png" width=700>

### ユーザーデータを作成する
インスタンスの設定の[高度な設定]のユーザーデータのところを開きます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/41604/639fd78c-7cc5-2f64-ab50-de8952eaca99.png" width=500>

そこに次のデータを入力します。

```text:ユーザーデータ
#cloud-config
repo_releasever: 2017.09
repo_upgrade: none

runcmd:
  - echo 'ECS_AVAILABLE_LOGGING_DRIVERS=["json-file","syslog","awslogs","fluentd","none"]' >> /etc/ecs/ecs.config
```

前半部分は lock-on-launch 機能を設定しています。これは自動的にアップデートがかかる設定をオフにする設定です。

この時、2017.09 の部分は次の記事を参考にしてベースとなる AMI のリリース年月に合わせてください。
[Elastic Beanstalk でサポートされているプラットフォーム](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/concepts.platforms.html)

<img src="https://qiita-image-store.s3.amazonaws.com/0/41604/e3deebea-7bbb-8229-e741-9bf1fee6bf45.png" width=700>

後半部分は /etc/ecs/ecs.config に関する設定です。
/etc/ecs/ecs.config ファイルを作成するコマンドを実行しています。

__ここで ECS_AVAILABLE_LOGGING_DRIVERS の値に fluentd が含まれるようにします。__

設定が終わったらインスタンスを立ち上げます。

### 状態を確認する
インスタンスに ssh して、/etc/ecs/ecs.config が正しく設定されているかを確認します。

```
$ ssh ec2-user@******.compute.amazonaws.com

       __|  __|_  )
       _|  (     /   Amazon Linux AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-ami/2017.09-release-notes/
[ec2-user@ip-*-*-*-* ~]$ cat /etc/ecs/ecs.config 
ECS_AVAILABLE_LOGGING_DRIVERS=["json-file","syslog","awslogs","fluentd","none"]
```

ただしく設定されていることが確認できました。

### カスタム AMI を作成する
料金がかからないようにインスタンスをストップしてから、カスタム AMI を作成します。

カスタム AMI の作成方法は次のドキュメントを参考に行ってください。

[EC2 カスタムAMIを作成する - Qiita](https://qiita.com/gitya107/items/bf071b55819cd1e4bbfa)

カスタム AMI を作成したらその AMI の ID をメモしておきます。


## EB に AMI を設定する
EB にさきほど作成した AMI の ID を設定します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/41604/d23841b9-0bec-b3df-270f-8b9a684b63a7.png" width=700>

設定が完了するとカスタム AMI をベースにしてインスタンスが再生成されます。


## 確認
新しい AMI では log-driver=fluentd が利用できるはずです。
設定変更のデプロイが完了したあと、アプリケーションをデプロイしてみてください。

正しくアプリケーションがデプロイできたら作業は完了です。

<img src="https://qiita-image-store.s3.amazonaws.com/0/41604/6b4de56d-ca15-128b-c007-54bea56eb4fc.png" width=400>

これで EB の Multicontainer Docker 環境で log-driver=fluentd が利用できるようになりました。