CoreOS + fleet でhubotを立ち上げてみた話


概要
--------
- CoreOS上にfleetのunitとしてhubotを立ち上げてみた
- 複数ホスト間でのLINKとかなければfleetは単純なツールなので使いやすかったという話

前提
--------
- Vagrantとvagrant-digitaloceanがインストール済み
- 手元にDocker開発環境が整備済み

やったこと
-------
### 1. CoreOSを立ち上げる
今回はvagrant経由でdigitalocean上にCoreOSのdropletsを立ち上げます。
Vagrantfileは下記のようになってます。
適宜、pathやファイル名など書き換えて使って下さい。

```ruby:
Vagrant.configure('2') do |config|
  config.ssh.username = 'core'

  config.vm.define "core-hubot" do |config|
    config.vm.provider :digital_ocean do |provider, override|
      override.ssh.private_key_path = '/path/to/your/ssh_key'
      override.vm.box               = 'digital_ocean'
      override.vm.box_url           = "https://github.com/smdahlen/vagrant-digitalocean/raw/master/box/digital_ocean.box"
      provider.token                = "YOUR digitalocean API TOKEN"
      provider.image                = 'coreos-stable'
      provider.region               = 'sgp1'
      provider.size                 = '512MB'
      provider.ssh_key_name         = "YOUR SSH KEY NAME"
      provider.setup                = false
      provider.private_networking   = true
      provider.user_data            = File.read('user-data.yml')
    end
  end
end
```

また、今回はCoreOSを立ち上げるに当たって、下記のuser-data.ymlをCoreOSにセットさせてます。

```yaml:
#cloud-config

coreos:
  etcd:
    # curl -w "\n" https://discovery.etcd.io/new
    discovery: https://discovery.etcd.io/YOUR_DISCOVERY_PATH
    addr: $private_ipv4:4001
    peer-addr: $private_ipv4:7001

  fleet:
    public-ip: $private_ipv4

  units:
    - name: etcd.service
      command: start
    - name: fleet.service
      command: start
```

etcdのホスト検出APIは`curl -w "\n" https://discovery.etcd.io/new`で各位取得して下さい。
ただし、今回はシングルホストなので特に用途はありません。CoreOSをクラスタ化して運用する際に重要なものになります。

### 2. hubotのDockerイメージを作成する
今回はシングルホストで運用するので、HubotとRedisは別々のコンテナとして運用し、LINKして運用する形式を採用しました。
hubotの開発自体はDocker外のローカル環境で行い、そのファイルをdocker build時にADDしています。
開発時のディレクトリ構成はこんな感じになってます。

```
.
├── Dockerfile    # hubot用Dockerfile
├── Vagrantfile
├── redis.service # redis用serviceファイル
├── hubot         # hubotの関連ファイル群
│   ├── Procfile
│   ├── README.md
│   ├── bin
│   ├── external-scripts.json
│   ├── hubot-scripts.json
│   ├── node_modules
│   ├── package.json
│   ├── scripts
│   └── start.sh
├── hubot-bot.service # hubot用serviceファイル
└── user-data.yml
```

hubot自体の開発に関する説明は割愛しますが、この辺を見るとわかると思います。
https://github.com/github/hubot/blob/master/docs/README.md


Dockerfileはnode.jsのコンテナをベースにするとhubotがサクッとインストールできるので楽だと思います。ちなみに今回はSlack投稿向けのhubotになってるので、そのへんの設定が追加されてます。

```shell:
FROM node:latest
MAINTAINER my@email.address

RUN npm install -g hubot coffee-script hubot-slack
RUN mkdir -p /root/hubot
ADD hubot/package.json /root/hubot/package.json
ADD hubot/bin /root/hubot/bin
RUN cd /root/hubot && npm install
ADD hubot/hubot-scripts.json /root/hubot/hubot-scripts.json
ADD hubot/external-scripts.json /root/hubot/external-scripts.json
ADD hubot/scripts /root/hubot/scripts
ADD hubot/start.sh /root/hubot/start.sh
RUN chmod a+x /root/hubot/start.sh

ENV HUBOT_SLACK_TOKEN YOUR_SLACK_TOKEN

CMD ["/root/hubot/start.sh"]
```

hubotの起動については、最後のCMDを見てもらえればわかると思いますが、簡単なシェルスクリプト実行で行ってます。

```shell:start.sh
#! /bin/sh
export REDIS_URL="redis://$REDIS_PORT_6379_TCP_ADDR:$REDIS_PORT_6379_TCP_PORT"
cd /root/hubot/
bin/hubot -a slack
```

ここでRedisのURLをredisという名前でlinkした前提で構築しています。

こちらのDockerfileをbuildし、docker hubのprivate registryにPUSHすればDockerのhubotイメージの準備完了です。

### 3. hubot、およびRedis用のserviceファイルを作る
fleetはsystemdに近い設定ファイルを用いてクラスタ内に指定しただけのサービスのunitを立ち上げます。

こちらはredisの立ち上げになります。

```redis.service
[Unit]
Description=Redis
After=docker.service
Requires=docker.service

[Service]
TimeoutStartSec=0
ExecStartPre=-/usr/bin/docker kill hubot-redis
ExecStartPre=-/usr/bin/docker rm hubot-redis
ExecStartPre=/usr/bin/docker pull redis:latest
ExecStart=/usr/bin/docker run --name hubot-redis -v /var/lib/redis:/data redis:latest redis-server --save 600 1
ExecStop=/usr/bin/docker stop redis
```

見てもらえればわかりますが、`ExecStart`にてdocker hubに公開されてるredisのイメージをベースに`hubot-redis`と言う名前で走らせてます。
また、Start前に今動いているコンテナを殺して、最新のコンテナをpullするようになっています。

```hubot.service
[Unit]
Description=Hubot
After=redis.service
Requires=redis.service

[Service]
TimeoutStartSec=0
ExecStartPre=-/usr/bin/docker kill hubot
ExecStartPre=-/usr/bin/docker rm hubot
ExecStartPre=/usr/bin/docker pull ymatsuwitter/hubot
ExecStart=/usr/bin/docker run --name hubot --link hubot-redis:redis ymatsuwitter/hubot
ExecStop=/usr/bin/docker stop hubot
User=core
```

hubotはredisに依存しているので、Unit節にて依存している旨を宣言しています。
それ以外は、`--link hubot-redis:redis`にてRedisをlinkしているところが先ほどと違います。

こちらの2つのサービスファイルができたらデプロイしてみましょう。

### 4. fleetctlでデプロイしてみる
とりあえず今回はCoreOSにログインして、そこにserviceファイルを配置、動作させるという手順を踏んでますが、別にログインせずともfleetctlは利用できるので`fleetctl help`を確認してみましょう。

では早速デプロイですが、先ほどのserviceファイル2つをCoreOS上にscpなりで持って来たら、

```
$ fleetctl load redis.service
$ fleetctl start redis.service
$ fleetctl load hubot.service
$ fleetctl start hubot.service
```

を実行すればすぐにhubotが動き始めるはずです。
もし何か問題がでている場合は、`fleetctl journal <service名>.service`で標準出力を確認できるので利用してみてください。

今後デプロイする際は、Dockerfileをビルド、Pushした後、

```
$ fleetctl stop hubot.service
$ fleetctl start hubot.service
```

すれば完了します。

最後に
-------
今回CoreOS上でhubotを動作させてみての感想ですが、サーバ跨いでのlinkがdockerには出来ないため、ここの解決さえ出来れば（例えばAWSならPrivate DNSの利用なりConsulを使うなり）、非常に夢のあるツールかなと思いました。kubernetesでもいいのかもしれませんが、名前解決関連を気にしない場合は非常にシンプルなツールなのでfleetも是非使ってみてください。
今後、その辺の運用ノウハウも公開していきます。