GitOpsでBlue/Greenデプロイしてみる

# はじめに

夏休みの自由研究的なノリで、GitOps的な方法でBlue/Green Deploymentを自動化できるかな、とふと思い立ってやってみました
最終的に完全な自動化はできてはいないものの、現時点でできているものと所感をまとめていきます

まだまだDevOpsもKubernetesもちょっとかじった程度なので、ベストプラクティスとは程遠いのであしからず
あと、文章書くのヘタクソなので説明が不十分だったりしますが、どうか許してください

## 使ったツール

- GitHub
- CircleCI
- Kustomize
- ArgoCD
- Kubernetes @ EKS
- Istio

# ざっくり概要

1. GitHubにアプリケーションリポジトリとK8s Manifestリポジトリをそれぞれ作って、アプリケーションのソースコードとDockerfileとビルド用のCircleCIのconfig.ymlを作成してアプリケーションリポジトリにPush
2. CircleCIでDockerfileビルドしてECRにPushして、新しいイメージのデプロイ用にDeploymentとVirtualServiceそれぞれのkustomization.yamlを更新して、K8s ManifestリポジトリにPush & Pull Request
3. GitHubでPRマージ（これは手動じゃないとダメだね）
4. ArgoCDでK8s Manifestを同期して新しいイメージをBlue or Greenにのみデプロイ
5. デプロイしたアプリケーションが正しく動いているのを確認できたらBlue/Green切り替えのVirtualServiceもArgoCDで同期（ここ手動だけど自動化できるといいな）

# 要件（というかこだわりたかったこと）

- ベースのマニフェストはそのままで、デプロイにかかる変更は全てkustomization.yamlで完結させたい
- トレースやログで監視できるように新しいアプリケーションのバージョンをPodのラベルで管理したい
- Deploymentを更新した後はすぐに自動的にトラフィック切り替えるのではなく、テストした後に切り替えるようにしたい

# 設定つくってみる

## Kubernetesマニフェスト

ファイル・ディレクトリ構成がArgoCDに設定に大きく関わってくるので、試行錯誤しながら最終的に以下のようにしました

```
gorilla-sfx-demo
└─ eks
    ├─ deployment
    │   ├─ blue
    │   │   ├─ gorilla-sfx-demo-deployment-blue.yaml
    │   │   └─ kustomization.yaml
    │   └─ green
    │       ├─ gorilla-sfx-demo-deployment-green.yaml
    │       └─ kustomization.yaml
    ├─ virtualservice
    │   ├─ gorilla-sfx-demo-istio-virtualservice-blue.yaml
    │   ├─ gorilla-sfx-demo-istio-virtualservice-green.yaml
    │   └─ kustomization.yaml
    ├─ gorilla-sfx-demo-istio-destination.yaml
    ├─ gorilla-sfx-demo-service.yaml
    └─ kustomization.yaml
```

ちなみにファイル名やディレクトリ名の **gorilla** というのはGorilla/Muxで作ったデモ用アプリケーションなので、ゴリラアプリとかそういうものではないです

DeploymentはBlueとGreenそれぞれ別々に作って置いてあります

```bash
kubectl get pods -l app=gorilla-sfx-demo
NAME                                                 READY   STATUS    RESTARTS   AGE
gorilla-sfx-demo-deployment-blue-f9784849d-jz8f6     2/2     Running   2          2d
gorilla-sfx-demo-deployment-blue-f9784849d-pmpk5     2/2     Running   0          2d
gorilla-sfx-demo-deployment-green-7db5b8b754-jrdzz   2/2     Running   0          2d
gorilla-sfx-demo-deployment-green-7db5b8b754-ltz8c   2/2     Running   0          2d
```

それぞれの `kustomization.yaml` は↓

```yaml:eks/deployment/blue/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: ************.dkr.ecr.**-****-*.amazonaws.com/gorilla-sfx-demo
  newTag: latest
commonAnnotations:
  version: latest
commonLabels:
  version: latest
resources:
- gorilla-sfx-demo-deployment-blue.yaml
```

```yaml:eks/deployment/green/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: ************.dkr.ecr.**-****-*.amazonaws.com/gorilla-sfx-demo
  newTag: latest
commonAnnotations:
  version: latest
commonLabels:
  version: latest
resources:
- gorilla-sfx-demo-deployment-green.yaml
```

```yaml:eks/virtualservice/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- gorilla-sfx-demo-istio-virtualservice-blue.yaml
```

VirtualServiceは↓のようにBlue/Greenそれぞれ作成

```yaml:eks/virtualservice/gorilla-sfx-demo-istio-virtualservice-blue.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: gorilla-sfx-demo-virtualservice
  namespace: default
spec:
  hosts:
  - gorilla-sfx-demo-service
  http:
  - match:
    - headers:
        x-deploy:
          exact: green
    route:
    - destination:
        host: gorilla-sfx-demo-service
        subset: green
        port:
          number: 9090
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: gorilla-sfx-demo-service
        subset: blue
        port:
          number: 9090
```

```yaml:eks/virtualservice/gorilla-sfx-demo-istio-virtualservice-green.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: gorilla-sfx-demo-virtualservice
  namespace: default
spec:
  hosts:
  - gorilla-sfx-demo-service
  http:
  - match:
    - headers:
        x-deploy:
          exact: blue
    route:
    - destination:
        host: gorilla-sfx-demo-service
        subset: blue
        port:
          number: 9090
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: gorilla-sfx-demo-service
        subset: green
        port:
          number: 9090
```

本番トラフィックが流れてない方には `x-deploy` ヘッダを使ってリクエストをルートしてテストできるようにしてあります
で、そのヘッダには必ず `blue` か `green` かで指定するようにしてあります

[この記事](https://tech.uzabase.com/entry/2018/11/26/110407) を参考にしました とても勉強になりました


## ArgoCD

こんな感じ↓
![argocd_blue_green_deploy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174837/c7f70e05-1a3e-dfb0-9681-5d4b059a0869.png)
**gorilla-sfx-demo**: DeploymentとVirtualService以外の同期用（ディレクトリrecurseはしない）
**gorilla-sfx-demo-deployment-blue**: BlueのDeployment同期用
**gorilla-sfx-demo-deployment-green**: GreenのDeployment同期用
**gorilla-sfx-demo-virtualservice**: VirtualServiceでトラフィック切り替え用

マニフェストを全て `git push` してmasterにマージしたら、ArgoCDでこれら全てをSyncして展開しよう

## CircleCI設定

`.circleci/config.yml` を以下のように作ってアプリケーションリポジトリにPush

```yaml:.circleci/config.yml
version: 2.1
jobs:
  manifest_pull_request:
    docker:
      # specify the version you desire here
      - image: cimg/base:stable-18.04

    working_directory: ~/

    environment:
      GH_CLI_VERSION: "0.11.1"

    parameters:
      env:
        default: eks
        description: Kubernetes environment (eks, minikube, etc)
        type: string

    steps:
      - kube/install-kubectl
      - kube/install-kubeconfig:
          kubeconfig: KUBECONFIG_DATA

      - aws-cli/install
      - aws-cli/setup:
          aws-access-key-id: AWS_ACCESS_KEY_ID
          aws-secret-access-key: AWS_SECRET_ACCESS_KEY
          aws-region: AWS_REGION

      - run:
          name: install gh
          command: |
            wget https://github.com/cli/cli/releases/download/v${GH_CLI_VERSION}/gh_${GH_CLI_VERSION}_linux_amd64.deb
            sudo dpkg --install ./gh_${GH_CLI_VERSION}_linux_amd64.deb
      - run:
          name: install kustomize
          command: |
            curl -s "https://raw.githubusercontent.com/\
            kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"  | bash
      - run:
          name: clone manifest repository
          command: |
            git config --global user.name ${GITHUB_USERNAME}
            git config --global user.email ${GITHUB_EMAIL}
            git clone https://${GITHUB_USERNAME}:${GITHUB_TOKEN}@github.com/${GITHUB_ORG}/${GITHUB_REPOSITORY}.git
      - run:
          name: get next deployment zone (blue/green)
          command: |
            kubectl get vs gorilla-sfx-demo-virtualservice -o yaml
            echo "export NEXT_DEPLOY=`kubectl get vs gorilla-sfx-demo-virtualservice -o jsonpath='{.spec.http[].match[].headers.x-deploy.exact}'`" >> $BASH_ENV
      - run:
          name: update deployment manifest
          command: |
            cd ~/${GITHUB_REPOSITORY}/${APP_NAME}/<<parameters.env>>/deployment/${NEXT_DEPLOY}
            git checkout -b gitops-${GITHUB_USERNAME}-${CIRCLE_SHA1}
            # Set new image tag
            ~/kustomize edit set image ${AWS_ECR_ACCOUNT_URL}/${AWS_ECR_REPOSITORY}:${CIRCLE_SHA1}
            # Set version as annotation and label
            ~/kustomize edit remove annotation version || true
            ~/kustomize edit add annotation version:${CIRCLE_SHA1}
            ~/kustomize edit remove label version || true
            ~/kustomize edit add label version:`echo ${CIRCLE_SHA1} | cut -b 1-7`
      - run:
          name: update virtualservice manifest
          command: |
            cd ~/${GITHUB_REPOSITORY}/${APP_NAME}/<<parameters.env>>/virtualservice
            # Set virtual service resource for blue/green deployment
            ~/kustomize edit remove resource *
            ~/kustomize edit add resource ${APP_NAME}-istio-virtualservice-${NEXT_DEPLOY}.yaml
      - run:
          name: git commit and push
          command: |
            cd ~/${GITHUB_REPOSITORY}
            git add .
            git commit -m "update ${APP_NAME} manifest for ${CIRCLE_SHA1}"
            git push origin gitops-${GITHUB_USERNAME}-${CIRCLE_SHA1}
      - run:
          name: create pull request
          command: |
            cd ~/${GITHUB_REPOSITORY}
            gh pr create \
              -t "deploy a new image ${APP_NAME}:${CIRCLE_SHA1}" \
              -b "CircleCI auto-generated pull request from https://github.com/${GITHUB_ORG}/${APP_NAME}/commit/${CIRCLE_SHA1}"

orbs:
  aws-ecr: circleci/aws-ecr@6.12.2
  kube: circleci/kubernetes@0.11.1
  aws-cli: circleci/aws-cli@1.2.1

workflows:
  main:
    jobs:
      - aws-ecr/build-and-push-image:
          account-url: AWS_ECR_ACCOUNT_URL
          aws-access-key-id: AWS_ACCESS_KEY_ID
          aws-secret-access-key: AWS_SECRET_ACCESS_KEY
          checkout: true
          create-repo: false
          dockerfile: Dockerfile
          region: AWS_REGION
          repo: ${AWS_ECR_REPOSITORY}
          skip-when-tags-exist: false
          tag: 'latest,${CIRCLE_SHA1}'
          context: my-context
          filters:
            branches:
              only: 
                - master
      - manifest_pull_request:
          env: eks
          context: my-context
          requires:
            - aws-ecr/build-and-push-image
```

### 設定のポイント

#### `get next deployment zone (blue/green)` ステップ

```yaml
      - run:
          name: get next deployment zone (blue/green)
          command: |
            kubectl get vs gorilla-sfx-demo-virtualservice -o yaml
            echo "export NEXT_DEPLOY=`kubectl get vs gorilla-sfx-demo-virtualservice -o jsonpath='{.spec.http[].match[].headers.x-deploy.exact}'`" >> $BASH_ENV
```

Blue/Greenデプロイの場合、今BlueかGreenどちらが動いているかを判別して、反対側を更新する必要があるので、VirtualServiceの `x-deploy` ヘッダがどちらに向いてるかを調べます
x-deployが `green` のときは、本番トラフィックは `blue` に向かってるので、次にデプロイするべきは `green` ということになりますね

で、それを調べるためには `kubectl` が必要で、更にEKSの場合kubeconfigに `aws` CLIを使っているので、 以下のステップが必要になるわけです

```yaml
      - kube/install-kubectl
      - kube/install-kubeconfig:
          kubeconfig: KUBECONFIG_DATA

      - aws-cli/install
      - aws-cli/setup:
          aws-access-key-id: AWS_ACCESS_KEY_ID
          aws-secret-access-key: AWS_SECRET_ACCESS_KEY
          aws-region: AWS_REGION
```

#### `update deployment manifest` ステップ / `update virtualservice manifest` ステップ

```yaml
      - run:
          name: update deployment manifest
          command: |
            cd ~/${GITHUB_REPOSITORY}/${APP_NAME}/<<parameters.env>>/deployment/${NEXT_DEPLOY}
            git checkout -b gitops-${GITHUB_USERNAME}-${CIRCLE_SHA1}
            # Set new image tag
            ~/kustomize edit set image ${AWS_ECR_ACCOUNT_URL}/${AWS_ECR_REPOSITORY}:${CIRCLE_SHA1}
            # Set version as annotation and label
            ~/kustomize edit remove annotation version || true
            ~/kustomize edit add annotation version:${CIRCLE_SHA1}
            ~/kustomize edit remove label version || true
            ~/kustomize edit add label version:`echo ${CIRCLE_SHA1} | cut -b 1-7`
      - run:
          name: update virtualservice manifest
          command: |
            cd ~/${GITHUB_REPOSITORY}/${APP_NAME}/<<parameters.env>>/virtualservice
            # Set virtual service resource for blue/green deployment
            ~/kustomize edit remove resource *
            ~/kustomize edit add resource ${APP_NAME}-istio-virtualservice-${NEXT_DEPLOY}.yaml

```

`kustomize` CLI使って、Deploymentではラベルとかイメージのタグをセットしてます（Annotationはオマケ程度です）

VirtualServiceは `resources` をblueかgreenで切り替えるようにしてます
（わざわざblue用とgree用それぞれのVirtualServiceマニフェスト作っといたのはこのためです）

#### `create pull request` ステップ

```yaml
      - run:
          name: create pull request
          command: |
            cd ~/${GITHUB_REPOSITORY}
            gh pr create \
              -t "deploy a new image ${APP_NAME}:${CIRCLE_SHA1}" \
              -b "CircleCI auto-generated pull request from https://github.com/${GITHUB_ORG}/${APP_NAME}/commit/${CIRCLE_SHA1}"
```

ここではPR作ってるだけですが、 [`gh` CLI](https://github.com/cli/cli) を使ってみました
この記事書いてる時点ではまだベータですけど、CLIで簡単にPR出せるし他のこともいろいろできそうなので試してみたくなって、ついつい、ね・・・

### 出来上がってくるファイル

Deploymentのkustomization.yamlでは、本番トラフィックが来てない方（今回はgreen）のDeploymentでイメージタグやラベルが更新されます

```yaml:eks/deployment/green/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: ************.dkr.ecr.**-****-*.amazonaws.com/gorilla-sfx-demo
  newTag: <GIT_COMMIT_SHA>
commonAnnotations:
  version: <GIT_COMMIT_SHA>
commonLabels:
  version: <GIT_COMMIT_SHA_SHORT>
resources:
- gorilla-sfx-demo-deployment-green.yaml
```

VirtualServiceは、次のデプロイでトラフィックが切り替わる方のVirtualServiceが指定されています

```yaml:eks/virtualservice/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- gorilla-sfx-demo-istio-virtualservice-green.yaml
```

## PRマージしてArgoCD同期

ここで一つどうしようもない問題が出てきます
kustomizeで `commonLabels` に設定すると厄介なことにselectorのラベルにも適用されてしまうんです
Kubernetesではselectorラベルはimmutable（不変）であるため、 `version` を `commonLabels` に指定すると、Deploymentを更新できなくなってしまいます

こんなふうに↓怒られます

```
The Deployment "gorilla-sfx-demo-deployment-green" is invalid: spec.selector: Invalid value: v1.LabelSelector{MatchLabels:map[string]string{"app":"gorilla-sfx-demo", "deploy":"green", "version":"xxxxxxx"}, MatchExpressions:[]v1.LabelSelectorRequirement(nil)}: field is immutable
```

なので、回避策としてDeploymentは手動で強制同期します

```
argocd app sync gorilla-sfx-demo-deployment-green --force
```

これ、なんとかならないですかね？良い方法あればコメントいただけるとありがたいです
なるべくkustomize CLIとkustomization.yamlで完結するようにしたいんだけどなあ

ちなみにこんな↓issue見つけたのですが、未だ解決に至ってないようです
https://github.com/kubernetes-sigs/kustomize/issues/1009

## テストしてVirtualService切り替え

適当なPodで `wget` でテスト

```
kubectl run -it --rm --image=busybox:latest --restart=Never \
--overrides='{ "apiVersion": "v1", "metadata": {"annotations": { "sidecar.istio.io/inject":"false" } } }'  \
busybox -- wget -qO- gorilla-sfx-demo-service:9090/healthz
```

テスト結果がOKだったらVirtualServiceも同期して切り替え

```
argocd app sync gorilla-sfx-demo-virtualservice
```

これで新しくデプロイしたgreenにトラフィックが向かいます

できればこのテストと同期も自動化したいな
良い方法ないかな？


# やってみた感想

ここまでやっといて何ですけど、GitOpsとBlue/Greenデプロイは相性が悪そうですね

- デプロイ設定そのものがVirtualServiceで設定した `x-deploy` ヘッダでの制御に頼らざるを得ない
- マニフェストのディレクトリ構成とかArgoCDの設定が複雑になりがち
- CircleCIでインストールしなきゃいけないものが大杉
- kustomizeの `commonLabels` がイケてない（じゃあ使うなよって話だけど）

ということで、やっぱりSpinnakerの方がいいのかなあ
まあ今回のやり方はあまり実運用向きじゃなさそうということがわかっただけでも収穫ありということにしておくか