CloudBuildとGitHubで、GitOpsスタイルの継続的デリバリー

この記事では、CloudBuildとGitHubを用いて以下のCI/CD戦略を実現します。
![t1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112715/c2b150bd-3d76-e53a-5482-171c7d3b39f9.png)

1. アプリケーションの変更を`App`リポジトリにプッシュ
2. 1のプッシュをトリガーに、CloudBuildを実行<br>
   ・ContainerRegistryにイメージをプッシュ<br>
   ・マニフェストファイルを自動更新して、`Manifest`リポジトリにプッシュ
3. 2のプッシュをトリガーに、CloudBuildを実行<br>
   ・GKEに最新のマニフェストファイルを適用<br>
   ・適用された旨を、`Manifest`リポジトリの特定のブランチにコミット

# 前提条件
・マニフェストファイルの適用に十分なリソースの**Kubernetesクラスターが構築されている**
・アプリケーションとマニフェストの**リポジトリが分割されている**

# 環境構築
[GitOps-style continuous delivery with Cloud Build](https://cloud.google.com/kubernetes-engine/docs/tutorials/gitops-cloud-build)を参考にしています。(タイトルも~~パクり~~参考にしました)
`App`と`Manifest`のリポジトリは、それぞれ以下を使用しています。

名前|リポジトリ
---|---
App|[go-grpc-health-probe-sample](https://github.com/istsh/go-grpc-health-probe-sample/tree/6b4622ae134fe50bed7f51f320f5e2d60c445d81)
Manifest|[go-grpc-health-probe-sample-manifests](https://github.com/istsh/go-grpc-health-probe-sample-manifests/tree/cf877c0b976934fdee8f7f89ad879cdb70843356)

※ 記事中に「App」や「Manifest」という名前が出てきた場合は、上記のリポジトリやそれに関連するファイルのことを表しています。

## 必要なAPIを有効にする
```
$ gcloud services enable \
      container.googleapis.com \
      cloudbuild.googleapis.com \
      containeranalysis.googleapis.com
```

## CloudBuildに、GKEへのアクセス権を付与する
1. Google Cloud Consoleで、Cloud Buildの`設定`ページを開きます。
2. Kubernetes Engine Developerのステータスを`有効`にします。
![t2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112715/4b87dc1e-e6fe-724c-0583-b7f1f7d04b1e.png)

## GitHubのSSH認証鍵をSecretManagerに保存する
詳しくは[ここ](https://cloud.google.com/cloud-build/docs/access-private-github-repos)を参考にしてください。

1. `workingdir`ディレクトリを作成し、そこに新しいGitHub SSH認証鍵を作成します。<br>
   (ディレクトリ名は任意)
2. キーの保存先ファイルの入力を求められたら、`id_github`と入力します。<br>
   ※ パスフレーズは空のままにしてください。
3. Google Cloud Consoleで、`セキュリティ > シークレット マネージャー`ページを開きます。
4. `+CREATE SECRET`をクリック<br>
   ・名前は`id_github`<br>
   ・先ほど作成したSSH認証鍵を選択<br>
   ・`シークレットを作成`をクリックします。

```
$ mkdir -p ~/workingdir
$ cd ~/workingdir
$ ssh-keygen -t rsa -b 4096 -C ${GITHUB_EMAIL}
```

![t3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112715/ea3a4381-795c-6272-eeba-16aec0e17684.png)

## GitHubに公開SSH認証鍵を追加する
1. `Manifest`リポジトリの`Settings > Deploy Keys`の `Add deploy key`をクリックする。
2. タイトルを入力し、先ほど作成した公開SSH認証鍵を貼り付ける。
3. `Allow write access`にチェックをいれて、`キーの追加`をクリックする。<br>
   ※ この認証鍵を使用して、`Manifest`リポジトリにプッシュするので、必ずチェックを入れる。
4. ローカルのSSH認証鍵は不要なので、削除する。
![t4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112715/4bfb4288-357b-daef-cf61-edfcc22a869f.png)

## CloudBuildサービスアカウントに、SecretManagerへのアクセス権を付与する
1. Cloud Consoleで`IAMと管理 > IAM`ページを開きます。
2. 表から、末尾が`@cloudbuild.gserviceaccount.com`のメールアドレスを見つけて、鉛筆アイコンをクリックします。
3. `Secret Manager のシークレット アクセサー`のロールを追加し、`保存`をクリックします。

## 各リポジトリでの準備
ブランチ名は、可能な限りデブロイ先の環境名に近づけてください。

デプロイ先|Appリポジトリのブランチ名|Manifestリポジトリのブランチ名
---|---|---
dev|develop|candidate-dev
staging|staging|candidate-staging
production|production|candidate-production

ここでは`dev環境`向けの設定について説明します。

### Appリポジトリの準備
#### cloudbuild.yaml
※見やすくする為に、リポジトリの内容から一部変更しています。
[cloudbuild.yaml](https://github.com/istsh/go-grpc-health-probe-sample/blob/6b4622ae134fe50bed7f51f320f5e2d60c445d81/.gcp/dev/cloudbuild.yaml)

```yaml
timeout: 780s
steps:
  - id: build and push grpc-gateway image
    name: gcr.io/kaniko-project/executor:latest
    args:
      - --destination=gcr.io/$PROJECT_ID/grpc-gateway:${SHORT_SHA}
      - --cache=true
      - --cache-ttl=6h
      - --dockerfile=Dockerfile.proxy
      - --build-arg=PROJECT_ID=${PROJECT_ID}

  - id: build and push grpc-server image
    name: gcr.io/kaniko-project/executor:latest
    args:
      - --destination=gcr.io/$PROJECT_ID/grpc-server:${SHORT_SHA}
      - --cache=true
      - --cache-ttl=6h
      - --dockerfile=Dockerfile.server
      - --build-arg=PROJECT_ID=${PROJECT_ID}

  - id: access the id_github file from secret manager
    name: gcr.io/cloud-builders/gcloud
    entrypoint: 'bash'
    args:
      - '-c'
      - |
        gcloud secrets versions access latest --secret=id_github > /root/.ssh/id_github
    volumes:
      - name: 'ssh'
        path: /root/.ssh

  - id: set up git with key and domain
    name: 'gcr.io/cloud-builders/git'
    entrypoint: 'bash'
    args:
      - '-c'
      - |
        chmod 600 /root/.ssh/id_github
        cat <<EOF >/root/.ssh/config
        Hostname github.com
        IdentityFile /root/.ssh/id_github
        EOF
        ssh-keyscan -t rsa github.com > /root/.ssh/known_hosts
    volumes:
      - name: 'ssh'
        path: /root/.ssh

  - id: connect to the repository
    name: 'gcr.io/cloud-builders/git'
    args:
      - clone
      - --recurse-submodules
      - git@github.com:istsh/go-grpc-health-probe-sample-manifests.git
    volumes:
      - name: 'ssh'
        path: /root/.ssh

  - id: switch to candidate-dev branch
    name: 'gcr.io/cloud-builders/gcloud'
    dir: go-grpc-health-probe-sample-manifests
    entrypoint: /bin/sh
    args:
      - '-c'
      - |
        set -x && \
        git config --global user.email $(git log --format='%an <%ae>' -n 1 HEAD | sed 's/.*\<\([^>]*\)\>.*/\1/g') && \
        git fetch origin candidate-dev && git switch candidate-dev && \
        git fetch origin master && git merge origin/master
    volumes:
      - name: 'ssh'
        path: /root/.ssh

  - id: generate manifest for grpc-gateway
    name: 'gcr.io/cloud-builders/gcloud'
    dir: go-grpc-health-probe-sample-manifests/k8s/base/grpc-gateway
    entrypoint: /bin/sh
    args:
      - '-c'
      - |
        set -x && \
        sed "s/GOOGLE_CLOUD_PROJECT/${PROJECT_ID}/g" deployment.yaml.tpl | \
        sed "s/COMMIT_SHA/${SHORT_SHA}/g" > deployment.yaml

  - id: generate manifest for grpc-server
    name: 'gcr.io/cloud-builders/gcloud'
    dir: go-grpc-health-probe-sample-manifests/k8s/base/grpc-server
    entrypoint: /bin/sh
    args:
      - '-c'
      - |
        set -x && \
        sed "s/GOOGLE_CLOUD_PROJECT/${PROJECT_ID}/g" deployment.yaml.tpl | \
        sed "s/COMMIT_SHA/${SHORT_SHA}/g" > deployment.yaml

  - id: push generated manifests to candidate-dev branch
    name: 'gcr.io/cloud-builders/gcloud'
    dir: go-grpc-health-probe-sample-manifests
    entrypoint: /bin/sh
    args:
      - '-c'
      - |
        set -x && \
        git add k8s/base/grpc-gateway/deployment.yaml k8s/base/grpc-server/deployment.yaml && \
        git commit \
        --author="Cloud Build Service Account <$(gcloud auth list --filter=status:ACTIVE --format='value(account)')>" \
        -m "Deploying images
        - gcr.io/istsh-sample/grpc-gateway:${SHORT_SHA}
        - gcr.io/istsh-sample/grpc-server:${SHORT_SHA}
        Built from commit ${COMMIT_SHA} of repository go-grpc-health-probe-sample-manifest" && \
        git push origin candidate-dev
    volumes:
      - name: 'ssh'
        path: /root/.ssh
```

ステップは以下の順序・内容で実行されます

1. grpc-gatewayのイメージを作成し、ContainerRegistryにプッシュ。タグは`:${SHORT_SHA}`。
2. grpc-serverのイメージを作成し、ContainerRegistryにプッシュ。タグは`:${SHORT_SHA}`。
3. SecretManagerからGitHub SSH認証鍵を取得し、`/root/.ssh/`に配置。
4. GitHubに接続する為の設定。
5. `Manifest`リポジトリをクローン。
6. `candidate-dev`ブランチにスイッチし、`master`ブランチをマージ。<br>
   マニフェストファイルの変更をmasterにマージしておけば、このステップで`candidate-dev`ブランチに取り込まれます。
7. grpc-gatewayの`deployment.yaml.tpl`の内容を一部変更し、`deployment.yaml`を作成または変更。<br>
   ・`GOOGLE_CLOUD_PROJECT` -> `${PROJECT_ID}`<br>
   ・`COMMIT_SHA` -> `${SHORT_SHA}`
8. grpc-serverの`deployment.yaml.tpl`の内容を一部変更し、`deployment.yaml`を作成または変更。<br>
   ・`GOOGLE_CLOUD_PROJECT` -> `${PROJECT_ID}`<br>
   ・`COMMIT_SHA` -> `${SHORT_SHA}`
9. `deployment.yaml`をコミットし、`Manifest`リポジトリにプッシュ

### Manifestリポジトリの準備
#### 対応するブランチを作成する
ブランチ名|役割
---|---
candidate-dev|最新のマニフェストファイルをコミットするブランチ。<br>デプロイのトリガーとなる。
dev|マニフェストファイルが適用された旨をコミットするブランチ

#### マニフェストファイルのテンプレートを用意する
`App`リポジトリのプッシュにより実行されるビルドでは、yamlのテンプレートを用いて、自動的にマニフェストファイルを生成します。
そこで自動で生成または変更する必要のあるマニフェストファイルのテンプレートを用意します。
今回は、`deployment.yaml`を自動で生成または変更する必要があるので、`deployment.yaml.tpl`ファイルを作成します。
ここでは1ファイルだけ紹介します。
[deployment.yaml.tpl](https://github.com/istsh/go-grpc-health-probe-sample-manifests/blob/cf877c0b976934fdee8f7f89ad879cdb70843356/k8s/base/grpc-server/deployment.yaml.tpl)

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: grpc-server
spec:
  replicas: 2
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: grpc-server
  template:
    metadata:
      labels:
        app: grpc-server
    spec:
      volumes:
        - name: envoy
          configMap:
            name: envoy-grpc-server-config
      containers:
        - name: grpc-server
          image: gcr.io/istsh-sample/grpc-server:COMMIT_SHA
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
          command: ["/server"]
          readinessProbe:
            exec:
              command: [ "/bin/grpc_health_probe", "-addr=:8080" ]
            initialDelaySeconds: 1
          livenessProbe:
            exec:
              command: [ "/bin/grpc_health_probe", "-addr=:8080" ]
            initialDelaySeconds: 1
        - name: envoy-proxy
          image: envoyproxy/envoy:v1.14-latest
          imagePullPolicy: Always
          command:
            - "/usr/local/bin/envoy"
          args:
            - "--config-path /etc/envoy/envoy.yaml"
          ports:
            - name: app
              containerPort: 10000
            - name: envoy-admin
              containerPort: 8001
          volumeMounts:
            - name: envoy
              mountPath: /etc/envoy
```
注目すべきは、`:COMMIT_SHA`の部分です。
CloudBuildのビルドにより、毎回ContainerRegistryにイメージがプッシュされるのですが、トリガーとなったコミットのIDで、この`:COMMIT_SHA`を置換します。

#### cloudbuild.yaml
※見やすくする為に、リポジトリの内容から一部変更しています。
[cloudbuild.yaml](https://github.com/istsh/go-grpc-health-probe-sample-manifests/blob/cf877c0b976934fdee8f7f89ad879cdb70843356/.gcp/dev/cloudbuild.yaml)

```yaml
steps:
  - id: deploy
    name: 'gcr.io/cloud-builders/kubectl'
    args:
    - 'apply'
    - '-k'
    - 'k8s/overlays/dev/'
    env:
    - 'CLOUDSDK_COMPUTE_ZONE=${_CLOUDSDK_COMPUTE_ZONE}'
    - 'CLOUDSDK_CONTAINER_CLUSTER=${_CLOUDSDK_CONTAINER_CLUSTER}'

  - name: gcr.io/cloud-builders/gcloud
    entrypoint: 'bash'
    args: [ '-c', 'gcloud secrets versions access latest --secret=id_github > /root/.ssh/id_github' ]
    volumes:
      - name: 'ssh'
        path: /root/.ssh

  - name: 'gcr.io/cloud-builders/git'
    entrypoint: 'bash'
    args:
      - '-c'
      - |
        chmod 600 /root/.ssh/id_github
        cat <<EOF >/root/.ssh/config
        Hostname github.com
        IdentityFile /root/.ssh/id_github
        EOF
        ssh-keyscan -t rsa github.com > /root/.ssh/known_hosts
    volumes:
      - name: 'ssh'
        path: /root/.ssh

  - name: 'gcr.io/cloud-builders/git'
    args:
      - clone
      - --recurse-submodules
      - git@github.com:istsh/go-grpc-health-probe-sample-manifests.git
    volumes:
      - name: 'ssh'
        path: /root/.ssh

  - name: 'gcr.io/cloud-builders/gcloud'
    id: Copy to dev branch
    dir: go-grpc-health-probe-sample-manifests
    entrypoint: /bin/sh
    args:
      - '-c'
      - |
        set -x && \
        git config --global user.email $(git log --format='%an <%ae>' -n 1 HEAD | sed 's/.*\<\([^>]*\)\>.*/\1/g') && \
        git fetch origin dev && git switch dev && \
        git checkout $COMMIT_SHA k8s/base/grpc-gateway/deployment.yaml && \
        git checkout $COMMIT_SHA k8s/base/grpc-server/deployment.yaml && \
        git commit \
        --author="Cloud Build Service Account <$(gcloud auth list --filter=status:ACTIVE --format='value(account)')>" \
        -m "Manifest from commit $COMMIT_SHA

        $(git log --format=%B -n 1 $COMMIT_SHA)" && \
        git push origin dev
    volumes:
      - name: 'ssh'
        path: /root/.ssh

substitutions:
  _CLOUDSDK_COMPUTE_ZONE: us-central1-b
  _CLOUDSDK_CONTAINER_CLUSTER: go-grpc-health-probe-sample
```

ステップは以下の内容・順序で実行されます

1. GKEに最新のマニフェストファイルを適用。
2. SecretManagerからGitHub SSH認証鍵を取得し、`/root/.ssh/`に配置。
3. GitHubに接続する為の設定。
4. `Manifest`リポジトリをクローン。
5. `dev`ブランチにスイッチ。<br>
   `candidate-dev`ブランチにコミットされた`deployment.yaml`を反映し、コミット&プッシュ。

## CloudBuildのトリガーを追加する
トリガーは`App用`と`Manifest用`の2つ追加する必要があります。

### Appリポジトリ用のトリガー
1. `リポジトリを接続`から、`GitHub`を選択し、`続行`をクリック
2. 対象の`App`リポジトリを選択し、`リポジトリを接続`をクリック
3. `pushトリガーを作成`をクリック
4. イベント > `ブランチにpushする`を選択
5. ソース > ブランチに `^develop$`と入力
6. ビルド構成 > `Cloud Build 構成ファイル`を選択し、`cloudbuild.yaml`のパスを入力
7. `保存`をクリック

### Manifestリポジトリ用のトリガー
1. `リポジトリを接続`から、`GitHub`を選択し、`続行`をクリック
2. 対象の`Manifest`リポジトリを選択し、`リポジトリを接続`をクリック
3. `pushトリガーを作成`をクリック
4. イベント > `ブランチにpushする`を選択
5. ソース > ブランチに `^candidate-dev$`と入力
6. ビルド構成 > `Cloud Build 構成ファイル`を選択し、`cloudbuild.yaml`のパスを入力
7. `保存`をクリック

## コミットの内容
`App`リポジトリのCloudBuildが成功すると、以下の様なコミットコメントで`Manifest`リポジトリにプッシュされます。

```
Author: Cloud Build Service Account <xxx@cloudbuild.gserviceaccount.com>
Date:   Sun Sep 6 14:25:37 2020 +0000

    Deploying images
    - gcr.io/istsh-sample/grpc-gateway:12792a4
    - gcr.io/istsh-sample/grpc-server:12792a4

    Built from commit 12792a48c682312075ff4988367eb0b7393d28cd of repository go-grpc-health-probe-sample-manifest
```

さらに、このプッシュをトリガーにした`Manifest`リポジトリのCloudBuildが成功すると、以下の様なコミットコメントで`Manifest`リポジトリにプッシュされます。

```
Author: Cloud Build Service Account <xxx@cloudbuild.gserviceaccount.com>
Date:   Sat Sep 5 16:16:40 2020 +0000

    Manifest from commit beaa97bc46cfb624f1f325da1702d99fec0c9af0

    Deploying images
    - gcr.io/istsh-sample/grpc-gateway:12792a4
    - gcr.io/istsh-sample/grpc-server:12792a4

    Built from commit 12792a48c682312075ff4988367eb0b7393d28cd of repository go-grpc-health-probe-sample-manifest
```

# まとめ
`App`リポジトリにプッシュするだけで、CloudBuildで2つのビルドが連鎖的に実行され、特定の環境にデプロイされるようになりました。
今後はArgoCDやSpinnakerを使用したCI/CDにも挑戦してみようと思います。