はじめに
夏休みの自由研究的なノリで、GitOps的な方法でBlue/Green Deploymentを自動化できるかな、とふと思い立ってやってみました
最終的に完全な自動化はできてはいないものの、現時点でできているものと所感をまとめていきます
まだまだDevOpsもKubernetesもちょっとかじった程度なので、ベストプラクティスとは程遠いのであしからず
あと、文章書くのヘタクソなので説明が不十分だったりしますが、どうか許してください
使ったツール
- GitHub
- CircleCI
- Kustomize
- ArgoCD
- Kubernetes @ EKS
- Istio
ざっくり概要
- GitHubにアプリケーションリポジトリとK8s Manifestリポジトリをそれぞれ作って、アプリケーションのソースコードとDockerfileとビルド用のCircleCIのconfig.ymlを作成してアプリケーションリポジトリにPush
- CircleCIでDockerfileビルドしてECRにPushして、新しいイメージのデプロイ用にDeploymentとVirtualServiceそれぞれのkustomization.yamlを更新して、K8s ManifestリポジトリにPush & Pull Request
- GitHubでPRマージ(これは手動じゃないとダメだね)
- ArgoCDでK8s Manifestを同期して新しいイメージをBlue or Greenにのみデプロイ
- デプロイしたアプリケーションが正しく動いているのを確認できたら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それぞれ別々に作って置いてあります
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
は↓
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
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
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- gorilla-sfx-demo-istio-virtualservice-blue.yaml
VirtualServiceは↓のようにBlue/Greenそれぞれ作成
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
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
かで指定するようにしてあります
この記事 を参考にしました とても勉強になりました
ArgoCD
こんな感じ↓
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
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)
ステップ
- 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を使っているので、 以下のステップが必要になるわけです
- 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
ステップ
- 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
ステップ
- 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 を使ってみました
この記事書いてる時点ではまだベータですけど、CLIで簡単にPR出せるし他のこともいろいろできそうなので試してみたくなって、ついつい、ね・・・
出来上がってくるファイル
Deploymentのkustomization.yamlでは、本番トラフィックが来てない方(今回はgreen)のDeploymentでイメージタグやラベルが更新されます
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が指定されています
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の方がいいのかなあ
まあ今回のやり方はあまり実運用向きじゃなさそうということがわかっただけでも収穫ありということにしておくか