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

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

全体像

全体像.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

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

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

横並び.png

ちょっとだけ余談

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

Smart Home - Scrapbox

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

前提条件

  • 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を控えておきます。

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

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

image.png

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

image.png

デプロイの準備

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

image.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

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

この環境変数を使用するように.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

せっかく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

image.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

image.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

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

まとめ

  • GitLab-CIを使って、Git PushするとBotをデプロイできるようになりました。
  • GitLab-RunnerやBotはDocker上で動作し、必要に応じて数を変えられます
  • ↑を利用して、本番サーバ―とステージングサーバーの2つを簡単に立てることができました
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.