GitLab-CIを使って、Docker上にHubot製Botをデプロイする

Hubotというチャットbot開発・実行フレームワークがあります。
Node.jsで動作するのでHerokuとか適当なサーバにデプロイすればよいのですが、
せっかく勉強しているのでDockerにデプロイしようと思います。
また、botのソースコードはGitLabで管理しているので
GitLabにPushすると、Dockerにデプロイすることを目指します。

## 全体像

![全体像.png](https://qiita-image-store.s3.amazonaws.com/0/217768/be5fdf69-b4f3-ecf0-99d7-cb912442c599.png)

GitLab-Runner、Build VM、HomeBot、RedisはすべてDocker上で動作します。
HomeBotは、Hubotで作ったbotでこれがGitLabでソースコード管理されているものです。
Dokcer上で動いているGitLab-RunnerがDocker上にBuild VMを起動して、
さらにBuild VMがDocker上にhomebot、Redisを起動します。
感覚的にはDocker in Docker in Docker の3階建てです。

![3階建て.png](https://qiita-image-store.s3.amazonaws.com/0/217768/e2c3bbd7-d16c-61f6-dc93-3b5d2ffe41cd.png)

ただ、Build VMはビルドが終わると落ちてしまうので、ずっと稼働し続ける予定の
homebotとRedisをここに配置するわけにはいきません。
（もしかして何か別の方法があるのでしょうか？）

Docker in Dockerの1種として、Dockerコンテナ上からHostのDocker機能を呼び出せる仕組みを使って、
Docker上のBuild VMからホストのDockerにhomebotとRadisをデプロイします。
ついでにBuild VMもホストのDocker上で動かすことにして、実際には以下のように横に並びます。

![横並び.png](https://qiita-image-store.s3.amazonaws.com/0/217768/0a5efb0a-fe6e-9add-becb-b3df37c63c75.png)

## ちょっとだけ余談

ここで作っているBotは、スマートスピーカーであるGoogle Homeを使って照明やエアコンを操作するものです。
ベース部分はこちらのサイトを参考にしています。

[Smart Home - Scrapbox](https://scrapbox.io/smart-home/TOP)

この記事には直接関係ありませんが、こういったことに興味がある方は是非ご覧ください。

## 前提条件

* GitLab ver.9.1.3
* GitLab-runner ver.10.5.0
* Docker ver.18.03.0-ce
* Hubot ver.2.19
* Redis ver.4.0.8

## Hubot開発環境とGitLabリポジトリを用意

Hubotの環境の準備は外部のサイトを参考にしてください。
特に説明する必要はないと思いますが、GitLabにリポジトリを作成してPushしておきます。

Hubot開発環境の構成ファイルはこんな感じのはずです。

    root
    ├ bin
    ├ lib
    ├ scripts
    ├ .gitignore
    ├ .hubot_history
    ├ external-scripts.json
    ├ package.json
    ├ Procfile
    └ README.md

## Dockerfileを用意

Botが動作するDockerイメージを作るためのdockerfileをリポジトリに追加します。

    root
	├ bin
	├ lib
	├ scripts
	├ .gitignore
	├ .hubot_history
	├ dockerfile          <-NEW!!
	├ external-scripts.json
	├ package.json
	├ Procfile
	└ README.md

dockerfile

	FROM centos

	RUN yum -y update
	RUN yum -y install epel-release
	RUN yum -y install git nodejs npm

	COPY . /app/bot/homebot
	WORKDIR /app/bot/homebot
	RUN npm install
	CMD sh /app/bot/homebot/bin/hubot -a slack

たいしたことはしていなくて、
Docker Buildでは、centosイメージをベースにして（深い意味はないです。単に好みです）、
gitとnodejs、npmを取得、リポジトリの中を /app/bot/homebot に配置してnpm installしておく。
実行時はHubotを起動するだけです。


ここまでで、手動デプロイができるようになったはずです。

dockerホストにgit cloneしてビルドが成功することを確認しておきます。

    # cd 適当なフォルダ
    # git clone http://GitLabのリポジトリURL
    # cd リポジトリのフォルダ
    # docker build -t homebot ./

また、ビルドされたイメージを使ってHubotを起動して、実装した応答を確認できます。

    # docker run --rm -it -w /app/bot/homebot/ homebot sh /app/bot/homebot/bin/hubot
    homebot> [Mon Mar 26 2018 13:02:49 GMT+0000 (UTC)] INFO hubot-redis-brain: Using default redis on localhost:6379

    homebot> homebot ping
    homebot> PONG
    homebot> exit



## GitLab-Runnerの準備


Docker in Dockerを行うためには
起動したDockerコンテナ内でDockerコマンドを実行する必要があります。
そのためのDockerクライアントを事前に用意しておきます。

    mkdir -p /root/gitlab-runner/bin
    curl -fsSL https://get.docker.com/builds/Linux/x86_64/docker-17.05.0-ce.tgz  | tar -xzC /root/gitlab-runner/bin/ --strip=1 docker/docker

Dockerクライアントの入手はこちらを参考にしています。
https://qiita.com/minamijoyo/items/c937fb4f646dc1ff064a

また、GitLabを開いて、gitlab-runnerを登録するためのregistration-tokenを控えておきます。

<img width="459" alt="GitLab-Runner-Regist-Token.png" src="https://qiita-image-store.s3.amazonaws.com/0/217768/a42b061f-d2e4-9520-01e8-7ad43b120521.png">



## GitLab-Runnerの起動と登録

GitLab-CIはRunnerと呼ばれるCIの実行エンジンをCIを実行するPCに常駐させます。
この仕組みのおかげで、違うOSでビルドを動かしたり、CIサーバの数を増やしたりといった柔軟性のある運用が可能です。

今回はGitLab-RunnerもDocker上で動かして常駐します。
まずは、gitlab-runnerの公式イメージを起動します。

	 ♯ docker run -d \
	    --name gitlab-runner-docker \
	    -h gitlab-runnder \
	    -v /root/gitlab-runner/config:/etc/gitlab-runner \
	    -v /root/gitlab-runner/bin/docker:/usr/local/bin/docker \
	    -v /var/run/docker.sock:/var/run/docker.sock \
	    gitlab/gitlab-runner 

ポイントとしては、3つのボリュームをマウントします。

* /etc/gitlab-runner <br/>
  gitlab-runnerの設定ファイルがここに保存されます。
  ホストにマウントしておくことでコンテナを作り直しても設定を復元できます。
* /usr/local/bin/docker <br/>
  上で取得したdockerクライアントを/usr/local/bin/dockerに配置します。
* /var/run/docker.sock <br/>
  Docker in Dockerで、ホストのDockerを操作するためにdocker.sockを共有します。
  


起動したコンテナでgitlab-runner registerを実行して、GitLabに登録します。

     ♯ docker exec -it gitlab-runner-docker \
	   gitlab-runner register --non-interactive \
	  --url https://(GitLabのURL)/ci \
	  --registration-token XXXXXXXXXXXXXXXXXX \
	  --executor docker \
	  --description "Docker-in-Docker" \
	  --tag-list docker-in-docker \
	  --docker-image "docker" \
	  --docker-privileged \
	  --docker-volumes /var/run/docker.sock:/var/run/docker.sock \
	  --docker-volumes /root/gitlab-runner/bin/docker:/usr/local/bin/docker

それぞれのパラメータは以下の通りです。

* url <br/>
  GitLabのURLです。「/ci」は付けても付けなくてもよいらしいですが、一応つけておきます。
* registration-token <br/>
  上で、GitLab上で確認したregistration-tokenを指定します。
* executor <br/>
  dockerを指定して、CI実行時に新たにdockerコンテナを起動してその中でビルドします。他にも直接シェル上で実行するshellがよく使われると思いますが、dockerがおすすめらしいので、そちらを指定します。
* tag-list <br/>
  実行するrunnerを選ぶためのキーワードを指定します。何でもよいのですが、runnerの実行環境にあった名前が良いです。
* docker-* <br/>
  runnerから起動されるdockerコンテナ(=Build VM)の情報を指定します。
  * docker-image <br/>
    デフォルトのDockerイメージで、ここでは基本的にgitlab-ci.ymlで指定するので適当に。
  * docker-privileged <br/>
    Build VMは特権モードで起動します。これはもしかすると不要かもしれません。
  * docker-volumes <br/>
    runnerを作った時と同じですが、ホストのdockerを操作するためのsockとdockerクライアントを配置します。
    注意点としては、docker in dockerといっても、マウントするボリュームはBuild VMのパスではなく、ホストのパスになる点です。

GitLabを開いて、runnerが登録されていることを確認します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217768/2c4e6d40-e62e-3b5c-aaed-a6a10c645d50.png)



最後に、先ほど登録したrunnerのEditを開き、GitLabのリポジトリに関連付けます。
これは設定によっては不要なようですが、安全のために手動で設定するようにしています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217768/a70633ea-9d77-7a53-54f1-1a6c00dc91c7.png)


## デプロイの準備

BotをSlackと連携させるにはSlack上にHubotアプリを追加し、Bot側にはAPIトークンを渡す必要があります。
SlackのHubotページを開いて、HUBOT_SLACK_TOKENを控えておきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217768/d35ec845-536d-9d90-9995-1cf5e05827fd.png)


## ビルドとデプロイのスクリプト作成

上で作ったrunnerから起動するdockerコンテナは、このコマンドと概ね同じはずです。
この中で、ビルドとデプロイを試して、スクリプトを作成します。

    # docker run --privileged --rm -it \
       -v /var/run/docker.sock:/var/run/docker.sock \
       -v /root/gitlab-runner/bin/docker:/usr/local/bin/docker \
       docker /bin/bash


### ビルド

リポジトリをcloneして、docker buildします。

    # cd 適当なフォルダ
    # git clone http://GitLabのリポジトリURL
    # cd リポジトリのフォルダ
    # docker build -t homebot ./

### デプロイ

redisとhomebotのコンテナを、古いものをstop & rmしてから、起動します。

    # docker stop "redis"    || echo ignore errors.
    # docker rm   "redis"    || echo ignore errors.
    # docker stop "homebot"  || echo ignore errors.
    # docker rm   "homebot"  || echo ignore errors.
    # docker run --name redis -d -p 6379:6379 -v /root/redis/data:/data redis redis-server --appendonly yes
    # docker run --name homebot -e REDIS_URL=http://127.0.0.1:6379/hubot -e HUBOT_SLACK_TOKEN=YYYYYYYYYYY --net=host -d homebot

docker stopやdocker rm時に指定したコンテナがないとエラーになり、
そこでビルドスクリプトが止まってしまうので、エラーを無視するようにします。
redisコンテナは、redis公式イメージで、ポートの公開(-p 6379:6379)、永続化用のフォルダのマウント(-v /root/redis/data:/data)、書き込みオプションの指定(--appendonly yes)を行います。
homebotコンテナは、環境変数でredisのURLとHUBOT_SLACK_TOKENの指定と、ネットワークをホストと同じにするオプション(--net=host)を指定します。
--net=hostは普通にHubotを使うだけなら不要です。今回作ったBotはスクリプトから赤外線送信モジュール(RM-Mini3)を使う関係上、探索するIPを外部ネットワークと同じにする必要があり、指定しています。


## .gitlab-ci.ymlを配置

リポジトリのルートに.gitlab-ci.ymlを配置します。
これがあると、git push時にGitLabが自動的にCIを走らせます。

    root
	├ bin
	├ lib
	├ scripts
	├ .gitignore
	├ .gitlab-ci.yml    <- NEW!!
	├ .hubot_history
	├ dockerfile
	├ external-scripts.json
	├ package.json
	├ Procfile
	└ README.md

上で試したビルドとデプロイのスクリプトを.gitlab-ci.ymlに記載します

.gitlab-ci.yml

    image: centos

    stages:
      - build
      - deploy

    build:
      stage: build
      tags:
        - docker-in-docker
      script:
        - docker build -t homebot .

    deploy to production:
      stage: deploy
      tags:
        - docker-in-docker
      variables: 
        REDIS_URL: redis://127.0.0.1:6379/hubot
        HUBOT_SLACK_TOKEN: YYYYYYYYYYYYYYY
      script: 
        - docker stop "redis"    || echo ignore errors.
        - docker rm   "redis"    || echo ignore errors.
        - docker stop "homebot"  || echo ignore errors.
        - docker rm   "homebot"  || echo ignore errors.
        - docker run --name redis -d -p 6379:6379 -v /root/redis/data:/data redis redis-server --appendonly yes
        - docker run --name homebot -e REDIS_URL=$REDIS_URL -e HUBOT_SLACK_TOKEN=$HUBOT_SLACK_TOKEN --net=host -d homebot
        - docker rmi `docker images -f "dangling=true" -q` || echo ignore errors.

ビルドのフェーズ(stage)はbuildとdeployの2つ。

上で作ったスクリプトと大きくは変わりませんが、
REDIS_URLとHUBOT_SLACK_TOKENは環境変数にしてみました。
また、docker-in-dockerのタグをつけて、今回作ったRunnerを対象にします。
最後の「docker rmi ～」は、ビルドを繰り返していくとイメージがたまっていくので、名前がついていないイメージを削除しています。
意図しないイメージ削除が走る可能性があるので少しいまいちですが。

## 試してみる

ここまでで、GitLabへPush→Dockerコンテナの起動ができるようになったはずです。

ということで、Pushしてみます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217768/0976d8aa-2c90-ce9d-8557-da67af4b2f83.png)

GitLabリポジトリのPipelinesを開くと、ビルドされていることがわかります。





## HUBOT_SLACK_TOKENをリポジトリから排除する

HUBOT_SLACK_TOKENをリポジトリにコミットしてしまうと消すことができません。
考えすぎかもしれませんが、将来的にリポジトリに参加するデベロッパーが増えたときに
誰でもSlack上のBotを触れる状態は良くないかもしれません。
GitLabには「Secret Variables」という機能があり、こういった非公開パラメータを管理する仕組みがあるので使ってみます。

「Secret Variables」は、リポジトリのページ→Settings→CI/CD Pipelinesにあります。
ここにHUBOT_SLACK_TOKEN_PRODUCTIONとして定義します。
ここに定義した変数は環境変数としてRunner上から使用できます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217768/5f4b0cd2-4896-fddb-8b6d-da0ddab749a6.png)


この環境変数を使用するように.gitlab-ci.ymlを変更します。

.gitlab-ci.yml

    image: centos

    stages:
      - build
      - deploy

    build:
      stage: build
      tags:
        - docker-in-docker
      script:
        - docker build -t homebot .

    deploy to production:
      stage: deploy
      tags:
        - docker-in-docker
      variables: 
        REDIS_URL: redis://127.0.0.1:6379/hubot
        HUBOT_SLACK_TOKEN: $HUBOT_SLACK_TOKEN_PRODUCTION          #Changed!
      script: 
        - docker stop "redis"    || echo ignore errors.
        - docker rm   "redis"    || echo ignore errors.
        - docker stop "homebot"  || echo ignore errors.
        - docker rm   "homebot"  || echo ignore errors.
        - docker run --name redis -d -p 6379:6379 -v /root/redis/data:/data redis redis-server --appendonly yes
        - docker run --name homebot -e REDIS_URL=$REDIS_URL -e HUBOT_SLACK_TOKEN=$HUBOT_SLACK_TOKEN --net=host -d homebot
        - docker rmi `docker images -f "dangling=true" -q` || echo ignore errors.



## 本番サーバーとステージングサーバーを分ける

さて、GitLabへPush→デプロイができるようになりましたが、
Pushするだけでいきなり本番サーバーにデプロイするのは、壊してしまいそうで少し怖いです。
一旦、ステージングサーバーにデプロイして動作を確認した後、本番サーバーにデプロイできれば、気分的に楽ですね。

![図7.png](https://qiita-image-store.s3.amazonaws.com/0/217768/a5feba68-2ee4-ae72-7600-7753d2c1dd06.png)



せっかくDockerを使っているのでこの辺のインスタンス数を簡単に増やしてみます。

方法としては、GitLab Flowを少し意識して、次のようにします。
* Masterブランチへコミット → Staging環境にデプロイ
* Productionブランチへコミット → Production環境にデプロイ

ちなみに、GitLabにはManual actionsという機能があり、GitLabのPipeline上からボタンを押すとデプロイする仕組みがあるようです。
面白そうですが、特定のCI機能に依存すると移植性が落ちるので、gitのブランチで実現しています。

そのために.gitlab-ci.ymlを変更して、Staging用のデプロイを増やします。

.gitlab-ci.yml

    image: centos

    stages:
      - build
      - deploy

    build:
      stage: build
      script:
        - docker build -t homebot .
      tags:
        - docker-in-docker

    deploy to staging:
      stage: deploy
      variables: 
        REDIS_URL: redis://127.0.0.1:16379/hubot
        HUBOT_SLACK_TOKEN: $HUBOT_SLACK_TOKEN_STAGING
      script:
        - docker stop "redis-staging"    || echo ignore errors.
        - docker rm   "redis-staging"    || echo ignore errors.
        - docker stop "homebot-staging"  || echo ignore errors.
        - docker rm   "homebot-staging"  || echo ignore errors.
        - docker run --name redis-staging -d -p 16379:6379 -v /root/redis-staging/data:/data redis redis-server --appendonly yes
        - docker run --name homebot-staging -e REDIS_URL=$REDIS_URL -e HUBOT_SLACK_TOKEN=$HUBOT_SLACK_TOKEN --net=host -d homebot
        - docker rmi `docker images -f "dangling=true" -q` || echo ignore errors.
      only:
        - master
      tags:
        - docker-in-docker

    deploy to production:
      stage: deploy
      variables: 
        REDIS_URL: redis://127.0.0.1:6379/hubot
        HUBOT_SLACK_TOKEN: $HUBOT_SLACK_TOKEN_PRODUCTION
      script: 
        - docker stop "redis"    || echo ignore errors.
        - docker rm   "redis"    || echo ignore errors.
        - docker stop "homebot"  || echo ignore errors.
        - docker rm   "homebot"  || echo ignore errors.
        - docker run --name redis -d -p 6379:6379 -v /root/redis/data:/data redis redis-server --appendonly yes
        - docker run --name homebot -e REDIS_URL=$REDIS_URL -e HUBOT_SLACK_TOKEN=$HUBOT_SLACK_TOKEN --net=host -d homebot
        - docker rmi `docker images -f "dangling=true" -q` || echo ignore errors.
      only:
        - production
      tags:
        - docker-in-docker

変わった点は、 deployステージとして、「deploy to staging」を増やして、以下のように設定します。
* コンテナ名を「redis-staging」「homebot-staging」と本番とは別の名前にする
* Radisのポートを16379にする
* Radisの永続化先を本番とは別にする
* SlackにHubotの設定をもう1つ作って、上で説明した「Secret Variables」に「HUBOT_SLACK_TOKEN_STAGING」としてトークンを設定しておきます。HUBOT_SLACK_TOKENにはその値を設定します。

また、「deploy to staging」はmasterブランチでのみ、「deploy to production」はproductionブランチでのみ、ビルドされるように「only」を設定してしておきます。


## Pushしてみる

masterブランチにPushすると、deploy to stagingビルドが走り、productionブランチにPushすると、deploy to productionビルドが走ることがわかります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217768/8edc4ba1-515c-c051-305b-355c1aefccd2.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217768/47a287ac-76c1-f937-0a92-94631c9083e8.png)


dockerのコンテナを見てみると、homebotとhomebot-staging、radisとradis-stagingの2つずつ起動しています。

    # docker ps -a
    CONTAINER ID        IMAGE                  COMMAND                  CREATED             STATUS                     PORTS                     NAMES
    cbb3f1c3f3cd        homebot                "/bin/sh -c 'sh /app…"   4 minutes ago       Up 4 minutes                                         homebot-staging
    deb43da1e0bc        redis                  "docker-entrypoint.s…"   4 minutes ago       Up 4 minutes               0.0.0.0:16379->6379/tcp   redis-staging
    bc126c964ccc        homebot                "/bin/sh -c 'sh /app…"   4 minutes ago       Up 4 minutes                                         homebot
    51eea4d849c9        redis                  "docker-entrypoint.s…"   4 minutes ago       Up 4 minutes               0.0.0.0:6379->6379/tcp    redis
    897153d3393f        aea904bf7887           "gitlab-runner-cache…"   5 minutes ago       Exited (0) 5 minutes ago                             runner-ccce4edb-project-22-concurrent-0-cache-3c3f060a0374fc8bc39395164f415a70
    b6b8ff1edcf9        aea904bf7887           "gitlab-runner-cache…"   5 minutes ago       Exited (0) 5 minutes ago                             runner-ccce4edb-project-22-concurrent-0-cache-0a45d2bc3900e132e076ffa38608b42a
    2a32bac99e7a        gitlab/gitlab-runner   "/usr/bin/dumb-init …"   4 days ago          Up 5 minutes                                         gitlab-runner-docker

Slack上のHubotもどちらもちゃんと応答します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217768/7c860de7-ad71-c678-c2f2-7bbd884d3c0f.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217768/c6966ef8-ebfd-710e-dd3c-f8f34b37d20a.png)

## GitLab Environmentsを使ってみる

GitLabには、Environmentsという機能があり、デプロイ先を登録する機能があるようです。
せっかくなので使ってみます。

.gitlab-ci.ymlにenvironmentを追加します。
urlを書いておくと、GitLab上からリンクされるようです。
今回はHubot自体にフロントエンドがありませんので、Slack上のHubotへのダイレクトメッセージのページを指定しておきます。

.gitlab-ci.yml

    image: centos

    stages:
      - build
      - deploy

    build:
      stage: build
      script:
        - docker build -t homebot .
      tags:
        - docker-in-docker

    deploy to staging:
      stage: deploy
      variables: 
        REDIS_URL: redis://127.0.0.1:16379/hubot
        HUBOT_SLACK_TOKEN: $HUBOT_SLACK_TOKEN_STAGING
      script:
        - docker stop "redis-staging"    || echo ignore errors.
        - docker rm   "redis-staging"    || echo ignore errors.
        - docker stop "homebot-staging"  || echo ignore errors.
        - docker rm   "homebot-staging"  || echo ignore errors.
        - docker run --name redis-staging -d -p 16379:6379 -v /root/redis-staging/data:/data redis redis-server --appendonly yes
        - docker run --name homebot-staging -e REDIS_URL=$REDIS_URL -e HUBOT_SLACK_TOKEN=$HUBOT_SLACK_TOKEN --net=host -d homebot
        - docker rmi `docker images -f "dangling=true" -q` || echo ignore errors.
      only:
        - master
      environment:
        name: review
        url: https://banban55remocon.slack.com/messages/D9S1U9C7P/
      tags:
        - docker-in-docker

    deploy to production:
      stage: deploy
      variables: 
        REDIS_URL: redis://127.0.0.1:6379/hubot
        HUBOT_SLACK_TOKEN: $HUBOT_SLACK_TOKEN_PRODUCTION
      script: 
        - docker stop "redis"    || echo ignore errors.
        - docker rm   "redis"    || echo ignore errors.
        - docker stop "homebot"  || echo ignore errors.
        - docker rm   "homebot"  || echo ignore errors.
        - docker run --name redis -d -p 6379:6379 -v /root/redis/data:/data redis redis-server --appendonly yes
        - docker run --name homebot -e REDIS_URL=$REDIS_URL -e HUBOT_SLACK_TOKEN=$HUBOT_SLACK_TOKEN --net=host -d homebot
        - docker rmi `docker images -f "dangling=true" -q` || echo ignore errors.
      only:
        - production
      environment:
        name: production
        url: https://banban55remocon.slack.com/messages/D9K4Z5AKB/
      tags:
        - docker-in-docker

Git PushしてビルドするとGitLabリポジトリのEnvironmentでは、このように2つ並びます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217768/1c78cfb6-fcfb-515e-da9a-010531d75ac8.png)

Re-deployボタンで再ビルドが走りますし、隣のリンクボタンでSlackのページが開くようになりました。


## まとめ

* GitLab-CIを使って、Git PushするとBotをデプロイできるようになりました。
* GitLab-RunnerやBotはDocker上で動作し、必要に応じて数を変えられます
* ↑を利用して、本番サーバ―とステージングサーバーの2つを簡単に立てることができました