GitOpsの概要と実践例 〜Kustomize + CircleCI編〜

こんにちは。テクノロジー本部バックエンド開発グループの山下です。

この記事は キャディ Advent Calendar 2020 の11日目です。 前日は大原さんの 図面を管理するために図面版 figma を開発している話 について でした。

今回は以前のKustomizeの記事に続き、 「GitOps概要と実践例 〜Kustomize + CircleCI 編〜」と題して、 GitOpsの概要を説明した上で、KustomizeとCirlceCIを利用したk8s上でのGitOpsの実践例について 書いていこうと思います。


[toc]

GitOpsとは

概要

GitOps対応のCICDパイプライン:
  WeaveWorksのGuide To GitOpsより引用

GitOpsはきちんと説明するだけでもかなりの分量になるので、かなり思い切って要約すると 「Kubernetesクラスターの構成をコード化してGitで管理し、その情報だけを正として環境を展開するもの」です。

え、それもうやってるよ? Terraformでk8sクラスターなどインフラ構築して、イメージをkubectlとかでデプロイしているよ? という方も多いかと思います。

ただ、ここで重要なのは、コード化されたもの「だけ」を正とする、ということです。

つまり、コンテナイメージなど動的に生成されうるものなども含めて全てを静的に管理する、ということです。

この前提で継続的デリバリーを構築しようとすると面倒な事実に気付きます。

例えば、リリース対象のアプリケーションをビルドすると当たり前ですがイメージが新しく作られます。

ですが、そのイメージをそのままクラスターにデプロイすると、コードだけを正とするGitOpsの概念から外れます。

何故なら、そのイメージを一意に特定する情報がインフラ側のコードにはないため、同じ状態を再現できないためです。

正しくGitOpsを行おうとすると、コンテナをプッシュするのではなく、 何らかの手段で、ビルドしたイメージの情報をインフラコードに反映させなければなりません。

そうすると、反映されたインフラコードに合わせて実際の環境が収束していきます。

こうすることで、常に定義した内容と現実が一致するようになるのです。

GitOpsのメリット

ここまで聞くと、そんな面倒なことしなくても、ビルドしたイメージをデプロイしちゃえば良いじゃない、と思われるかと思います。

ですが、GitOpsにはいくつかのメリットがあり、この面倒なことをする理由になります。

そのメリットの中で、キャディで採用した際に重視した点は大きく三つです。

信頼性と復元力

一つ目が、信頼性と復元力、つまり必ずある時点での状態に戻すことが出来ることです。

GitOpsでは必ずコードで定義されている状態に収束します。

つまりアプリケーションのイメージをタグなどを用いて定義してあれば、 必ずクラスターで展開されるイメージ全てが、その時々で一意に決まります。

加えて、キャディでは、Helmなどを利用して各種ミドルウェアの設定などを行っていますが、 その時のバージョンや設定なども合わせて管理されているため、 リリースした当時、バグが発生した当時の状況を確実に再現することができます。 (DBなどState部分に関しては出来ませんが)

これによって、バグ発生当時の状況の再現やロールバックなどの動作が容易になります。

関心の分離による生産性の向上

二つ目が、CIとCDの分離による関心の分離と、それに伴う生産性の向上です。

キャディでは稼働中のプロダクトが4つ存在しますが、 内部的には、さらに細かいサービスに別れて存在しています。

このサービス毎に、どうやってデプロイしよう、と考えたり 実際にCDの処理を書いたりするのは非常に面倒ですし、 デプロイ先のクラスターのことを理解していないといけないのは開発者にとって負担が大きいです。

しかし、GitOpsでは、イメージをビルドさえ出来れば、 その情報(タグなど)をインフラ側に反映させるだけでよく、 その書き換え処理の共通化も容易です。 (キャディではOrbを使ってCD処理などを共通化しています。以前書いたOrbの記事はこちらです)

そのためGitOpsでは、 アプリケーション開発チームはコードを書いてイメージが出来上がるまでに集中すればよく、 後のインフラ構成やインフラエンジニアの稼働状況などを気にせず、 開発を進めることが出来るようになりDX(開発者体験)が高まります。

もし、これが従来のE2Eのパイプラインによるデプロイ形式だと、 実際にアプリケーション側のCI/CDパイプラインでデプロイが完了するまで気にしなくてはいけませんが、 GitOpsであればイメージのタグが書き換わりさえすれば、そのあとはデプロイが何らかの理由で失敗しても 既にコードは変更してあるので、再度デプロイをしたり原因調査をインフラ担当者が行えばよくなります。

これはアプリケーション開発だとPubSubモデルに近く、 実際に処理が完了するまでをPublisher側が気にしなくてもよくなるのに近いです。

高い効率性とセキュリティを保つ運用体制

最後に三つ目が、高い効率性とセキュリティを保つ運用体制が構築できることです。

これはGitOpsでは、全ての変更やリリースがGitで管理され、その変更に応じて環境が変わるので、 誰が、何を変更したことによって、このリリースを行われたかを確実に分かるためです。

これによって、問題の発覚から原因の特定・対策までをスムーズに行うことが出来ますし、 許可されていないユーザーからの変更も防ぐことが出来ます。

更に、CIとCDを分離したことで、CI側にクラスターを変更する権限を渡すことなくリリースが行えるため、 セキュリティ的にも安全性が高まります。

GitOpsをより詳しく知るには

以上、大胆にGitOpsの定義を削って簡単に説明しましたが、 GitOpsには他にも様々な要素があり学びも大きいです。

詳細を知りたい方は、是非、この考えを提唱したWeaveWorksの公式資料をご覧ください。

キャディでの実践例

前置きが長くなりましたが、ここから実際にGitOpsを実現する方法を具体的に書いていきます。

前置きで色々書いていたり、公式資料にも小難しいこと書いているな、と思われるかもしれませんが、 GitOpsを実現する際に考えることは実は大きく三つだけです。

  1. どのような形式でイメージの情報を管理するか(≒構成管理)
  2. どのタイミングでイメージの情報を書き換えるか(≒ブランチ戦略)
  3. どうやってイメージの情報を書き換えるか(≒CDの実装)

どのような形式でイメージの情報を管理するか

ここでいうイメージの情報とはImageのtagやdigestなどで、 それをどう保存して管理するか、つまり構成管理の方法を最初に決める必要があります。

キャディではKustomizeを使って構成管理を行っています。

詳しくはこちらの記事で書いているので詳細は省略しますが、 基本はk8sのPodを定義するbaseのファイルと具体的なタグや設定値などが書いてあるファイルでoverlay つまり、上書きする、という方式を使っています。

具体的なサンプルで説明すると baseのファイル(path: base/services/**/deployment.yaml)が下記だとすると

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: sample-app
          image: sample-app-image

overlayするファイル(path: overlays/${env}/kutomization.yaml)が下記になります。

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- ../../base

images:
- name: sample-app
  newName: gcr.io/bucket-name/image-name
  newTag: 91c8cee4b05a0ab1642d63198969fac9df5f62ae

この二つを元にkustomize buildすると下記のようなmanifestが生成されます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: sample-app
          image: gcr.io/bucket-name/image-name:91c8cee4b05a0ab1642d63198969fac9df5f62ae

つまり、上記のbaseファイルでImage名だけで指定していたものを overlay側のkustomization.yamlでtagを指定して上書きすることで、 その時点でのイメージ情報を確定できます。

overlayのファイルは各環境毎に設定するので環境毎に設定することができます。 (詳細なディレクトリー構成はこちら

どのタイミングでイメージの情報を書き換えるか

CIを定義している各サービスのレポジトリーから インフラのレポジトリーで定義しているイメージ情報を変更する訳ですが、 それをどのタイミングにやるのか、を決める必要があります。

これはイコール、ブランチ戦略を決めることでもあります。

よく使われているパターンだとGitHubフローとGitフローがありますが、 どちらの場合でも下記の二つのアクションをどのブランチやタグに結びつけるか、 が変わるだけになります。

  1. いつイメージをビルドするか
  2. いつ、そのイメージ情報をインフラコード側に反映するか

対応表

この結びつきをGitHubフローのバージョンで対応表にすると下記のようになります。

Appブランチ イメージビルド インフラコード変更 デプロイされる環境
feature/* ビルドなし 変更なし なし
master ビルド developブランチを変更 Dev環境
masterブランチでtag打ち(vx.y.z) ビルドなし masterブランチを変更 Stg環境

深掘って説明すると、 最初の、featureブランチでは開発しているだけなので特にインフラの変更がないです。

次に、masterブランチでは、実際にbuildが走り、インフラのdevelopが変更されDev環境にデプロイされます。

最後は、GitHubのリリース機能を利用して、masterブランチにtagを打つことで処理が走ります。 tagが打たれたタイミングで、インフラのmasterブランチが変更され、Stg環境にデプロイされます。

イメージビルドなし、が気になるかと思いますが、masterブランチでのbuild時点でイメージタグをcommit hashにしているため、tagを打たれたとしても、あくまで、その時点のhash値を使ってイメージを指定すれば良いのでビルドの必要がないのです。

どうやってイメージの情報を書き換えるか

上記でいつ、どのように保存するかが決まりました。

最後にどのように書き換えるのか、ということですが、 こちらもKustomizeの機能を使って書き換えます。

具体的に利用するのは、KustomizeのEdit機能です。 (以前の記事の参考箇所

この機能で各アプリケーションサービスのCI後に、 インフラコードにあるoverlays/${env}/kustomization.yamlを対象の環境分だけ書き換えます (対象の環境は、developブランチであればdevだけ、masterブランチであれば、stgとprod)

コマンドとしては下記のようになります。

$ kustomize edit set image image_name:tag_name

ただ、このコマンドだけだとイメージがつきにくいと思うので、 キャディでこの書き換え処理を共通化しているOrbの設定を一部抜粋・変更して掲載すると

description: kustomization.yamlのimageのtagを変更する処理です
parameters:
  fingerprint:
    type: string
  gcr_host_name:
    type: string
    default: gcr.io
  github_group:
    type: string
    default: caddijp
  infra_repo_name:
    type: string
  project_name:
    type: string
    default: ${CIRCLE_PROJECT_REPONAME}
    description: the name of the project (usually the same as the repo and image name)
steps:
  - add_ssh_keys:
      fingerprints:
        - << parameters.fingerprint >>
  - run:
      name: Avoid hosts unknown for github
      command: echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
  - run:
      name: git clone infra code in specific branch decided by CIRCLE_BRANCH of CircleCI environment variables
      command: |
        git clone -b ${INFRA_BRANCH} git@github.com:<< parameters.github_group >>/<< parameters.infra_repo_name >>.git
  - run:
      name: rewrite image tag for release
      command: |
        if [[ ${INFRA_BRANCH} = develop ]];
        then
          TARGET_ENVS=("dev")
          IMAGE_TAG="dev-${CIRCLE_SHA1}"
        elif [[ ${INFRA_BRANCH} = master ]];
        then
          TARGET_ENVS=("stg" "prod")
          IMAGE_TAG=${CIRCLE_SHA1}
        else
            echo "This step failed because branch name is not suitable"
            echo "We have to set branch name from master, feature/*"
            exit 1
        fi
        for ENV in ${TARGET_ENVS[@]}
        do
            cd ~/infra/<< parameters.infra_repo_name >>/k8s/overlays/${ENV}
            kustomize edit set image << parameters.project_name >>=<< parameters.gcr_host_name >>/${GOOGLE_PROJECT_ID}/<< parameters.project_name >>:${IMAGE_TAG}
        done

下記のStep毎に順を追って説明します。 1. SSH keyの追加 2. Hostの追加 3. インフラコードのClone 4. 実際の書き換え処理

SSH Keyの追加

アプリケーション側のCIコンテナ上からインフラコードをCloneする際に必要です。

Hostの追加

CircleCIで対象のレポジトリー以外のレポジトリーからコードを取得する際に必要な設定になります。

インフラコードのClone

こちらは、書き換えたいインフラレポジトリーのブランチを ブランチ戦略に合わせて選択してCloneします。

実際の書き換え処理

キャディではインフラレポジトリーは release, master, develop, feature/* でブランチを運用しています。

そのブランチのコードを先ほどのブランチ戦略に対応表に合わせて変更しているのがこの部分になります。

変数の${INFRA_BRANCH}が対象となるブランチですが、これをブランチ戦略に合わせて処理しているOrbのコマンドを一部変更・抜粋したものが下記になります。

description: ビルド対象のブランチを元に対象となるinfraコードのブランチを設定します
steps:
  - run:
      name: Set infra branch name with CIRCLE_BRANCH in CircleCI environment variables
      command: |
        if [[ ${CIRCLE_TAG} =~ ^v ]];
        then
            INFRA_BRANCH="master"
        elif [[ ${CIRCLE_BRANCH} = master ]];
        then
            INFRA_BRANCH="develop"
        elif [[ ${CIRCLE_BRANCH} =~ ^(feature)/ ]];
        then
            echo "In build of feature branch, this step is skipped"
            exit 0
        else
            echo "This step failed because branch name is not suitable"
            echo "We have to set branch name from master and feature/*"
            exit 1
        fi
        echo "${INFRA_BRANCH}"
        echo "export INFRA_BRANCH=${INFRA_BRANCH}" >> $BASH_ENV

ブランチが決まれば、あとは対象のkustomizaton.yamlを書き換えるだけです。

developブランチであれば、overlays/dev/kustomization.yamlを masterブランチであれば、overlays/stg/kustomization.yamloverlays/prod/kustomization.yamlを書き換えます。

stgとprodを同じタイミングで書き換えるのは、stgからprodへの展開する際の手順を出来るだけ最小限にするためです。 (releaseブランチにmasterブランチをマージするだけで本番にリリース出来るようにすることで誤操作などが入らないようにしている)

応用編: GitOpsのPush型とPull型

以上で、キャディで実践しているGitOpsの実践例の紹介は終了となりますが、 ここまでで、GitOpsを理解されている方だと、何か違和感があるな、と思われる方もいらっしゃるかもしれません。

それは、実は、イメージ情報を変更する際や、インフラコードの変更をクラスターに反映する際の方法がPush型になっているからです。

最初のメリットのところで、GitOpsがPubSubのようなものだと書きましたが、 PubSubにPush型とPull型があるように、実はGitOpsにもPush型とPull型があります。

そして、PubSubがそうであるように、GitOpsに関しても同様にPull型の方が理想的だと言われています。

具体的には、ImageRegistryの変更を検知して、インフラコードをConfig Updaterが変更、 (現在ですとCI側でPushして変更していますが、PubSubでいうSubscriberが書き換える方がより適切に分離されることになる)

その変更内容をk8s側でwatchしておいて(pubsub的で例えるならsubscribeして)、 インフラコードに合わせて実態を収束させていく、というのが理想的な形になります。

Example GitOps Pipeline WeaveWorksのGuide To GitOpsより引用

ただ、PubSubでPull型で実装するのに、少し手間がかかるのと同様に、GitOpsでもPull型で実装するのは更に追加の対応が必要になります。

そのため、どれぐらいの工数を自分達のチームはかけることが出来るかに応じて、方法を選択してみるのも良いかもしれません。

Pull型で実装する場合は、一から実装するのは工数がかかりすぎるので、 GitOps系のCDツールとして最有力で開発が活発に続いているArgoCDや GitOpsを最初に提唱したWeaveWorksが開発しているFlux(ver2)などを試してみるのも良いかもしれません。

(因みに弊社の飯迫さんがArgo RolloutsでBlueGreenの実装を行った話を記事にされていますので、ご興味のある方はどうぞご覧ください)

最後に

長文になりましたが、KustomizeとCircleCI Orbのみで シンプルにGitOpsの考えを実現する方法をご紹介してみました。

応用編でも少し書きましたが、GitOpsはその考えが広まる中で、 それに関連する様々なツールやサービスが出ていますが、 間違いなく、これがデファクトスタンダードだ!と言えるツールやサービスがあるわけではありません。

その中で、今回の方法ですと、依存するものを最小限にしながらGitOpsの考えを取り入れることができ、結果的に学習コストも下がります。

加えて、最後に取り上げたArgoCDもKustomize対応をしていますし、 Fluxも、Kustomizeを使ったサンプルもあったりと、 今後、次の一手を打つ際にも柔軟さを保てるのもメリットです。

GitOpsの考えは良いけど、実際に適応するのをどうすれば良いんだ!?と悩んでいた昔の私に、 こういう理由でこういう選択をしたけど、どう?っという情報があったら、 もっと効果的な判断が出来たのではないかと思い、こちらの記事を書いてみました。

これからGitOpsを実現しよう、という方のお力になれましたら幸いです。


CADDiでは「モノづくり産業のポテンシャルを解放する」ために仲間を探しています。 実現したい世界に向け、作らなければならないもの、改善したいことが無限にあります。

少しでも興味を持って頂けましたら、リニューアルされたばかりの採用サイトをご覧ください。

また、カジュアル面談も行っていますので、実際にエンジニアに会ってみたい方はこちらから、 どうぞ宜しくお願いいたします。