開発中のDockerベースのサービスのCI環境をAWS上にGitlab CIで一から作ることになったのでせっかくなので手順や気にしたこと、そのうち何とかしたいことなどをまとめました。
開発中のサービスについて
だいたい以下のような構成のサービスの開発をしてます。そのため、構築するCI環境はDockerをbuildし、テストスクリプトを実行するといったものです。
- マイクロサービス
- 各サービスごとのリポジトリ(リポジトリが複数ある)
- リポジトリはgitlab.comで管理
- 各サービスはDocker(1つのサービスが複数コンテナの場合はある)
- テストはテストスクリプトから一括で実行できるようラップされている
※ テストコードの中身・内容については今回は述べません。
環境
- リポジトリ:gitlab.com
- CIツール:Gitlab CI + Gitlab Runner
- CI環境:EC2 t2.small * 2台
AWS EC2上に2台インスタンスを立ててそれぞれにGitlab Runnerを導入。これらで並列にCIを回せるようにしたいと思います。
今のところこのCI環境はあまり負荷がかからない想定ですが、開発が進むにつれリソース不足になると思われます。その時にインスタンスを増やすことで対応できる構成にしておきます。
ある程度必要なスペックに予測が立つなら大きめのを1個用意する方が良いのかも。また、いずれはスポットインスタンスで自動 or 半手動くらいで増減できるようにしたいです。
CIの全体像
今回作成したCI環境の全体像です。
以下の順番で動きます。
- リポジトリへのpushをトリガーとしてrunnerが動く
- リポジトリのdocker-compose.ymlをベースにbuild
- buildしたimage内のtest.shを実行してテストの合否判定
- slackに結果通知
図ではrunner、dood、tested serviceと3つのコンテナが書いてありますが、これはrunner自体がコンテナのためserviceのbuildのためにコンテナ上からdocker(or docker-compose)コマンドを実行できる必要があります。これを実現する方法は"Building Docker images with GitLab CI/CD"に
- Use shell executor
- Use docker-in-docker executor
- Use Docker socket binding
の三種類の方法が紹介されていますが今回は"Use Docker socket binding"の方法を採用しました。ちなみにこのコンテナ内部からホスト上へのコンテナ立ち上げや操作をする方法を"DOOD (Docker-Outside-Of-Docker)"と呼ぶらしいです。この方法ではdockerが入っているコンテナ(図のdoodコンテナ)にホストの/var/run/docker.sock
をマウントすることでコンテナ内からホスト側にdockerコマンドを実行できます。
※ "Use shell executor"を使わない理由
※ "Use docker-in-docker executor"を使わない理由
CI構築
今回は2台分構築しますが、1台構築後に複製します。
インスタンスに事前準備
事前準備として以下を行います。
- Dockerのインストール
- docker-composeのインストール
ここの手順は省略します。
gitlab.com側での確認
CIの対象のリポジトリで Settings > CI/CD に移動しRunnerに設定するTokenを確認します(下図参照)。また、今回は自前でrunnerを用意するので、Disable shared runner
でGitlab.comで用意されているShared Runnersを利用しないようにします。
各リポジトリでTokenを確認したら、以下の手順も各リポジトリに対して繰り返し実行します。
Gitlab Runnerのインストール(Docker)
コンテナでGitlab Runnerを動作させます。docker run
で立ち上げてもよいのですが、composeでymlを残して立ち上げるほうが好きなので、gitlab_runnerディレクトリを作ってdocker-compose.ymlを作成します。
- ディレクトリ構成
$HOME/
┗ gitlab_runner/
┗ docker-compose.yml
- docker-compose.yml
version: '3'
services:
runner:
image: gitlab/gitlab-runner
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./config:/etc/gitlab-runner
これを起動します。
$ cd $HOME/gitlab_runner/
$ docker-compose up -d
これで起動するとgitlab-runnerコマンドの実行がコンテナ内部からになります。
# 通常のRunnerコマンド
$ gitlab-runner [Runner command and options...]
↓
# composeを使ったいる場合のRunnerコマンド
$ docker-compose exec runner gitlab-runner [Runner command and options...]
Runnerの登録
gitlab-runner register
コマンドでgitlabにrunnerを登録します。
$ docker-compose exec runner gitlab-runner register -n \
--url "https://gitlab.com/" \
--registration-token "確認したトークン" \
--executor "docker" \
--description "Docker Runner" \
--docker-image "docker:stable" \
--tag-list docker \
--docker-volumes "/var/run/docker.sock:/var/run/docker.sock" \
--docker-volumes "/builds:/builds"
上記がDOODコンテナを立ち上げる設定になります。最後の--docker-volumes "/builds:/builds"
はbuildするコンテナでvolumeをマウントする場合に必要です。
また、--tag-list
は後で作成する.gitlab-ci.yml
(CIのパイプライン定義)で実行する runnerを特定するために使用します。
- 複製した場合
$HOME/gitlab_runner/config/config.toml
が作られていると思います。これを削除してgitlab-runner register
はやり直してください。
.gitlab-ci.yml
.gitlab-ci.yml
を作成してCIパイプラインの定義をします。このファイルはrunner側ではなくリポジトリのコードのトップに置かれるものです。今回CIを適用するリポジトリの構成は以下のようになっています。
- gitlab.com上のリポジトリの構成
/
┣ docker-compose.yml
┣ src/
┃ ┣ Dockerfile
┃ ┣ code/
┃ ┗ tests/
┃ ┣ testcode/
┃ ┗ test.sh
┗ .gitlab-ci.yml
このリポジトリ上のコードはdocker-compose build
でbuildできて、作成されたimageの内部にはsrc/
以下のファイルがコピーされ、docker-compose run <サービス名> sh ./tests/test.sh
を実行すればテストが走るように作ってあります。
そのため、.gitlab-ci.yml
は以下のようにbuildとtestの2ステージだけで、ジョブはそれぞれ1つです。また、before_scriptでdocker-composeをインストールしてます。大抵の場合はdockerコマンドで十分だと思うので不要かもしれません。
- .gitlab-ci.yml
image: docker:stable
before_script:
- apk add --no-cache py-pip
- pip install docker-compose
build:
stage: build
tags:
- docker
script:
- docker-compose build
test:
stage: test
tags:
- docker
script:
- docker-compose run <Service名> sh ./tests/test.sh
書けたら.gitlab-ci.yml
をリポジトリのトップにpushします。
CIのパイプラインはなるべくシンプルな形を維持したいです。ここをいじればだいぶ柔軟なテストができるようになりますが、複雑になると、Jenkinsおじさんを生み出しがちです。特に、今回はこの.gitlab-ci.yml
はなるべく全てのリポジトリで使いまわしたいと考えています。そのため、パイプラインはシンプルに、テストコードでカバーする。テストコードでカバーできない部分はテスト可能な形にコードの本体を修正するのが理想的かと思います。
ここまでで一通りCIとして動くようになりました。
Slackへの通知
基本はここの手順で設定できると思いますがこちらにも載せておきます。
- ここから通知先にしたいSlackのチャンネルを選んでIncoming WebHooksを作成します。
- リポジトリのSettings > Integrations > Slack notificationsに移動
- Activeにチェック
- Pipelineにチェックが入っていることを確認し、不要であれば他のチェックを外す
- Notify only broken pipelinesのチェックを外す(チェックがあると失敗時のみ通知)
CloudWatchで停止
利用しない時間はCI環境は停止しておきたいのでCloudWatchで毎日19:00にインスタンスを停止しようと思います。
ClowdWatch > ルール > ルールの作成 と進んで
- スケジュールを選択
- Cron式に
0 10 * * ? *
を入力(GMTなので注意) - ターゲットの追加
- "EC2 StopInstance API呼び出し"を選択
- インスタンス IDを入力
- 設定の詳細に進んでルールの名前と説明を入力
起動に関してはlambdaを使って自動化はできますが、使わない日もそこそこあるので一旦は実装しないことにしました。
その他、残課題等...
"Use shell executor"を使わない理由
本当は"Use shell executor"の方法で直接runnerからbuildできるほうが綺麗です。しかしこの場合
usermod -aG <host側のdocker group id> gitlab-runner
を実行して、gitlab-runnerの行う必要があり、少々手間でした。あまり、runnerコンテナ自体に手を入れたくなかったので、今回はDOODの手法を採用しました。
"Use docker-in-docker executor"を使わない理由
DOODと異なりdockerコンテナ内部でコンテナを親子構造に立ち上げるのがこの手法です。CIの最後に親コンテナごと消してしまえば綺麗になるのでこちらを採用するのもアリだと思いますが、コンテナのbuild時間がimageのcacheが効かなく長くなるようなので今回は採用しませんでした。ただし、この問題の回避方法も"Building Docker images with GitLab CI/CD"内に紹介されているので将来的には検討してもよいかと思います。
差分だけのテストが必要か?
今のCIとテストコードの仕様ではpushするたびに全部のテストが走ってしまいます。各サービスが小さい間は良いですが、大きくなるにつれテスト時間の増大が懸念されます。そのため、
- サービスは小さく維持する(大きくなるようなら分離を考える)
- 変更ファイルに応じてテストが実行されるようにする
のどちらかを考える必要があると思っています。今のところ、サービスを小さく維持する方針なのですが、検討しておく必要があると思っています。
以前はgit diffやcommit logから抽出して実現していたことがあるが、あまり綺麗な感じではなかったのでよい方法を探しています。
GitLab Container Registry
今回はbuild → testで終わってますが、RegistryへのpushまでCI側に含めてCDのパイプラインに組み込みたい。