[Gitlab RunnerとArgo CD使用]GitOpsスタイルなCI/CDパイプラインを構築したのでふりかえる

23 min read読了の目安(約20700字

0. はじめに

こんにちは。都内でエンジニアをしている、@gkzvoiceです。Gitlab RunnerとArgo CDを使って、GitOpsの考え方を取り入れたCI/CDパイプラインを構築したのでそのふりかえりをします。構築する当初、そもそもなぜGitOpsという考え方が台頭したのか?について分からず、調べたので、併せてお話しできればと思います。目次は以下のとおりです。

0. はじめに
  0-1. 前提
  0-2. 2つのCDの個人的なメモ

1. GitOpsとはなにか?
  1-1. GitOpsの個人的なメモ

2. そもそもなぜGitOpsという考え方が台頭したのか?
  2-1. CIOpsが抱えていた課題とはなにか?

3. サンプルのGitOpsスタイルなCI/CDパイプラインの構成図とその技術構成
  3-1. サンプルのCI/CDパイプラインの技術構成
  3-2. サンプルのCI/CDパイプラインでおこなうこと

4. サンプルのCI/CDパイプラインを構築するにあたり工夫した2つのこと
  4-1. manifestのイメージタグにアプリケーションリポジトリのcommit hashを渡す方法
  4-2. なぜmanifestに書くイメージタグはcommit hashとしたのか?
  
5. .gitlab-ci.yml解説
 5-1. Runnerが最初に読み込む.gitlab-ci.ymlの解説
 5-2. アプリケーションコンテナのイメージをbuildするところからGCRにpushするところまでの解説
 5-3. manifestのイメージタグを更新し、manifestリポジトリへpushするところまでの解説

6. トラブルシュート

7. おわりに

8. 参考
  • CI/CDパイプラインが完走した様子(画面左)ととmanifestのイメージタグを書き換えている様子(画面右)

  • Runnerが作成したMRからマージすると(画面左)、Argo CDが同期(sync)してくれる(画面右)

0-1. 前提

  • ここで扱うCI/CDパイプラインがデプロイするアプリケーションは以下のとおり簡素なものとします。そのため、パイプラインは実運用には足らないところがあるかもしれません。
  • また、ここでの CDとは「 継続的デリバリー(Continuous Delivery)」 を指します。「 継続的デプロイメント(Continuous Deployment)」ではありません。両者の違いについてはRed Hat社の記事が分かりやすかったので、ご紹介させてください。なお、僕個人の考えは「0-2. 2つのCDの個人的なメモ」に書きました。
    • 継続的デリバリーは一般に、開発チームによるアプリケーションへの変更に対してバグがないか自動でテストを行い、変更をリポジトリ (GitHub やコンテナレジストリなど) にアップロードします。 ここで、変更が運用チームによって本番環境に導入されます。

    • 継続的デプロイでは、新しいソフトウェアのリリースプロセスを通じてさらにいくつかのステップをカバーします。これには通常、開発者による変更をリポジトリから本番環境に自動的にリリースし、顧客が使用できるようにするプロセスが含まれます。運用チームが担当する手動プロセスが多すぎて、アプリケーションの提供が遅れるという問題に対処します。

    • 参考. 継続的デリバリーとは
  • 構成図はdraw.ioにて作成しました。アイコンはこちらから拝借しました。

0-2. 2つのCDの個人的なメモ

  • 「継続的デリバリー(Continuous Delivery)」 では商用環境へのデプロイの手前までパイプラインでおこない、同環境へのデプロイは手動でおこなう
  • 一方、 「 継続的デプロイメント(Continuous Deployment)」 では、商用環境へのデプロイをも自動化するということ。
    • つまり、「 継続的デプロイメント(Continuous Deployment)」では、開発者のpushに始まるパイプラインが完走した場合、商用環境までpushされた結果が反映される ことになる。

1. GitOpsとはなにか?

GitOpsとは、Weaveworks社が提唱した、CDの方法のひとつです。同社のブログでは、GitOpsについて以下のように書かれています。(記事をGoogle翻訳に書けた結果を引用する。2020/04/04現在。)

  • GitOpsは、Kubernetesクラスター管理とアプリケーション配信を行う方法です。これは、宣言型インフラストラクチャとアプリケーションの信頼できる唯一の情報源としてGitを使用する ことで機能します。

  • 参考. Guide To GitOps

1-1. GitOpsの個人的なメモ

  • GitOpsとは、manifestの更新をデプロイ環境に引き込むデリバリー方法 のこと
    • この「デプロイ環境への引き込み」はCIツールではなく、Argo CDをはじめとするOperator と呼ばれるツールがおこなう
    • そのデリバリー方法の特徴から「Pull Model」と評されることもある
  • GitOpsにおいて重要な考え方は、"a single source of truth"
    • アプリケーションをデプロイする際に使うmanifestをGitのバージョン管理下におく
    • ロールバックも容易なものとなるように目指す
  • GitOpsを最大限に生かすには、 アプリケーションコードとmanifest(インフラストラクチャコード)を2つのリポジトリに分けること が必要ではないか?
    • アプリケーションコードとmanifest(インフラストラクチャコード)を2つのリポジトリに分けることで、CIを頻繁に実行してもCDまで実行されるかどうかはパイプラインの設定に「カンタンに」委ねる ことが出来る
    • これがよくいわれる CIとCDを分離する ということだと思う
    • もちろんアプリケーションとmanifestを別々のリポジトリに分けなくても、CIのみ実行しCDは実行しないという制御はできるはずです。しかし、別々にリポジトリを分ける場合に比べて、パイプラインの制御は難しくなるでしょう。設定ファイルの見通しは悪くなり、保守も行き届かなくなるかもしれません。そこで頻繁にソースコードに変更が加わるアプリケーションをCDから切り離すことでパイプラインの健全性を維持することを目指すべきなのだと考えました。

2. そもそもなぜGitOpsという考え方が台頭したのか?

続いて、GitOpsという考え方が台頭したのか?について考えましょう。この問いは次のように言い換えることができます。

GitOpsが台頭する以前のデリバリー方式が抱えていた課題はなんだったのか?

ここでは、GitOpsが台頭する以前のデリバリー方式をCIOpsとして話を進めます。CIOpsのツールは、たとえば、JenkinsやTravis CIが挙げられます。

2-1. CIOpsが抱えていた課題とはなにか?

端的に言えば、CIとCDが密結合の関係 となってしまっているという点でしょう。

これの何が面倒かというとたとえば、以下のような点があげられるのではないでしょうか。

  • デプロイ環境へのアクセスやデプロイ方法が変われば、CD構築者は CIOpsのツールの設定内容を更新しなければならない
  • CDを進める際に使うツールは「CI」でも使われているものが兼務する場合、CD構築者は CIOpsのツールの権限を拡大せざるを得ない
    • CIOpsのツールはインテグレーションのみならずデリバリーの責務も追うことになる
    • たとえば、CIで使うコンテナレジストリーのみならず、CDで使うクラスターへのアクセス権限もCIOpsのツールに付与されるなど
  • CDの実行状況はGitの管理下から外れ、CI/CDパイプラインの実行状況を追うことが煩わしい
    • CDの実行状況はCDを担うCIOpsのツールが提供するログを追うことができるけど。。

※「Push Model」とはCIOpsのデリバリー方法の特徴を表現するもの。

さて、これまでGitOpsの概要とそれが台頭してきた背景=CIOpsが抱えていた課題についてお話してきました。

それではいよいよ、GitOpsスタイルなCI/CDパイプラインとはどんなものか。サンプルの構成図を使ってお話しましょう。


3. サンプルのGitOpsスタイルなCI/CDパイプラインの構成図とその技術構成

3-1. サンプルのCI/CDパイプラインの技術構成

3-2. サンプルのCI/CDパイプラインでおこなうこと

CI=Gitlab Runnerがおこなうこと

  • buildしたイメージを使ってcoverageの測定
  • coverageの結果がしかるべき場合のみ、GCRへイメージをpush
  • manifestのイメージタグの書き換え及びmanifestリポジトリへpush

CD=Argo CDがおこなうこと

  • manifestリポジトリの任意のブランチに更新が走った場合、その更新をデプロイ環境に引き込むようにする
    • ここでは、${BRANCHNAME}というブランチをPR/MR先としてマージされたら、Argo CDがmanifestリポジトリからmanifestへ自動で同期するようにしている。
    • なお、--dest-server https://kubernetes.default.svc はデプロイするアプリケーションをArgo CDと同じクラスター内とするというオプションである
$ argocd app create hello-python \
--repo git@gitlab.com::${ACCOUNTNAME}/${MANIFEST_ROOT_DIR}.git  \
--path manifests \
--dest-server https://kubernetes.default.svc \
--dest-namespace ${APPNAMESPACE} \
--revision ${BRANCHNAME} \
--sync-policy automated --auto-prune --self-heal

※アプリケーションがあるクラスターはArgo CDからするとクラスタの外にある場合の書き方はMulticluster GitOps with ArgoCDによるとこのようなものらしい(筆者、未検証。)

$ argocd cluster add $CLUSTER_NAME

# これをおこなったあと、デプロイしたいクラスターのMASTER_IPをdest_serverに渡す
$ argocd app create hello-python \
--repo git@gitlab.com::${ACCOUNTNAME}/${MANIFEST_ROOT_DIR}.git  \
--path manifests \
--dest-server $MASTER_IP \
--dest-namespace ${APPNAMESPACE} \
--revision ${BRANCHNAME} \
--sync-policy automated --auto-prune --self-heal

参考. Multicluster GitOps with ArgoCD

4. サンプルのCI/CDパイプラインを構築するにあたり工夫したこと

ところで、リポジトリをアプリケーションとmanifestの2つに分けるということは、GitOpsの重要な考え方である、"a single source of truth" に反しているとはならないのでしょうか?

リポジトリをアプリケーションリポジトリとmanifestリポジトリの2つに分けることになっても、「a single source of truth」をどうやって守るか?その鍵を握るのが工夫したこととして挙げる、manifestに書くイメージタグはcommit hashを採用 することです。

4-1. manifestのイメージタグにアプリケーションリポジトリのcommit hashを渡す方法

  • manifestで使うアプリケーションコンテナのイメージタグをdocker buildする際にタグの値を git rev-parse --short HEAD で渡す
    • dockerコマンドはオプションが似たようなものが多く、可読性が低いことからdockerコマンドはMakefileに寄せ、Runnerはmakeコマンド経由でイメージタグを付与した上でdocker buildするようにしている
  • manifestのイメージタグの書き換えはRunnerがyqコマンドで書き換える
- >
  docker run --rm -v "$PWD:$PWD" -w="$PWD"
  --entrypoint yq linuxserver/yq
  -ry '.spec.template.spec.containers[0].image
  |="gcr.io/'${PROJECT_ID}'/'${CONTAINER_NAME}':'{COMMIT_HASH}'"'
  deployment.yml.tmpl > deployment.yml

4-2. なぜmanifestに書くイメージタグはcommit hashとしたのか?

当初はタイムスタンプを採用していましたが、やめました。理由は、以下の2点です。

  • 仮にRunnerが並列で実行された場合のパイプラインの実行順序とタグが生成される順序がイコールであるとは保証できないため
  • 一般的にタグの命名規則が推測されることはあまりよろしくないと考えたため

タグの命名規則について考えるにあたり、以下の2点は学びがありました。

とりわけ前者の書籍では複数のサーバー間で採番する難しさとして、「「Chapter 7: Design A Unique Id Generator In Distributed Systems」でこのように書かれていたことが印象的です。したがって、タグの名目規則にv1、v2といった数字がインクリメントされる方式も見送ることにしました。

auto_inrements(DBが提供する自動採番)の課題

  • Hard to scale up with multiple data centers(Only writer is responsible for generating IDs)
  • IDs do not go up with time across multiple svs
  • It does not scale well when a sv is added/removed.

他にも学びがあったので、zennのスクラップにメモを残しました。こちらもぜひ。

5. .gitlab-ci.yml解説

5-1. Runnerが最初に読み込む.gitlab-ci.ymlの解説

  • 一部抜粋
  • .gitlab-ci.ymlはincludeとextendsを使って、.gitlab-ci.ymlがincludeに書かれたymlを読み込むようにしている(参考5-1-1及び5-1-2)
include:
  - .gitlab-ci.d/.build_dev.yml
  - .gitlab-ci.d/.deploy_dev.yml

stages:
  - build
  - deploy

pages:  ## 5-2
  stage: build
  extends: .build_dev

deploy_dev: ## 5-3
  stage: deploy
  extends: .deploy_dev

5-2. アプリケーションコンテナのイメージをbuildするところからGCRにpushするところまでの解説

.build_dev:
  image: google/cloud-sdk:330.0.0-slim
### cloud-sdkコンテナのなかでアプリケーションコンテナをbuildしていく「Docker in Docker」に必要な設定
  services:
    - docker:19.03.13-dind
#    - docker:20-dind
#    - docker:20.10-dind
#    - docker:dind   ## 「6. トラブルシュート」に記載
#### 参考5-2-1
  variables:
    DOCKER_DRIVER: overlay2
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_CERTDIR: "/certs"
    DOCKER_TLS_VERIFY: 1
    DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
  before_script:
    - PROJECT_ID=$(gcloud config list project --format="value(core.project)")
#### gcloudコマンドをサービスアカウント権限で使うようにする
    - echo $SERVICE_ACCOUNT_KEY > ${HOME}/gcloud-service-key.json
#### 参考5-2-2
    - gcloud auth activate-service-account --key-file ${HOME}/gcloud-service-key.json
    - gcloud config set project $PROJECT_ID
#### 参考5-2-3
    - gcloud auth configure-docker
    - COVERAGE=0
  script:
    - make build
    - make run
#### 参考5-2-4
    - make pytest
    - COVERAGE=$(grep -E '<span class="pc_cov">([0-9]{1,3})%</span>' app/htmlcov/index.html | grep -o '[[:digit:]]*')
    - mv app/htmlcov/ public
    - if [ $COVERAGE -lt 80 ]; then exit 1 ;fi
    - make push
#### 参考5-2-4, 5-2-5
#### coverageレポートはGitlab Pagesで見るようにした
  artifacts:
    paths:
#      - app/htmlcov
      - public
CONTAINER_NAME := hello-python
#### 参考5-2-6
PROJECT_ID:= $(shell gcloud config list project --format="value(core.project)")
COMMIT_HASH := $(shell git rev-parse --short HEAD)

.PHONY: build
build: ## make build
        docker build -f Dockerfile -t gcr.io/${PROJECT_ID}/${CONTAINER_NAME}:${COMMIT_HASH} .

.PHONY: run
run: ## make run
        docker run -d -p 80:80 --name ${CONTAINER_NAME}-${COMMIT_HASH} -v ${PWD}/app:/app gcr.io/${PROJECT_ID}/${CONTAINER_NAME}:${COMMIT_HASH}

.PHONY: push
push: ## make push
        docker push gcr.io/${PROJECT_ID}/${CONTAINER_NAME}:${COMMIT_HASH}

#### 参考5-2-4, 5-2-5
.PHONY: pytest
pytest: ## make pytest
	docker exec ${CONTAINER_NAME}-${COMMIT_HASH} /bin/bash -c 'pytest -v --cov=tests --cov-report=html'

5-3. manifestのイメージタグを更新し、manifestリポジトリへpushするところまでの解説

.deploy_dev:
  image: google/cloud-sdk:330.0.0-slim
  services:
    - docker:19.03.13-dind
  variables:
    DOCKER_DRIVER: overlay2
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_CERTDIR: "/certs"
    DOCKER_TLS_VERIFY: 1
    DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
  before_script:
    - PROJECT_ID=$(gcloud config list project --format="value(core.project)")
    - COMMIT_HASH=$(git rev-parse --short HEAD)
    - PROJECT_OWNER=gkzz
    - MANIFEST_ROOT_DIR=gke-quickstart-manifest
    - CONTAINER_NAME=hello-python
    - TARGET_BRANCH=demo
  script:
### Runnerがmanifestリポジトリをclone/MRするために必要な鍵認証の設定をする
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - git credential-cache exit
    - ssh-keyscan -H "$CI_SERVER_HOST" >> ~/.ssh/known_hosts
    - 'which ssh-agent || ( apk add --update openssh )'
    - eval "$(ssh-agent -s)"
    - echo "$SSH_PRIVATE_KEY" | ssh-add - > /dev/null
### manifestリポジトリをcloneし、MR元のブランチへcheckout
    - git config --global user.name "dummy@example.com"
    - git config --global user.email "dummy@example.com"
    - git clone git@${CI_SERVER_HOST}:${PROJECT_OWNER}/${MANIFEST_ROOT_DIR}.git && cd ${MANIFEST_ROOT_DIR}/manifests
    - git remote set-url --push origin git@${CI_SERVER_HOST}:${PROJECT_OWNER}/${MANIFEST_ROOT_DIR}.git
    - git checkout -b argocd/${COMMIT_HASH}
#### yqのイメージを使ってmanifestのイメージタグを${COMMIT_HASH}(commit hash)の値に書き換える
#### 参考5-3-1
    - >
      docker run --rm -v "$PWD:$PWD" -w="$PWD"
      --entrypoint yq linuxserver/yq
      -ry '.spec.template.spec.containers[0].image
      |="gcr.io/'${PROJECT_ID}'/'${CONTAINER_NAME}':'${COMMIT_HASH}'"'
      deployment.yml.tmpl > deployment.yml
 #### MR
 #### 参考5-3-2
    - git add deployment.yml
    - 'git commit -m "RUNNER: ${COMMIT_HASH}" deployment.yml'
    - >
      git push
      -o merge_request.create
      -o merge_request.title="Runner: ${COMMIT_HASH}"
      -o merge_request.target=${TARGET_BRANCH}
      origin argocd/${COMMIT_HASH}

6. トラブルシュート

  • Docker in Dockerの構成でRunnerを動かす際、servicesで指定するDockerのバージョンを19.0.13と指定する必要があった
  services:
    - docker:19.03.13-dind
## 以下、3通りの指定では、エラーを引いた
#    - docker:20-dind
#    - docker:20.10-dind
#    - docker:dind
  variables:
    DOCKER_DRIVER: overlay2
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_CERTDIR: "/certs"
    DOCKER_TLS_VERIFY: 1
    DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
  • ジョブのログからエラーメッセージの箇所を抜粋
Step 10/37 : ENV HOME_DIR $HOME_DIR
 ---> Running in 521c0b3d7a75
Removing intermediate container 521c0b3d7a75
 ---> 13301e24b7de
Step 11/37 : RUN echo $HOME_DIR
 ---> Running in 9516feda59bc
io.containerd.runc.v2: failed to adjust OOM score for shim: set shim OOM score: write /proc/300/oom_score_adj: invalid argument
: exit status 1: unknown
runners:
  config: |
    [[runners]]
      [runners.kubernetes]
        image = "ubuntu:20.04"
        privileged = true
      [[runners.kubernetes.volumes.empty_dir]]
        name = "docker-certs"
        mount_path = "/certs/client"
        medium = "Memory"

7. おわりに

さいごに、Gitlab RunnerとArgo CD使用]GitOpsスタイルなCI/CDパイプラインを構築して学んだこと、今後も考えなければならないこと、そしてCI/CDパイプラインを設計する際に参考にした資料を列挙します。

学び

  • アプリケーションのソースコードとKubernetesのmanifest(インフラ)を分離することで、CIツール=Gitlab Runnerにデプロイ環境へのアクセス権限を渡す必要がなくなった。
  • manifestをGitで管理することで、かつCDはArgo CDを通してGitでおこなうことで、デプロイ環境をGitでバージョン管理することができた
    • このような手順書と作業ログファイル大量発生から解放されるはず!?
    • デプロイ手順書_v1、デプロイ手順書2_v1レビュー済、デプロイ手順書_v1デプロイ後のロールバック作業ログ。。

今後も考えなければならないこと

  • 環境変数など、デプロイ環境に応じて値が変わる変数の取り扱いや設定ファイルの指定はだれがおこなうか?
    • 従来はアプリケーションのconfig、dotenvやDockerfileのENVなどだったはずだが、デプロイ環境がKubernetesの場合はどうする?
  • GKEの場合、セキュアな変数を取り扱う際にはSecretを使うことがあるが、これもコード化する?manifestリポジトリのGit管理下に含める?

CI/CDパイプラインを設計する際に参考にした資料

8. 参考