Hubotというチャットbot開発・実行フレームワークがあります。
Node.jsで動作するのでHerokuとか適当なサーバにデプロイすればよいのですが、
せっかく勉強しているのでDockerにデプロイしようと思います。
また、botのソースコードはGitLabで管理しているので
GitLabにPushすると、Dockerにデプロイすることを目指します。
全体像
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階建てです。
ただ、Build VMはビルドが終わると落ちてしまうので、ずっと稼働し続ける予定の
homebotとRedisをここに配置するわけにはいきません。
(もしかして何か別の方法があるのでしょうか?)
Docker in Dockerの1種として、Dockerコンテナ上からHostのDocker機能を呼び出せる仕組みを使って、
Docker上のBuild VMからホストのDockerにhomebotとRadisをデプロイします。
ついでにBuild VMもホストのDocker上で動かすことにして、実際には以下のように横に並びます。
ちょっとだけ余談
ここで作っているBotは、スマートスピーカーであるGoogle Homeを使って照明やエアコンを操作するものです。
ベース部分はこちらのサイトを参考にしています。
この記事には直接関係ありませんが、こういったことに興味がある方は是非ご覧ください。
前提条件
- 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の起動と登録
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のパスではなく、ホストのパスになる点です。
- docker-image
GitLabを開いて、runnerが登録されていることを確認します。
最後に、先ほど登録したrunnerのEditを開き、GitLabのリポジトリに関連付けます。
これは設定によっては不要なようですが、安全のために手動で設定するようにしています。
デプロイの準備
BotをSlackと連携させるにはSlack上にHubotアプリを追加し、Bot側にはAPIトークンを渡す必要があります。
SlackのHubotページを開いて、HUBOT_SLACK_TOKENを控えておきます。
ビルドとデプロイのスクリプト作成
上で作った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してみます。
GitLabリポジトリのPipelinesを開くと、ビルドされていることがわかります。
HUBOT_SLACK_TOKENをリポジトリから排除する
HUBOT_SLACK_TOKENをリポジトリにコミットしてしまうと消すことができません。
考えすぎかもしれませんが、将来的にリポジトリに参加するデベロッパーが増えたときに
誰でもSlack上のBotを触れる状態は良くないかもしれません。
GitLabには「Secret Variables」という機能があり、こういった非公開パラメータを管理する仕組みがあるので使ってみます。
「Secret Variables」は、リポジトリのページ→Settings→CI/CD Pipelinesにあります。
ここにHUBOT_SLACK_TOKEN_PRODUCTIONとして定義します。
ここに定義した変数は環境変数としてRunner上から使用できます。
この環境変数を使用するように.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するだけでいきなり本番サーバーにデプロイするのは、壊してしまいそうで少し怖いです。
一旦、ステージングサーバーにデプロイして動作を確認した後、本番サーバーにデプロイできれば、気分的に楽ですね。
せっかく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ビルドが走ることがわかります。
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もどちらもちゃんと応答します。
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つ並びます。
Re-deployボタンで再ビルドが走りますし、隣のリンクボタンでSlackのページが開くようになりました。
まとめ
- GitLab-CIを使って、Git PushするとBotをデプロイできるようになりました。
- GitLab-RunnerやBotはDocker上で動作し、必要に応じて数を変えられます
- ↑を利用して、本番サーバ―とステージングサーバーの2つを簡単に立てることができました