こんばんは。インサイトテクノロジー札幌R&Dセンターの笹谷です。好きなハイランドはクライヌリッシュとグレンモーレンジィ、好きなスペイサイドはグレンエルギン、好きなアイラはブルックラディとキルホーマンです。
現在はGitlabを用いたCI/CDの構築、開発部門の業務プロセスの改善タスクを担当しております。
今回は、Gitlabが持つCI/CDの機能であるGitlab CI/CD
について、基本的な使い方を、実際に社内で利用している具体例(に近いもの)を用いてご説明します。
Gitlab CI/CDとは
- Gitlabレポジトリ内で.gitlab-ci.ymlで定義する、CI/CD(継続的インテグレーション/継続的デプロイ)の仕組み
- Gitlab CI/CDで実行する、一つのまとまった処理の単位をpipelineとし、pipelineはjobの集合からなる
- jobはstageによってカテゴライズ・順序付けができ、シーケンシャルにもパラレルにも実行できる
- Gitlab CI/CDの実行主体はRunnerといい、クラウド版の場合はGitlab.com上に存在する
- Runnerには複数の実行形態があり、Docker, kubernetes, VirtualBox, Parallels executorといった仮想環境を利用するものや、SSH、Shell executorといった、Runnerを導入したマシンでそのまま実行するものもある
- オンプレミス版としてGitlabServerをローカルに建て、リモートレポジトリとして扱うこともできる
かいつまんで言うと上記のようなサービスです。
シンプルなサンプル
Gitlab CI/CDの設定ファイルは以下のように記述します。
# .gitlab-ci.yml
stages:
- prepare
- echo
prepare-job:
stage: prepare
script:
- echo "Prepare before echo-job..."
echo-job:
stage: echo
script:
- echo "Ahoy! This is Gitlab CI/CD!"
ディレクトリ構成
Gitlab CI/CDをお試しするために、最低限必要なレポジトリのディレクトリ構成は以下です。シンプルですね。
CI/CDのPipelineのログを見るだけなので、README.mdすら不要です。
sample-repository/
└── .gitlab-ci.yml
簡単な説明
まず、設定ファイルとして、先に例示したような.gitlab-ci.yml
ファイルを用意します。
CIを実行したいGitlab repositoryのtop levelにpushすると以下のように、Gitlabの画面のCI/CD -> Pipelinesにおいて新規にpipelineが生成され、実行されます。
.gitlab-ci.yml
は、YAML形式で記述するため、それぞれの設定項目を字下げとコロン、配列で表現します。
中身について簡単に触れます。stages:
では、pipelineにおけるjobの実行順序を制御します。prepare-job
,echo-job
はjobの定義で、pipeline上で実行したいscriptや、諸条件についてまとめています。stages
やimages
など、yamlのトップレベルにおける一部の予約語以外は原則job名として解釈されます。
サンプルを実行する前に、CI Lintにかけ、実行可能なものか確認します。(CI Lintは公式ではUI上でしか確認できないので、ローカルで確認したいところ。。。非公式にはLint用docker imageがあるようですが。)
CI Lintがpassし、一般branchとdefault branch(変更していなければmaster)において実行されるjobと実行条件がValidateボタンの下に表示されます。
では、上記をrepositoryに配置し、実行してみましょう。Gitlab CI/CDの実行ログは、GitlabのWebUI上で、各job単位で確認できます。
CI/CD -> jobs -> prepare
CI/CD -> jobs -> echo
prepare-jobに続いて、echo-jobのscript:
のecho
出力が確認できます。それぞれのjobはrunner上で実行されます。
このサンプルをpushしただけのprojectでは、shared runnerがデフォルトで使用されます。
Gitlab runnerについて
Runnerには大別して2種類あります。
- shared runner
- Gitlab.comのリソースを共有する形でjobを実行するRunner
- 上記サンプルで利用しているのはこちら
- specific runner
- GitlabRunnerを導入した環境(物理・仮想マシン問わず)で、tokenを用いてregisterすることでGitlabと連携し、CI/CDが実行される
Gitlabのprojectで、Pipeline/Jobの実行環境を、ローカルサーバーやVM上で登録する場合、gitlab-runnerの導入と、registerの作業が必要になります。
実際の利用例
では、弊社で使用している.gitlab-ci.yml(*一部割愛)をもとに、上記の例を実務で役立つ形に変更するとしたらどのように定義できるのか、ご紹介します。
lintしてtestしてdeployするPipeline
image: docker:latest
services:
- docker:dind
variables:
DOCKER_DRIVER: overlay2
GIT_SUBMODULE_STRATEGY: recursive
DOCKER_TLS_CERTDIR: ""
stages:
- lint
- test
- deploy
.test_template: &test_job_definition
image: registry.gitlab.com/iti-sample/InsightLabo/sample_image:latest
tags:
- sample
services:
- name: mcr.microsoft.com/mssql/server:2017-latest-ubuntu
alias: mssql
variables:
ACCEPT_EULA: Y
SA_PASSWORD: ******************************
MSSQL_PID: Express
# deployの共通コマンドをエイリアス・アンカーでテンプレート利用
# デプロイ先等の各条件はそれぞれのdeployジョブのvariablesにて定義
.common_deploy_template: &deploy_job_definition
image: registry.gitlab.com/iti-sample/InsightLabo/sample_image:latest
stage: deploy
tags:
- sample
script:
- export APP_VERSION="${CI_COMMIT_SHORT_SHA}"
- >-
az login --service-principal
--user "${SERVICE_PRINCIPAL_USER}"
--password "${SERVICE_PRINCIPAL_PW}"
--tenant "${SERVICE_PRINCIPAL_TENANT}"
- >-
if [ `az functionapp list | grep -c "${SAMPLE_APP_NAME}"` -ne 0 ]; then
echo "# Function App has already exist. Skip creating and overwrite."
;else
az functionapp create
--resource-group "${RESOURCE_GROUP_NAME}"
--os-type Linux
--consumption-plan-location westeurope
--runtime python
--runtime-version 3.7
--name "${SAMPLE_APP_NAME}"
--storage-account "${STORAGE_ACCOUNT_NAME}"
--app-insights "${APP_INSIGHTS_NAME}"
;fi
- >-
if [ `az functionapp list | grep -c "${HTTPTRIGGER_APP_NAME}"` -ne 0 ]; then
echo "# Function App has already exist. Skip creating and overwrite."
;else
az functionapp create
--resource-group "${RESOURCE_GROUP_NAME}"
--os-type Linux
--consumption-plan-location westeurope
--runtime python
--runtime-version 3.7
--name "${HTTPTRIGGER_APP_NAME}"
--storage-account "${STORAGE_ACCOUNT_NAME}"
--app-insights "${APP_INSIGHTS_NAME}"
;fi
- cd ./execute_app && i=0 && until func azure functionapp publish "${SAMPLE_APP_NAME}"; do if [[ $i != 30 ]];then echo "Waiting for creating functionapp..." && sleep 10 && $(( i++ ));else break ;fi ;done
- cd ../request_app && i=0 && until func azure functionapp publish "${HTTPTRIGGER_APP_NAME}"; do if [[ $i != 30 ]];then echo "Waiting for creating functionapp..." && sleep 10 && $(( i++ ));else break ;fi ;done
- sleep 60
- >-
az functionapp config appsettings set
-g "${RESOURCE_GROUP_NAME}"
-n "${SAMPLE_APP_NAME}"
--settings MASTER_COMMIT_ID="${CI_COMMIT_SHORT_SHA}"
flake8:
image: python:3.6-alpine
stage: lint
tags:
- insight
script:
- pip install flake8
- flake8 ./
unittest-master-with-coverage:
<<: *test_job_definition
stage: test
script:
- pytest -v ./tests/ -m 'not production' --cov-report html:cov_html --cov-report term --cov=.
- sed -i -e '11a \ <script type="text/javascript" src="heatmap.js"></script>' cov_html/index.html
- mkdir coverage/
- cp -R cov_html/ coverage/
- mv coverage/cov_html coverage/coverage
- cp tests/coverage_utils/heatmap.js coverage/coverage
artifacts:
paths:
- coverage/
deploy-master:
<<: *deploy_job_definition
variables:
DOCKER_DRIVER: overlay2
SERVICE_PRINCIPAL_USER: ${INSIGHT_SP_USER}
SERVICE_PRINCIPAL_PW: ${INSIGHT_SP_PW}
SERVICE_PRINCIPAL_TENANT: ${INSIGHT_SP_TENANT}
RESOURCE_GROUP_NAME: dev-lab-sample-app
STORAGE_ACCOUNT_NAME: samplestorage
SAMPLE_APP_NAME: app-${CI_COMMIT_REF_NAME}-test
HTTPTRIGGER_APP_NAME: httptrigger-${CI_COMMIT_REF_NAME}-test
APP_INSIGHTS_NAME: app-${CI_COMMIT_REF_NAME}-test
acceptance-test-production:
<<: *test_job_definition
stage: test-after-deploy-production
script:
- export TEST_ENVIRONMENT=production
- pytest -v ./tests/test_app_on_production.py
上記の例では、CIが実行されたとき、*lint jobとしてPythonのflake8を実行*unittestを実行*deploy jobとして、レポジトリからAzureFunctionsへのpublish実行といった流れを記述しています。
各説の簡単な説明:
images:
冒頭のimages:
では、GitlabRunnerがDocker runnerなので、Docker runner内でさらに複数のdocker containerを使ってjobを実行するため、docker in dockerのimageを採用しています。
varibles:
varibles:
では、docker in docker環境で使用する環境変数をセットしています。主にdocker engine用ですね。
stages:
stages:
では、先述の通り、pipeline内でのjobのカテゴライズと、実行順序を制御しています。
.test_template:
.test_template:
ですが、こちらはテンプレートで、.
始まりのjobは実際には実行されないことを利用して、&test_job_definition
でエイリアスを定義し、後段のjob内で<<
を利用して共通の定義を読み込んでいます。こうすることで、jobの実行条件ごとに共通の処理を1つに束ねることができます。.common_deploy_template:
も、役割としては同じです。
flake8:
flake8:
は、stages:
で定義したlint jobの実体です。tags: insight
は、GitlabRunnerので紹介した、Specific runnerを利用する設定です。script:
でflake8
コマンドを実行します。
unittest-master-with-coverage:
unittest-master-with-coverage:
も、stages:
で定義したtest jobの実体で、先程の.test_template
の共通設定を利用し、jobを実行します。artifacts:
では、jobの処理結果をディレクトリ単位でjob artifactsとしてuploadしています。ここでは、pytestで算出したtest coverageをuploadし、HTMLのホスティング(gitlabメンバーのみ閲覧可能)のテストをしています。
deploy-master:
deploy-master:
でも、templateを共通設定として利用し、deploy-master
のjob固有のvariables
を使ってjobを実行します。なお、ここでのvariablesは${SOMEENVVAR}
といった形で定義していますが、Gitlab CI/CD上で利用可能な環境変数をGitlabのWebUI上で登録でき、こちらを利用する記述をしています。
acceptance-test-production:
acceptance-test-production:
では、上記のunittest-master-with-coverage:
と同じtest用templateを利用し、script:
の内容を切り替えて、deploy後の環境に対するacceptance test用スクリプトを実行するように定義しています。
備考
なお、本来はrules:if:
や only:
などで、どのbranchへのpushなのかや、各jobが成功したときだけ次のjobが動く、といったように、条件ごとにjobの実行を分岐していますが、ここでは割愛しています。
終わりに
ざっくりですが、実用例に近い形の解説は以上となります。上記例は説明用にシンプルにしたもので、運用上ノウハウが必要な部分などは割愛しています。(個人的にはもっと実践的ノウハウを知りたい。。。他社様の例とか見てみたい。。。)
次回は、Gitlab CI/CD実践編-GitlabPagesは便利-と題して、Pagesを用いたtest coverageのホスティングや、ドキュメントの収集・管理についてお話します。