EC2インスタンス内のdocker containerをdocker statsでcontainerを監視して閾値を超えたらSlackに通知するRuby

# 背景

![image](https://qiita-image-store.s3.amazonaws.com/0/38292/65dd0d47-916f-ff38-0c57-e187560b5f75.png)

Docker Hostの中で複数のContainerが走っている場合、どのContainerがどれくらい負荷がかかっているか？といったようなモニタリングをしたいなと思いました。AWSの場合ですとECSなど高機能でHostedなソリューションもございますが、今回はそういったケースではない場合においてDocker HostのContainerの負荷状況を監視するような仕組みを考えたいと思いました。そこでなんとかいい感じにSlackに流せないかな？と思いました。



# 構成

Deploy Serverから4台のDocker Hostに対して内部で実行されているDocker ContainerのContainer Nameを取得して`docker stats`して整形します。

```
[EC2] Deploy Server
  ├ [EC2] docker-host-1
  │   ├ [Docker Container] elevennines-web-1
  │   └ [Docker Container] elevennines-web-2
  ├ [EC2] docker-host-2
  │   ├ [Docker Container] elevennines-web-1
  │   └ [Docker Container] elevennines-web-2
  ├ [EC2] docker-host-3
  │   ├ [Docker Container] elevennines-job-1
  │   └ [Docker Container] elevennines-job-2
  └ [EC2] docker-host-4
      ├ [Docker Container] elevennines-job-1
      └ [Docker Container] elevennines-job-2
```

# `~/.ssh/config`からHostを持ってきて使う

Deploy ServerのDeployユーザーの`~/.ssh/config`でdocker-hostたちを宣言しているので普段から`ssh docker-host-1 docker ps`とかしていたりしたワケです。今回はこれも利用します。`Host *****`の部分を使います。`HostName`でもよかったんですけどね…

```
<pts/7> % cat ~/.ssh/config
Host docker-host-1
    HostName        10.0.11.***

Host docker-host-2
    HostName        10.0.1.**

Host docker-host-3
    HostName        10.0.11.**

Host docker-host-4
    HostName        10.0.1.**
```

# `docker stats`について

Docker containerのステータスを取得するコマンドの一種ですが`--no-stream`を付けることでその瞬間のステータスを取得します。以下の場合、`docker-host-1`で`elevennines-web-1`というContainerのステータスを取得しています。

```
<pts/5> % docker stats --no-stream elevennines-web-1
CONTAINER           CPU %               MEM USAGE / LIMIT   MEM %               NET I/O               BLOCK I/O
elevennines-web-1   0.00%               559 MB / 8.375 GB   6.67%               1.926 MB / 1.259 MB   72.22 MB / 3.944 MB
```

詳しくは公式Documentをご覧ください:bow:

* [stats - Docker](https://docs.docker.com/engine/reference/commandline/stats/)

定期的に上記の以下フィールドをRubyで整形して閾値を超えた場合Slackの監視用Channelに流したいと思います。

* CONTAINER
* CPU %
* MEM USAGE / LIMIT
* MEM %
* NET I/O
* BLOCK I/O

# `container_watch.rb`とでもしておきましょうか

ユーザーのホームディレクトリに置きましょうか。aws-cli周りの初期設定等は事前に行っておいてください:bow: また[SlackのIncoming Webhookの取得](http://qiita.com/vmmhypervisor/items/18c99624a84df8b31008)もお忘れなく。URLをslack_incoming_webhook_urlに設定してください。

```rb:/home/tokifujp/container_watch.rb
require 'net/http'
require 'uri'
require 'json'

# AWS EC2 Instance ID (AWSじゃない場合は要らないです)
instanceid = %x[ curl -s 169.254.169.254/latest/meta-data/instance-id/ ]
cpu_max = 80 # CPU %が80%を超えたら通知
mem_max = 80 # MEM %が80%を超えたら通知

def post2slack(channel, bot_name, icon_emoji, text, subsys_name, cpu, mem_usage_limit, mem, net_io, block_io)
    # Slack Incoming Webhook URL
    slack_incoming_webhook_url = URI.parse("https://hooks.slack.com/services/*********/*********/************************")
    payload = {
        text: text,
        channel: channel,
        username: bot_name,
        icon_emoji: icon_emoji,
        attachments: [
        {
            fields: [
                {
                    title: "CONTAINER",
                    value: subsys_name,
                    short: true
                },
                {
                    title: "CPU %",
                    value: cpu,
                    short: true
                },
                {
                    title: "MEM USAGE / LIMIT",
                    value: mem_usage_limit,
                    short: true
                },
                {
                    title: "MEM %",
                    value: mem,
                    short: true
                },
                {
                    title: "NET I/O",
                    value: net_io,
                    short: true
                },
                {
                    title: "BLOCK I/O",
                    value: block_io,
                    short: true
                }
            ],
            color: "red"
        } ]
    }
    Net::HTTP.post_form(slack_incoming_webhook_url, { payload: payload.to_json })
end

# ~/.ssh/configのHostを取ってくる
hosts = %x[cat ~/.ssh/config | grep "Host " | sed -e 's/Host //' | sed '/[*]/d']
hosts.each_line do |host|
    channel = "channel_name" # Slack Channel Name
    bot_name = "[#{instanceid}] #{host.strip}" # Slack Bot Name
    icon_emoji = ":thumbs_up:" # Slack Icon Emoji
    text = "" # Slack text message
    subsys_name, cpu, mem_usage_limit, mem, net_io, block_io = ''
    subsys_name_to, cpu_from, cpu_to, mem_usage_limit_from, mem_usage_limit_to, mem_from, mem_to, net_io_from, net_io_to, block_io_from, block_io_to = 0

    # Docker Host内のDocker ContainerのNameを取得
    docker_stats = %x[ssh #{host.strip} docker ps --format "{{.Names}}" | ssh #{host.strip} xargs docker stats --no-stream]
    docker_stats.each_line do |docker_stat|
        if docker_stat.strip.index('CPU %') then
            # 各フィールドの開始位置
            cpu_from = docker_stat.strip.index('CPU %')
            mem_usage_limit_from = docker_stat.strip.index('MEM USAGE / LIMIT')
            mem_from = docker_stat.strip.index('MEM %')
            net_io_from = docker_stat.strip.index('NET I/O')
            block_io_from = docker_stat.strip.index('BLOCK I/O')

            # 各フィールドの終了位置
            subsys_name_to = cpu_from - 1
            cpu_to = mem_usage_limit_from - cpu_from - 1
            mem_usage_limit_to = mem_from - mem_usage_limit_from - 1
            mem_to = net_io_from - mem_from - 1
            net_io_to = block_io_from - net_io_from - 1
            block_io_to = docker_stat.strip.length
        else
            # 各フィールドの値をセット
            subsys_name = docker_stat.strip[0, subsys_name_to].strip
            cpu = docker_stat.strip[cpu_from, cpu_to].strip
            mem_usage_limit = docker_stat.strip[mem_usage_limit_from, mem_usage_limit_to].strip
            mem = docker_stat.strip[mem_from, mem_to].strip
            net_io = docker_stat.strip[net_io_from, net_io_to].strip
            block_io = docker_stat.strip[block_io_from, block_io_to].strip

            # 閾値を超えたらSlack通知
            if cpu.gsub('%', '').to_f >= cpu_max || mem.gsub('%', '').to_f >= mem_max then
                post2slack(channel, bot_name, icon_emoji, text, subsys_name, cpu, mem_usage_limit, mem, net_io, block_io)
            end
        end
    end
end
```

# スクリプトを実行してみる

CPUが閾値を超えているケースで以下みたいにSlackに通知されます。意図したようにSlackに投稿されていたらテストはOKです。用途に応じて閾値を下げて確認すると良いかもしれません。

![image](https://qiita-image-store.s3.amazonaws.com/0/38292/65dd0d47-916f-ff38-0c57-e187560b5f75.png)

# crontabに登録しておく

5分毎に実行する場合の例だとこんな感じでしょうか。

```
<pts/7> % crontab -l                                                                                                                                                                                                             
*/5 * * * * ruby /home/tokifujp/container_watch.rb
```

# 最後にというか所感的なもの

どうしてもコストとの兼ね合いなどで同じDocker Hostに複数のContainerを実行しなくてはならなくて、ContainerごとにDocker Hostのリソースを取り合って落ちたりすることがあります。そうならば予めContainerの負荷状況をこうして監視できると少し楽ができるかもしれませんね。