AWSのCloudWatch Agentを任意のDockerイメージにねじ込んでログを転送する

おはようこんにちはこんばんは
えんじにあの@tumuginです
きんようびです

さて、今日はECSなどで運用しているDockerコンテナにAWSのCloudWatch Agentを入れて任意のログファイルをCloudWatchに転送できるようにしたのでその方法を紹介したいと思います。

通常は標準出力に出したものをECSの機能などを用いてCloudWatch側に送り込むことが多いと思いますが、コンテナ内部に複数のログファイルがあってそれぞれを別のロググループに送りたい、のようなときには困ってしまいます。

また、CloudWatchのAPIを用いて転送するのもひとつの手ですが、レートリミットのことを考慮しなければならないなど様々なハマりポイントがあるので、今回はなんとかしてCloudWatch Agentを入れてあげて送る方法にしました。

## 大前提
- 運用しているDockerイメージをビルドするDockerfileがある
- アプリケーションから吐き出されるログファイルがある

## 諸々の設定を用意する
まずは `amazon-cloudwatch-agent.json` から... ここにCloudWatch Agentに渡す設定を入れておきます。

```json:cloudwatch/amazon-cloudwatch-agent.json
{
  "agent": {
    "run_as_user": "app"
  },
  "logs": {
    "logs_collected": {
      "files": {
        "collect_list": [
          {
            "file_path": "/var/www/html/storage/logs/hogelog*.log",
            "log_group_name": "/hoge-log-group/hogelog",
            "log_stream_name": "all",
            "timezone": "UTC"
          }
        ]
      }
    }
  }
}
```

今回はアプリケーションの実行ユーザはappに降格してるので、 `run_as_user` でそのユーザを指定してあげます。

これは１つハマりポイントですが、Agentそのものはrootで開始されてないとエラーになって落ちてしまうので注意です。

このファイルに転送したいログファイルを記載しておきましょう。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html

次に、 `cloudwatch-agent-common-config.toml` を用意します。

```toml:cloudwatch/cloudwatch-agent-common-config.toml
# This common-config is used to configure items used for both ssm and cloudwatch access
 
 
## Configuration for shared credential.
## Default credential strategy will be used if it is absent here:
##            Instance role is used for EC2 case by default.
##            AmazonCloudWatchAgent profile is used for onPremise case by default.
# [credentials]
#    shared_credential_profile = "{profile_name}"
#    shared_credential_file= "{file_name}"
 
## Configuration for proxy.
## System-wide environment-variable will be read if it is absent here.
## i.e. HTTP_PROXY/http_proxy; HTTPS_PROXY/https_proxy; NO_PROXY/no_proxy
## Note: system-wide environment-variable is not accessible when using ssm run-command.
## Absent in both here and environment-variable means no proxy will be used.
# [proxy]
#    http_proxy = "{http_url}"
#    https_proxy = "{https_url}"
#    no_proxy = "{domain}"
```

こちらについては、特に何も設定してない状態ですがファイルが無いとこれもエラーになってしまうので入れておきます。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/install-CloudWatch-Agent-on-EC2-Instance-fleet.html

また、リージョンについては `.aws/config` に配置するファイルで設定しておきます。

```toml:cloudwatch/aws-config
[profile AmazonCloudWatchAgent]
region = ap-northeast-1
```

最後に、ENTRYPOINTを書き換えてCloudWatch Agentを立ち上げるためのスクリプトを配置しておきます。

```sh
#!/bin/sh
set -e

# CloudWatch Agentを起動する(rootでないと立ち上がらないので注意)
nohup /opt/aws/amazon-cloudwatch-agent/bin/start-amazon-cloudwatch-agent > /dev/null 2> /dev/null &

# workaround: ユーザ切り替え後に/dev/stderrと/dev/stdoutをappユーザから触れないのでパーミッションを変えておく
# https://superuser.com/questions/1699317/permission-denied-on-dev-stderr-after-sudo
chmod 777 /dev/stderr
chmod 777 /dev/stdout

# コマンドそのものはappユーザで実行する
sudo -u app "$@"
```

## DockerfileにCloudWatch Agent周りの諸々を入れる
```Dockerfile:Dockerfile
FROM debian:buster-slim

COPY --chmod=775 docker/entrypoint /usr/local/bin/entrypoint

COPY --from=amazon/cloudwatch-agent:1.247350.0b251780 /opt/aws/amazon-cloudwatch-agent /opt/aws/amazon-cloudwatch-agent
COPY cloudwatch/amazon-cloudwatch-agent.json /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json
COPY cloudwatch/cloudwatch-agent-common-config.toml /opt/aws/amazon-cloudwatch-agent/etc/common-config.toml

RUN useradd -m app

COPY --chown=app:app cloudwatch/aws-config /home/app/.aws/config

USER root

ENTRYPOINT /usr/local/bin/entrypoint
```

COPY命令でCloudWatch Agentのイメージから実行ファイルを取ってきて、必要な設定ファイルをイメージ内に入れてあげることで動かしています。

ENTRYPOINTだけを書き換えているので、元々実行したいCMDには影響することなく起動させることができます。