Insight Technology

2020.09.29

Gitlab CI/CD -基本編-

このエントリーをはてなブックマークに追加

こんばんは。インサイトテクノロジー札幌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が生成され、実行されます。
cicd_running.png .gitlab-ci.ymlは、YAML形式で記述するため、それぞれの設定項目を字下げとコロン、配列で表現します。

中身について簡単に触れます。 stages:では、pipelineにおけるjobの実行順序を制御します。 prepare-job,echo-jobはjobの定義で、pipeline上で実行したいscriptや、諸条件についてまとめています。 stagesimagesなど、yamlのトップレベルにおける一部の予約語以外は原則job名として解釈されます。

サンプルを実行する前に、CI Lintにかけ、実行可能なものか確認します。(CI Lintは公式ではUI上でしか確認できないので、ローカルで確認したいところ。。。非公式にはLint用docker imageがあるようですが。)

cilint.png

CI Lintがpassし、一般branchとdefault branch(変更していなければmaster)において実行されるjobと実行条件がValidateボタンの下に表示されます。

cicorrect.png

では、上記をrepositoryに配置し、実行してみましょう。 Gitlab CI/CDの実行ログは、GitlabのWebUI上で、各job単位で確認できます。

CI/CD -> jobs -> prepare preparesuccess.png

CI/CD -> jobs -> echo echosuccess.png

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 \    ' 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のホスティングや、ドキュメントの収集・管理についてお話します。

ページトップへ