この記事は ユニークビジョン株式会社 Advent Calendar 2018 の 14 日目の記事です。
GitLab CI 、良いと思います
会社では社内で余っていた MacBook Air を Runner として活用して GitLab CI / CD しています。その際に行った設定について書きます。
GitLab CI はドキュメントも充実しており、yaml ファイルでの設定もシンプルで CI サービスとして基本的な機能は揃っていそうに見えます。オンプレで CI を回したい場合、一考に値するサービスなのではないかと思います。
環境
- GitLab CE 10.1.0
Runner 環境
- MacBook Air (13-inch, Early 2015) 2台。メモリのみ盛られており 8GB。
- macOS 10.14
- Docker for Mac Version 2.0.0.0-mac81 (29211)
- gitlab-runner 11.4.2 (cf91d5e1)
config.toml
本家ドキュメントの通りに Runner を登録すると、~/.gitlab-runner/config.toml
が生成されているのでそれを編集して Runner の設定を行います。現在以下の設定で運用しています。
concurrent = 4
check_interval = 0
[session_server]
session_timeout = 1800
[[runners]]
name = "MacBook Air (sticker: XXX)"
url = "https://ourgitlab.example.com/"
token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
executor = "docker"
builds_dir = "/Users/ouruser/Work/builds"
[runners.docker]
pull_policy = "if-not-present"
tls_verify = false
image = "docker:latest"
privileged = false
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock:rw", "/Users/ouruser/Work/builds:/Users/ouruser/Work/builds:rw"]
shm_size = 0
[runners.cache]
[runners.cache.s3]
[runners.cache.gcs]
dind
CI パイプライン上での処理がホスト PC を汚さないよう、executor
には docker を指定しました。CI パイプラインでのテストには docker-compose
コマンドを使用するので、Docker in Docker (dind) となります。dind には2つの方法があるようですが、/var/run/docker.sock
を executor にマウントすることで、 macOS 上の Docker daemon を共有する方法をとりました。
Job 実行時にファイルが見つからない問題
docker-compose run ...
でテストを行ったところ、CI 実行時に git fetch
したはずのファイルが見つからず、no such file or directory.
を吐いてテストの実行に失敗する現象に困りました。この現象は volumes
でビルドに使うディレクトリ(builds_dir
で指定)と同じフォルダをホスト PC に作成し、 executor 上の同一のパスにマウントすることで回避できました。dind でホスト PC から見ると並列に並んでいるコンテナ同士で親子関係を持つような構造になっており、孫にあたる Rails アプリのコンテナがホスト PC のファイルを参照しようとして親にあたる executor のコンテナのファイルを参照したことで起こったエラーだと思っています。
並列数
concurrent
は 2~8 まで試しましたが 4 に落ち着きました。Docker for Mac の設定では Docker Engine のリソースを 4 CPU と 4GB のメモリに制限しています。
CI パイプラインの設定
git リポジトリのルートディレクトリに .gitlab-ci.yml
を作成することで CI パイプラインの設定を行うことができます。
runner 用に Docker イメージを用意しており、docker-compose
や kubectl
等 Job の実行に必要なコマンドを入れています。それを使用することで Job ごとに apk add
等が走らないので、 Job にかかる時間が短縮できます。
docker-compose
コマンドを実行した際に、CI の Job ごとに別々の Docker ネットワークでそれぞれのコンテナ群を動かすために COMPOSE_PROJECT_NAME
が Job 毎に異なるように環境変数を設定します。また環境変数に COMPOSE_FILE
を指定することで CI 用の docker-compose ファイルを毎度指定する必要をなくせます。
image: ourproject-gitlab-runner:1.0.1
variables:
COMPOSE_FILE: docker-compose.ci.yml
COMPOSE_PROJECT_NAME: project$CI_JOB_ID
CD を導入する
CD は以下のような Job を作って実施しています。初めて CD の設定をしたので拙いと思いますが説明します。とりあえず動いています。
script
を何行にも渡って書いていますが、いずれ他のスクリプト等に切り出したいです。
prepare_env_for_deploy:
stage: prepare_deploy
only:
- /^\d+\.\d+\.\d+(\.\d+)?-yet$/
artifacts:
paths:
- $DEPLOY_ENV_FILE
script:
- |-
NEW_VERSION=${CI_COMMIT_TAG%-yet}
PREV_TAGGED_COMMIT_SHA=$(make get-previous-staging-deployed-commit-sha PREVIOUS_STAGING_DEPLOYED_VER=$(make get-previous-staging-deployed-ver))
WEB_CHANGED_FILE_COUNT=$(git diff $PREV_TAGGED_COMMIT_SHA --name-only apps/web | wc -l)
PREV_WEB_IMAGE_VERSION=$(make --no-print-directory -C infra get-previous-web-image-version)
if [ $WEB_CHANGED_FILE_COUNT -gt 0 ]; then
WEB_IMAGE_VERSION=$(echo $PREV_WEB_IMAGE_VERSION | $INCREMENT_VER_CMD)
else
WEB_IMAGE_VERSION=$PREV_WEB_IMAGE_VERSION
fi
echo "export NEW_VERSION=$NEW_VERSION" > $DEPLOY_ENV_FILE
echo "export WEB_CHANGED_FILE_COUNT=$WEB_CHANGED_FILE_COUNT" >> $DEPLOY_ENV_FILE
echo "export WEB_IMAGE_VERSION=$WEB_IMAGE_VERSION" >> $DEPLOY_ENV_FILE
cat $DEPLOY_ENV_FILE
deploy_staging:
stage: deploy_staging
tags:
- wired-network
dependencies:
- prepare_env_for_deploy
script: # Upload docker images -> Deploy CloudFormation -> Apply K8s manifests -> Run SQLs
- |-
source $DEPLOY_ENV_FILE
export ENV=staging
$DOCKER_COMPOSE_BUILD_WEB_CMD
$DOCKER_COMPOSE_BUILD_ALL_CMD
if [ $WEB_CHANGED_FILE_COUNT -gt 0 ]; then
make -C infra upload-webassets-and-image
fi
make -C infra cfn-deploy kubectl-apply
make -C infra run-sql DB_HOST=$DB_HOST_STAGING
after_script:
- docker system prune -f --volumes
git_tag:
stage: git_tag
dependencies:
- prepare_env_for_deploy
only:
- /^\d+\.\d+\.\d+(\.\d+)?-yet$/
script:
- |-
source $DEPLOY_ENV_FILE
git config user.name "GitLab CI"
git config user.email "gitlabci@example.co.jp"
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -p $GITLAB_SSH_PORT $GITLAB_HOST >> ~/.ssh/known_hosts
git tag $NEW_VERSION
git push ssh://git@$GITLAB_HOST:$GITLAB_SSH_PORT/$CI_PROJECT_PATH.git --tags
deploy_production:
stage: deploy_production
only:
- /^\d+\.\d+\.\d+(\.\d+)?-yet$/
when: manual
dependencies:
- prepare_env_for_deploy
...
デプロイのタイミング
master ブランチが更新される度にステージング環境へのデプロイを行いたいところですが、そんなに並列実行数に余裕が無いので 0.0.0.1-yet
のようなフォーマットのタグがついたコミットについてのみステージング環境にデプロイするようにしています。このタグは各エンジニアが簡単なスクリプトを叩くことで push されます。-yet
は未だデプロイしていないことを現す気分でいます。ステージング環境へのデプロイに成功した後にバージョン番号のみのタグを push します。
本番環境へのデプロイは手動で Job を動かすことで実行します。
デプロイに必要な変数を保存しておく
prepare_deploy
という stage
でデプロイに必要な変数を artifacts
に保存しておき、デプロイの Job の dependencies
でそれを読み込むようにしています。こうすることでステージング環境と本番環境へのデプロイ時に同じ変数を参照することができます。
デプロイ
前回のデプロイから差異があった場合にのみ新しい Docker イメージを作成して push するようにします。
有線 LAN 接続をしている runner には wired-network
タグを付与しており、デプロイはそのタグがついた runner からのみ行っています。
溜まっていく古い Docker リソースを消す
ホスト PC のストレージが枯渇しないように docker system prune -f --volumes
を実行します。これはストレージが枯渇しない程度の頻度で行えばよいと思っており、適当にステージング環境へのデプロイが終わった後に実行しています。
これから
来年はパイプラインをもう少しスマートにして、Slack から CD をトリガするなど ChatOps を導入していけたら良いと思っています。
明日は @nariTota さんです。