GKEでArgoを使ったカナリアリリースを実現する（完結編）

## 概要
[GKEでArgoを使ったカナリアリリースを実現する（セットアップ編）](https://qiita.com/takemotto/items/2ed4478a314bf7e4a63e)の続編になります。
前回は、Argoをセットアップして、GitOpsできるところまで確認しました。
今回は完結編となります。

## 構成図
![argocd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95324/4bc0455e-48a9-3b39-4486-82d0b850979b.png)

## 構成要素
* GKE
* GCR
* Cloud Build
* Cloud Load balancer(ingress)
* GitHub
* [argo cd](https://github.com/argoproj/argo-cd)
* [argo rollouts](https://github.com/argoproj/argo-rollouts)

## 前提条件
* [GKEでArgoを使ったカナリアリリースを実現する（セットアップ編）](https://qiita.com/takemotto/items/2ed4478a314bf7e4a63e)でのセットアップが終わっていること

## 環境
* macOS Majave
* kubectl Client v1.15.1
* kubectl Server v1.12.8-gke.10
* Google Cloud SDK 259.0.0(gcloudはこれに含まれる）

## 今回の流れ
完結編の大まかな流れがこちらです。

1. 前回のおさらい
2. Dockerイメージの更新された時はどうするのか？
3. HELMのセットアップ
4. GCRのセットアップ
6. Dockerfileの修正
7. CloudBuildのセットアップ
8. ようやくカナリアリリース

## 手順
### 1. 前回のおさらい
前回のおさらいで、GitOpsができているか確認します。
`guestbook/frontend-deployment.yaml`でコンテナに割り当てるリソースを変更し、Commit、Pushしてみます。

```yaml

# before
      - name: php-redis
        image: gcr.io/google-samples/gb-frontend:v4
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
```
```yaml

# after
      - name: php-redis
        image: gcr.io/google-samples/gb-frontend:v4
        resources:
          requests:
            cpu: 110m　# スケールアップ
            memory: 110Mi　# スケールアップ
```

ArgoのGUIで確認すると、新たにReplicasetが作成され、最終的に切り替わっていることが確認できます。
Argoの`REFRESH`ボタンを押すと、すぐに同期してくれます。
<img width="1352" alt="スクリーンショット 2019-08-28 15.20.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95324/35f93ac1-b1b9-d53e-b258-b22e73486924.png">
<img width="1338" alt="スクリーンショット 2019-08-28 15.24.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95324/c0696e77-2cc8-dbc1-cf2c-897272e14e18.png">

無事GitOpsによるデプロイが完了しました。
裏では、ArgoがManifestファイルを基に`kubectl apply`を実行しているようです。

### 2. Dockerイメージの更新された時はどうするのか？
ここまでやってみて、1つ疑問が浮かびます。
確かにManifestファイルが更新された時は、ArgoでGitHubにあるManifestファイルとの差分を検知し、
自動でデプロイが走ります。
しかし、 **Dockerfileやアプケーションコードに変更があった場合はどうでしょうか？**
例えば[ここ](https://github.com/kubernetes/examples/blob/master/guestbook/frontend-deployment.yaml#L19)。

```yaml

      - name: php-redis
        image: gcr.io/google-samples/gb-frontend:v4　# タグがハードコードされている
        resources:
          requests:
            cpu: 110m
            memory: 110Mi
```

`gb-frontend:v4` という形でDockerイメージのタグがハードコードされてしまっています。
例えばCloud build等のCIを使っている場合、アプリケーションに変更があったら自動でテスト、ビルドを行い、GCR等にDockerイメージをPushするかと思います。
しかし、この書き方だと毎回Dockerイメージのタグを書き換え(例えばv4→v5)ないと新しいDockerイメージがデプロイされません。
せっかくCIを使って自動化しているのに、**毎回Dockerイメージのタグ変更をPull-Requestを出すのはしんどいな** と思いました。
GitOpsじゃないと言われそうですが、どうせならここも自動化したいです。
そこで出てくるのがHelmのtemplate使った方法です。

### 3. HELMのセットアップ
Helmにはテンプレートの機能があり、公式のGitHubリポジトリではそのテンプレート機能を使ってタグを書き換えています。
サンプルはblue-greenですが、やることは基本的には同じです。

サンプル：https://github.com/argoproj/argocd-example-apps/tree/master/blue-green

やってみたが方がわかりやすいのこのまま進めます。

```shell

# Helmのインストール
$ brew install kubernetes-helm

# tillerのインストール
$ cd path/to/sourcecode # 今回の例だとexamples
$ cd guestbook
$ helm init
$ kubectl get pods --namespace kube-system | grep tiller

# HELMチャートの作成
$ APPNAME=guestbook # アプケーション名を設定
$ helm create $APPNAME
$ ls $APPNAME
Chart.yaml      charts          templates       values.yaml

# デフォルトで作成されるManifestファイルを削除
$ rm -rf guestbook/templates/*

# Manifestファイルをtemplateに移動
$ mv *.yaml guestbook/templates/
$ ls guestbook/templates/
frontend-deployment.yaml	frontend-service.yaml		ingress.yaml			redis-master-deployment.yaml	redis-master-service.yaml	redis-slave-deployment.yaml	redis-slave-service.yaml
```

上記の修正が終わったら、Commit,Pushしておきます。
次にArgoが同期するManifestファイルのパスが変わったので、修正しておきます。

```
$ argocd app edit $APPNAME
```

変更内容は以下の通りです。

```yaml

# before
destination:
  namespace: default
  server: https://35.194.96.41
project: default
source:
  path: guestbook/
  repoURL: git@github.com:<your GitHub account>/examples.git
  targetRevision: master
syncPolicy:
  automated:
    prune: true
```
```yaml

# after
destination:
  namespace: default
  server: https://35.194.96.41
project: default
source:
  path: guestbook/guestbook/ # ここのパスを修正
  repoURL: git@github.com:<your GitHub account>/examples.git
  targetRevision: master
syncPolicy:
  automated:
    prune: true
```

GUI上でもPathが修正されているいことを確認できます。
<img width="491" alt="スクリーンショット 2019-08-29 15.04.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95324/1bd11d4b-1795-5355-432a-86d57b877c57.png">


Argoは、Helmに対応しているため特別な変更なく同期されました。
Helmの他、Kustomizeにも対応しているようです。詳細は[こちら](https://argoproj.github.io/docs/argo-cd/docs/user-guide/application_sources.html)を参照してください。

### 4. GCRのセットアップ
次にDockerイメージをGCRにPushして、リポジトリを作成します。
`guestbook/php-redis/Dockerfile` をビルドしてイメージをGCRにPushします。

```shell
# setup
$ PROJECT_ID=<your GCP Project ID>
$ gcloud config set project ${PROJECT_ID}
$ gcloud auth configure-docker

# Build
$ cd path/to/sourcecode
$ cd guestbook/php-redis/
$ docker build -f Dockerfile -t asia.gcr.io/${PROJECT_ID}/${APPNAME}:stable .
$ docker images | grep ${APPNAME}
asia.gcr.io/<your GCP Project ID>/guestbook                         stable              0700c57dd8bc        26 seconds ago      357MB

# Push
$ docker push asia.gcr.io/${PROJECT_ID}/${APPNAME}:stable
$ gcloud container images list --repository asia.gcr.io/${PROJECT_ID}
NAME
asia.gcr.io/<your GCP Project ID>/guestbook
```
DockerイメージがGCRにPushされました。
次にGCRからGKEにデプロイしてみます。
以下のように`guestbook/guestbook/templates/frontend-deployment.yaml`修正し、Commit、Pushします。
変更内容は以下の通りです。

```yaml
# before
      - name: php-redis
        image: gcr.io/google-samples/gb-frontend:v4
```
```yaml
# after
      - name: php-redis
        image: asia.gcr.io/<your GCP Project ID>/<your APPNAME>:stable
```

ArgoのGUIでも前回と同じように新たなReplicasetが作成され、デプロイされたことが確認できると思います。

### 5. Dockerfileの修正
現状`guestbook/php-redis/Dockerfile`のままだとCloud Buildでbuildする際にコケるので、以下の箇所を修正し、Commit,Pushします。

```yaml

# bofore
ADD guestbook.php /var/www/html/guestbook.php
ADD controllers.js /var/www/html/controllers.js
ADD index.html /var/www/html/index.html
```
```yaml

# after
ADD guestbook/php-redis/guestbook.php /var/www/html/guestbook.php
ADD guestbook/php-redis/controllers.js /var/www/html/controllers.js
ADD guestbook/php-redis/index.html /var/www/html/index.html
```

### 6. CloudBuildのセットアップ
続いて、この処理をCloud Buildを使って自動化していきます。

#### ArgoのIPアドレス確認

```shell

$ kubectl get service argocd-server -n argocd | tail -n 1 | awk '{print $4}'
34.85.104.22
```


#### cloudbuild.yamlの作成
リポジトリのroot階層に、`cloudbuild.yaml`を作成し、Commit,Pushします。

```yaml

timeout: 780s
steps:
- name: gcr.io/kaniko-project/executor
  args:
    - --destination=asia.gcr.io/$PROJECT_ID/guestbook:${COMMIT_SHA}
    - --cache=true
    - --cache-ttl=6h
    - --dockerfile=guestbook/php-redis/Dockerfile
- name: argoproj/argocd
  entrypoint: 'bash'
  args: ['-c', 'argocd login --insecure ${_ARGO_SERVER} --username admin --password ${_ARGOCD_PW} && argocd app set ${_PRJ} -p image.tag=${COMMIT_SHA} && argocd app sync ${_PRJ}'] 
substitutions:
  _PRJ: guestbook
  _ARGO_SERVER: <確認したArgoのIPアドレス>
```

処理内容をざっくり解説すると、指定したDockerfileを基にビルドし、`guestbook:${COMMIT_SHA}`という名前でGCRにPushしています。
`${COMMIT_SHA}`は、Cloud Buildで用意された変数です。詳細はは、[こちら](https://cloud.google.com/cloud-build/docs/configuring-builds/substitute-variable-values?hl=ja)を参照してください。
次に、argocdコマンドを用いてログインし、`argocd app set ${_PRJ} -p image.tag=${COMMIT_SHA}`で新たにPushしたDockerイメージに変更しています。

#### Cloud Buildの設定

##### 1. Cloud Build APIの有効化
Cloud Buildに移動します。APIが有効化されていない場合は、有効化してください。
<img width="478" alt="スクリーンショット 2019-08-28 18.22.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95324/b5055840-8430-940b-d063-6d99d7046585.png">


##### 2. トリガーの設定
###### 2-1. トリガーの作成
<img width="482" alt="スクリーンショット 2019-08-28 18.23.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95324/46ea84d0-8d1b-1ab7-1d24-8e775a76ce30.png">

###### 2-2. ソースを選択（GitHubを選択）
<img width="543" alt="スクリーンショット 2019-08-28 18.23.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95324/04567680-2127-04c3-c3be-ce3d1897021e.png">

###### 2-3. リポジトリを選択（Forkしたexamplesリポジトリを選択）
![スクリーンショット 2019-08-28 18.24.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95324/b638391c-5143-ef3c-4e8d-4075940d1bdc.png)

###### 2-4. トリガーの作成
![スクリーンショット 2019-08-29 9.57.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95324/6abf4174-0a74-06e8-a5d8-ff36c1f72756.png)

`無視されるフィルタ`に入れているのは、Manifestファイルに更新があった場合、GitHubとの同期で更新されるためです。
`_ARGOCD_PW`に関しては、Cloud Build内のArgoコマンドで使用しています。ここなんとかして暗号化したい。。

保存したら、`トリガーを実行`ボタンを押して、Cloud Buildを走らせてみます。
２つのビルドステップが成功すればOKです。

最後に`guestbook/guestbook/templates/frontend-deployment.yaml`のタグ部分を`stable`から`{{ .Values.image.tag }}`に修正し、Commit、Pushします。
変更内容は以下の通りです。

```yaml

# before
      containers:
      - name: php-redis
        image: asia.gcr.io/<your GCP Project ID>/<your APPNAME>:stable
```
```yaml

# after
      containers:
      - name: php-redis
        image: asia.gcr.io/<your GCP Project ID>/<your APPNAME>:{{ .Values.image.tag }}
```

ちなみにこの`{{ .Values.image.tag }}`という値は、`guestbook/guestbook/values.yaml`に設定されているものです。
このあたりは、Helmの話なので割愛します。

### 7. ようやくカナリアリリース
お待たせしました。
`guestbook/guestbook/templates/frontend-deployment.yaml`を以下のように修正し、Commit,Pushします。

```yaml
apiVersion: argoproj.io/v1alpha1  # 変更
kind: Rollout  # 変更。ここでようやくargo-rolloutsを登場
metadata:
  name: frontend
spec:
  selector:
    matchLabels:
      app: guestbook
      tier: frontend
  replicas: 3
  template:
    metadata:
      labels:
        app: guestbook
        tier: frontend
    spec:
      containers:
      - name: php-redis
        image: asia.gcr.io/<your GCP Project ID>/guestbook:{{ .Values.image.tag }} # GCP Project IDを修正
        resources:
          requests:
            cpu: 110m
            memory: 110Mi
        env:
        - name: GET_HOSTS_FROM
          value: dns
          # If your cluster config does not include a dns service, then to
          # instead access environment variables to find service host
          # info, comment out the 'value: dns' line above, and uncomment the
          # line below:
          # value: env
        ports:
        - containerPort: 80
  # ここから下を追記
  minReadySeconds: 30 # ３０秒待つ？
  revisionHistoryLimit: 3 # 3世代保持。つまり3世代までロールバック可能。
  strategy:
    canary:
      steps:
      - setWeight: 20  # 全体トラフィックの20%を
      - pause: 
          duration: 20  # 20秒間新しいアプケーションに流す
      - setWeight: 50  # 次に全体トラフィックの５0%を
      - pause:
          duration: 20  # 20秒間新しいアプケーションに流す
```

しばらくすると、`rollouts`リソースが作成されていることが分かります。
<img width="1025" alt="スクリーンショット 2019-08-29 15.44.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95324/962d7b6c-3ac4-f03f-c86d-5f413357d27d.png">


次にカナリアリリースできているかどうかを確認するために`guestbook/php-redis/index.html`を`Guestbook ver.2`に修正し、Commit,Pushします。

```html
# before
      <h2>Guestbook</h2>
```
```html
# before
      <h2>Guestbook ver.2</h2>
```

しばらくするとカナリアリリースが走るので、ブラウザで何度かアクセスすると`Guestbook ver.2`が何度か1度表示されるようになると思います。
<img width="295" alt="スクリーンショット 2019-08-29 11.37.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95324/188acb79-6862-ecd5-986e-f39cf60f1e2e.png">

最終的には、`Guestbook ver.2`のみ表示されるようになるはずです。


## 参考文献

この記事は以下の情報を参考にして執筆しました。
[Helmの概要とChart(チャート)の作り方](https://qiita.com/thinksphere/items/5f3e918015cf4e63a0bc)