AWSにおけるDockerコンテナの監視

この記事は[株式会社ネクスト(Lifull) Advent Calendar 2016](http://qiita.com/advent-calendar/2016/lifull)の9日目です。

業務でDockerコンテナを導入してみたいと思ったので
それらを監視するために調べた色々を書き出します。
「ログ監視」「リソース監視」の観点でまとめてみました。

## :computer: 環境

弊社で運営している多くのWebサービスではAWSを利用しています。
AWS上でコンテナを利用する方法はいくつかありますが
基本はElasticBeanstalk（以下、EBと略）上での複数コンテナを想定します。
（とはいえ裏側ではECSが動いているのですが）

## :page_with_curl: ログ監視

### Docker: logsコマンド

Dockerのコマンドで、ホストインスタンスから
コンテナ内のSTDOUTとSTDERRを取得することができます。

```shell:例：アプリケーションコンテナに対するlogsコマンド
$ docker logs a59845048cea
[2016-12-05 17:10:05 +0000] [7] [INFO] Starting gunicorn 19.6.0
[2016-12-05 17:10:05 +0000] [7] [INFO] Listening at: http://0.0.0.0:5000 (7)
[2016-12-05 17:10:05 +0000] [7] [INFO] Using worker: sync
[2016-12-05 17:10:06 +0000] [10] [INFO] Booting worker with pid: 10
```

デバッグ時には便利ですが、毎回インスタンスに入るわけにもいきません。

Dockerには[ロギングドライバ](https://docs.docker.com/engine/admin/logging/overview/)という機能があり
`logs`で得られる内容をいくつかの方法で外部に出力することができます。

### AWS: CloudWatch Logsの活用

Dockerは前述のロギングドライバにおいて
公式で[Cloudwatch Logsへの出力](https://docs.docker.com/engine/admin/logging/awslogs/)をサポートしています。

EBでは`--log-driver`のように`docker run`の際に渡すパラメータを
[dockerrun.aws.json](http://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/create_deploy_docker_v2config.html#create_deploy_docker_v2config_dockerrun)という独自形式のjsonに定義します。
このファイルの中のコンテナ定義形式はECSと共通なので
ログの設定については[ECSのドキュメント](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html)に記載がありました。

```javascript:ロギングの設定
"logConfiguration": {
      "logDriver": "json-file"|"syslog"|"journald"|"gelf"|"fluentd"|"awslogs",
      "options": {"string": "string"
        ...}
```

`logDriver`については`awslogs`を選択して問題なさそう。
[options](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/using_awslogs.html#d0e13461)には、`awslogs-region`と`awslogs-group`を指定すればよさそうです。

試しに以下の定義でnginxコンテナのログを出力してみます。
公式のnginxイメージで、内容は80番ポートを開けているだけです。

```javascript:Dockerrun.aws.json
{
  "AWSEBDockerrunVersion": "2",
  "containerDefinitions": [
    {
      "name": "nginx",
      "image": "nginx:1.11.5-alpine",
      "essential": true,
      "memory": 128,
      "portMappings": [
        {
          "hostPort": 80,
          "containerPort": 80
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-region": "ap-northeast-1",
          "awslogs-group": "nginx-group"
        }
      }
    }
  ]
}
```

**指定したロググループがCloudWatch側に定義されていないと
EBのデプロイごとコケるようなので注意してください。**

デプロイが成功すると、CloudWatchのコンソールやcliでログが見られます。
何度か踏んだ結果、アクセスログとエラーログの両方が出ているようです。

![awslogs.png](https://qiita-image-store.s3.amazonaws.com/0/43116/5a167128-6ee1-d861-6e32-bb6e6beae0b2.png "awslogs.png")

ちなみに、STDOUTとSTDERRを表示するはずの`docker logs`で
これらが見られるのは公式のnginxイメージを使用したからです。
Dockerfileを見ると、ログの出力先がシンボリックリンクになっています。

```text:Dockerfile
# forward request and error logs to docker log collector
&& ln -sf /dev/stdout /var/log/nginx/access.log \
&& ln -sf /dev/stderr /var/log/nginx/error.log
```

`docker logs`の集約についてはこの方法でなんとかなりそうです。
STDOUTとSTDERRを分けられないのは難点ですが
DockerのIssueでも少なからず[議論](https://github.com/docker/docker/issues/25683)があるので、いずれ改善されるかもしれません。
ちなみに、[fluentdのロギングドライバ](http://docs.docker.jp/engine/admin/logging/fluentd.html)では分けることができるようです。

また、EBでホストインスタンスにアクセスすると
`/var/log/docker-events.log`などのログが残されていることもわかります。
こちらは上記のロギングドライバで出力されない情報なので
[.ebextensions](http://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/ebextensions.html)などでホスト側にログ出力の仕組みを作る必要がありそうです。

## :bar_chart: リソース監視

### Docker: statsコマンド

Dockerのコマンドで、コンテナごとのリソース使用量を表示します。

```shell:例：アプリケーションとDBを起動した状態でstatsコマンド
$ docker stats
CONTAINER           CPU %               MEM USAGE / LIMIT       MEM %               NET I/O             BLOCK I/O             PIDS
a59845048cea        1.02%               39.42 MiB / 1.951 GiB   1.97%               1.038 kB / 648 B    32.77 kB / 0 B        5
ac7470b95cef        0.19%               353.9 MiB / 1.951 GiB   17.72%              1.296 kB / 648 B    4.149 MB / 317.8 MB   27
```

Linuxの`top`コマンドのように使えてわかりやすいのですが
集約や保存のためにはこれでは不十分です。

### AWS: CloudWatchメトリクス

EBやECSで複数コンテナを扱う場合は
ECSのコンソールでメトリクスを確認することができます。

インスタンスのクラスタ単位では以下の[4つのメトリクス](http://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/ecs-metricscollected.html)が見えます。

![metrics.png](https://qiita-image-store.s3.amazonaws.com/0/43116/c51732c0-0188-394f-5bcb-93a67ff2d5f5.png "metrics.png")

ECSで言うサービスの単位では、以下の2つのメトリクスのみでした。

![service.png](https://qiita-image-store.s3.amazonaws.com/0/43116/4e438f27-c135-bca3-90d4-a4d52d67d1a1.png "service.png")

ちなみにEBのコンソールでは
コンテナではない通常のインスタンス監視と特に変わりません。

![eb.png](https://qiita-image-store.s3.amazonaws.com/0/43116/7b6fb7dd-aa58-59fa-74ae-17a997d2325f.png "eb.png")


見慣れたCloudWatchの形式で見られるのはありがたいのですが
コンテナごとの使用リソースなど、もう少し細かい部分が気になります。

### サードパーティのサービス/ソフトウェアに頼る

[cAdvisor](https://github.com/google/cadvisor)や[ELKスタック](https://www.elastic.co/jp/webinars/introduction-elk-stack)などのソフトを活用したメトリクス収集や
フルスタックな有料サービスの活用など、追加の選択肢は多く存在します。
自部署では監視領域で全般的に[Mackerel](https://mackerel.io)を利用しているため
コンテナのメトリクス可視化も試してみました。

Mackerelではagentを使用してメトリクス情報を送信しますが
コンテナのかたちで監視を行う[mackerel-agent](https://hub.docker.com/r/mackerel/mackerel-agent/)も公式で提供されています。
これで、一緒に起動しているコンテナとホストインスタンスの監視が可能です。

ドキュメント内では`docker run`の引数として書かれている部分を
Dockerrun.aws.jsonに落とし込んでみます。

```javascript:Dockerrun.aws.json
{                                                                                     
  "AWSEBDockerrunVersion": "2",                                                       
  "volumes": [                                                                        
    {                                                                                 
      "name": "docker-sock",                                                          
      "host": {                                                                       
        "sourcePath": "/var/run/docker.sock"                                          
      }                                                                               
    },                                                                                
    {                                                                                 
      "name": "lib-mackerel-agent",                                                   
      "host": {                                                                       
        "sourcePath": "/var/lib/mackerel-agent"                                       
      }                                                                               
    }                                                                                 
  ],                                                                                  
  "containerDefinitions": [                                                           
    {
      "nginxコンテナの設定は": "省略"
    },
    {
      "name": "mackerel-agent",                                                       
      "image": "mackerel/mackerel-agent",                                             
      "environment": [                                                                
        { "name": "apikey", "value": "Hoge8noPEnZj9HogeMEwnHogep5aMQzKMHogee7ZHoge" },
        { "name": "enable_docker_plugin", "value": true },                            
        { "name": "auto_retirement", "value": true }                                  
      ],                  
      "essential": true,                            
      "memory": 128,                                                                         
      "mountPoints": [                              
        {                                           
          "sourceVolume": "docker-sock",            
          "containerPath": "/var/run/docker.sock"   
        },                                          
        {                                           
          "sourceVolume": "lib-mackerel-agent",     
          "containerPath": "/var/lib/mackerel-agent"
        }                                           
      ]                                             
    }                                               
  ]                                                 
}                                                                                                               
```

mackerel-agentコンテナには
サービスを利用するためにapikeyなどの環境変数を渡しています。
また、メトリクスデータのやりとりのためのボリュームマウントが必須です。

上記のDockerrun.aws.jsonからコンテナを起動した結果
Mackerelのコンソール上でメトリクスが閲覧できます。

以下のグラフはホストインスタンスの使用メモリを示します。

![memory.png](https://qiita-image-store.s3.amazonaws.com/0/43116/2009ee77-e166-196a-0711-4096af130da9.png "memory.png")

メモリ以外にもloadaverate、cpu、diskioなどのメトリクスが見られます。

さらに、`{ "name": "enable_docker_plugin", "value": true }`の設定により
起動しているコンテナのメトリクスも取得することができます。

以下のグラフは **各コンテナ** の使用メモリを示しています。

![docker_memory.png](https://qiita-image-store.s3.amazonaws.com/0/43116/de1ef2f4-98eb-a4c6-805d-e4de5e9c4ecb.png "docker_memory.png")

勝手に起動するecs-agentを含めて、3つのコンテナのメトリクスがあります。

これに加えてBlkIO Bytes、BlkIO IOPS、CPUの4種類が閲覧可能で
それぞれのメトリクスを起点としたアラートも飛ばせるため
かなり柔軟に監視対応ができます。

CloudWatchのメトリクスだけではかゆいところに手が届かないため
Mackerelに限らず、監視専用のソリューションは使った方が良いと思います。

## :end: まとめ

コンテナは通常のサーバ運用と比べて永続性がないため
ログやメトリクスを集約し、残していくことは特に重要だと思っています。
もっと監視が簡単になって、導入の障壁がなくなれば良いですね。