背景
先日、私が携わっているプロダクトでGitOps
を導入しました。
一言でGitOpsといっても、ただGitのプッシュをトリガーにデプロイするだけではなく、マニフェストの自動更新フローの構築
やSecretの扱いの変更
などの作業が発生し、一つ一つ調べながらやったので、これらの手順をまとめることにしました。
ちょっとした工夫点もあります。
また、CD(Continuous Delivery)ツールには ArgoCD を、Secretの暗号化には SealedSecrets を採用しました。
この記事では、これらを用いて、以下の画像のようなデプロイ戦略を実現するためのGitOpsの導入手順をまとめていますが、基本的な流れは他のツールを導入する場合でも有用だと思います。GitOpsを導入する上で必要な手順を全て書いているのでまぁまぁ長いですが、ご容赦ください。
※記事中に出てくる環境は、個人のものであり、弊社のプロダクトとは関係ありません。
環境
使用している各ツール等のバージョンは以下の通りです。
- GKE: 1.16.13-gke.401
- Argo CD: v1.7.7
- argocd: v1.7.4
- Sealed Secrets: v0.12.5
- kubeseal: v0.12.5
また、サンプルは以下のリポジトリにコミットしてあります。
- App: istsh/gitops-sample-app
- Manifest: istsh/gitops-sample-manifests
GitOpsでは、アプリケーションとマニフェストのリポジトリを分離することが望ましいです。
デプロイ戦略
大まかに順序をまとめると、以下のようになります。
- Appリポジトリにプッシュ
- CloudBuildのビルドが実行され、Manifestリポジトリにプッシュされる
- ArgoCDによって最新のマニフェストが適用される = デプロイ
それぞれのリポジトリとブランチの対応表です。
トリガーとなる Appリポジトリのブランチ |
CloudBuildによりでプッシュされる Manifestリポジトリのブランチ |
デプロイ環境 |
---|---|---|
main | dev | Dev |
staging | staging | Staging |
production | production | Production |
環境構築
必要な作業を順に解説していきます。
リンクを載せておくので、一部だけ必要な方はここから飛んでください。
GCPでの作業
CloudBuildに、GKEへのアクセス権を付与
- Google Cloud Consoleで、Cloud Buildの
設定
ページを開きます。 - Kubernetes Engine Developerのステータスを
有効
にします。
GitHubのSSH認証鍵をSecretManagerに保存する
-
workingdir
ディレクトリを作成し、そこに新しいGitHub SSH認証鍵を作成します。
(ディレクトリ名は任意) - キーの保存先ファイル名の入力を求められたら、
id_github
と入力します。
※ パスフレーズは空のままにしてください。 - Google Cloud Consoleで、
セキュリティ > シークレット マネージャー
を開きます。 -
+CREATE SECRET
をクリック
・名前はid_github
・先ほど作成したSSH認証鍵を選択
・シークレットを作成
をクリックします。
$ mkdir -p ~/workingdir
$ cd ~/workingdir
$ ssh-keygen -t rsa -b 4096 -C ${GITHUB_EMAIL}
限定公開 GitHub リポジトリへのアクセスもご参考ください。
GitHubに公開SSH認証鍵を追加する
-
Manifest
リポジトリのSettings > Deploy Keys
のAdd deploy key
をクリックする。 - タイトルを入力し、先ほど作成した公開SSH認証鍵を貼り付ける。
-
Allow write acces
にチェックをいれて、キーの追加
をクリックする。
※ この認証鍵を使用して、Manifest
リポジトリにプッシュするので、必ずチェックを入れる。 - ローカルのSSH認証鍵は不要なので、削除する。
CloudBuildのサービスアカウントに、SecretManagerへのアクセス権を付与する
- Google Cloud Consoleで
IAMと管理 > IAM
を開く。 - 表から、末尾が
@cloudbuild.gserviceaccount.com
のメンバーを見つけて、編集アイコンをクリックする。 -
Secret Managerのシークレット アクセサー
のロールを追加し、保存をクリックする。
SealedSecretの導入
GitOpsではマニフェストをGit管理するので、SecretもGitに上げる必要があります。
しかしSecretはbase64エンコードされているだけなので、そのままコミットしてしまっては、そのリポジトリにアクセスできるユーザーであれば、誰でもデコードして見ることができてしまいます。
そこでSealedSecretというツールを用いて暗号化することにしました。
鍵はクラスターで管理されているので、暗号化された状態でコミットすることができます。
kubesealのインストール
まず最初に、kubeseal
をインストールする必要があります。
手順は以下のリンクにも載っているので、よかったらご覧ください。
sealed-secrets - Releases
ここでは、Macでの手順をご紹介します。
$ # kubesealのインストール
$ brew install kubeseal
$ # バージョンの確認
$ kubeseal --version
kubeseal version: v0.12.5
SealedSecret関連のオブジェクトを生成
SealedSecretのCustomResourceDefinitions(CRD)をインストールします。
$ # sealed-secrets-controllerに関連するオブジェクトを生成
$ kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.12.5/controller.yaml
kubesealのヘルプを見ると、namespaceはkube-system
のようです。
$ kubeseal -h
...
--controller-name string Name of sealed-secrets controller. (default "sealed-secrets-controller")
--controller-namespace string Namespace of sealed-secrets controller. (default "kube-system")
...
必要なオブジェクトが生成されているか確認します。
$ # Podの確認
$ kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
...
sealed-secrets-controller-867447b788-7sdhq 1/1 Running 0 8m58s
...
$ # Secretの確認
$ kubectl get secrets -n kube-system
NAME TYPE DATA AGE
...
sealed-secrets-controller-token-xplmz kubernetes.io/service-account-token 3 23h
sealed-secrets-key4wlwz kubernetes.io/tls 2 9m56s
...
証明書の取得
Secretを暗号化するために必要な証明書を取得します。
この証明書は、後にManifestリポジトリでの作業で使用するので、予め移動しておいてください。
また、証明書はコミットする必要がないので、.gitignore
に追加するなどしてコミットできないようにすることをお勧めします。
$ kubeseal --fetch-cert > cert.pem
Manifestリポジトリでの作業
自動更新するyamlのテンプレートを作成
GitOpsでは、Appリポジトリへのプッシュをトリガーに、yamlを自動編集します。
そこで、対象となるyamlのテンプレートを用意します。
ここでは1ファイルだけ紹介します。
deployment.yaml.tpl
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitops-sample-server
spec:
replicas: 2
revisionHistoryLimit: 2
selector:
matchLabels:
app: gitops-sample-server
template:
metadata:
labels:
app: gitops-sample-server
spec:
containers:
- name: gitops-sample-server
image: gcr.io/PROJECT_ID/app:COMMIT_SHA
command: ["/gitops-sample-server"]
imagePullPolicy: Always
resources:
limits:
cpu: 500m
memory: 1000Mi
requests:
cpu: 200m
memory: 500Mi
ports:
- containerPort: 8080
envFrom:
- secretRef:
name: env-secret
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-alpine:v1.14.4
command: ["/bin/sh", "-c", "/usr/local/bin/envoy -c /etc/envoy/envoy.yaml"]
resources:
limits:
cpu: 500m
memory: 500Mi
requests:
cpu: 100m
memory: 100Mi
ports:
- name: app
containerPort: 10000
- name: envoy-admin
containerPort: 8001
volumeMounts:
- name: envoy-volume
mountPath: /etc/envoy
readinessProbe:
httpGet:
scheme: HTTP
path: /healthz
httpHeaders:
- name: x-envoy-livenessprobe
value: healthz
port: 10000
initialDelaySeconds: 3
livenessProbe:
httpGet:
scheme: HTTP
path: /healthz
httpHeaders:
- name: x-envoy-livenessprobe
value: healthz
port: 10000
initialDelaySeconds: 10
volumes:
- name: envoy-volume
configMap:
name: envoy-gitops-sample-server-config
Appリポジトリと連携しているCloudBuildで、このテンプレートからdeployment.yaml
が生成されるのですが、その時に、イメージのパスを自動で更新します。
gcr.io/PROJECT_ID/app:COMMIT_SHA
のPROJECT_ID
は$PROJECT_ID
で、COMMIT_SHA
は$COMMIT_SHA
で更新します。
- $PROJECT_ID: GCPのプロジェクトID
- $COMMIT_SHA: トリガーとなったコミットのコミットID
SealedSecretのyamlを生成する
例として、以下のSecretのyamlを使用します。
apiVersion: v1
data:
MYSQL_HOST: bG9jYWxob3N0
MYSQL_PASSWORD: cGFzc3dvcmQ=
MYSQL_USER: cm9vdA==
kind: Secret
metadata:
creationTimestamp: null
name: sample-secret
namespace: default
このSecretからSealedSecretのyamlを生成します。
cert.pem
は先ほど生成したものを使用します。
$ kubeseal --format=yaml --cert=cert.pem \
< secret.yaml \
> sealed-secret.yaml
生成されたSealedSecretのyamlはこちら。
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
creationTimestamp: null
name: sample-secret
namespace: default
spec:
encryptedData:
MYSQL_HOST: AgBPhntyTvUOYPtjJrdZk0fbVyljZFQExAVKAHYjE0rtTNsqaNXygninGs5CPPALfIf/BgmONK35xepHnH7kqc+WNm2nt4MueAMHAtrHQ/8SCkzXbQGLuiyUUds2Q2xVu/rBAVwGfWaKkwi9Db8Y7u6UOLE2wjgk2hpfqnf0KCgR4+u4q936tszBlqQrzk6+ZCqn7oPkOHI3iHJIi4Myo+Xh51Cx0ItEcq18huAK04tTgPoNDJgjLZZnWt83eOJcdqs6mNuI2khsi+/sXizA/naRRDJjiZat5//v/e73ejPRYLJkLCxmbiqBkZUmzBcTgj9PNvOPd+Be/VlKV0I4oO23e4f4CTp+xsq/hxje1qo38zL5UEalbYvtWVCAQCHyA5zcacpD7Sbl0hDSn4JCmXgktrtmgp4iADw2dGDqj7yGyvgQt18VUyWwqIzdhdih9a9LLvqK+gEf9pHs6p6j+rSlvIhKWM9k+vp44h9GLv5ViXB0jFD6Ig5kuyrne6OPceJgj1kuoGQbGdwCvd6thjNytWwBuPujKo5AK4VepLUUh7q7zZwqBv0yXMwSkmFDpJMTmMuy6W32HZvmJw1m/ajaAqlltDKl2pbyDClRLJ/K1n9QDiOEgIdLX3t9C5tJL1G9b1bMTpTfqqYcQbTQnDBEJJ7du4eYQRoT9EP4vI0eyab8beuvBvwIdPvsPWwY+uf7TzuyX0ve6/8=
MYSQL_PASSWORD: AgAV4O7rOmyqL2JtPZ/XtUIMNW5GPgSMhc8zU0zU7LyaNfu5PPFfZrqmyvV4u0XjiNL0nH0rxCEnbm8ZlYtDQCDTMAZMgQkDSWVq7RGnL0txkE3mtsgzctNvIk54lvoVi7fzsBUHXLWsXu58NgQsGhc/VG25zMIq2ycEKd0nEFoa3EK1EoNsb3gKNS4qGM7hqkS5y7ufOcUR5mVuvXlihrfn9nGcucFsRsMbxsTYWUauM8x0wqxrnw7o74EkA0Gyhu2FI43n4mleubC8pzOEYvurfyfd1UmS7H6j/Cm4VuPtiX/uynvKyb2iVJX3vkeyKnmvhP6AcegfI1UYSD1nLg6taDOUFDdScCRsdI97lF0ehYauinLLp94rRQIXOFWYaSqq8VNbQzApXTDAgSzjazdx17p1IVrspEc7xCzD1Dcn3RiiiSpGnYEbG4rF4pFu8AcKH5j/MSydKN4iJgQ2utOgJ7pUXNeShgRrNf/BI67uus44fgeIXnkG8+LG7/WGhsjnYTRsECZD4g+q2vB3tYeBSFQ7ojUFVN5ChsZgTS+uziIp3jzxu3sWfSt4puGx+BlFp+dxv0Zp1YrRKuje+dBWlCkrf9Sa16SR7DCqKdX+1G0W6rwYYEyXwRW9Y0s6lUZwfpd0kOVcYZABohfH3RkWmxgKjF76xFlyFfHILVtdGJbe1nz1144wAMQeE3jvk/ypeAGy+I8lbg==
MYSQL_USER: AgDjq7qBJR1DDOWapYF4AhtQi1sQgeglSRhu6tZ+JHPTPPdNFBV+zQk3xzKKRldZeeIlXQ5ZquEdc3lMWYKqg5xvL2e21YhgPv4oWADDvsWxWdklZVLDl0SGPQT64v6K+lPVR5Gia6zIpocvv4v3GbMcCXFkFK/b5HeSU6FcxrAhrQ+vdxSWKfkezO8vrS1wJ3MYLgLSwtiRDsibjpYwVTg0sLDcpTlrDk95IEnmcY8bE6qGJ3BO8mUBPhsBnk5yLJO2MZeOofA9IbpUDIpUJSFcaaXc+PK3C+vKxxJ2Ia94Y07UshUOrl72dTrp9aJv1mdkVfzZ7EHQpHh+2k7b2U1nMFVFwK2QV193vJVSQDM21IzIttE4QOxllv0ei48FVMVw3pJ0ABJMKiYta6KAZvKQ5EJkEIVVHtVqRH6plpBgU72dAPVuJAmdlAQ3I844lj1AKwfydylFRJP/olkHegB5Lu3ScwblicPZt4Q+f+IV79YefKR+Bf8zwLB5bd5uFOVx5Jj3dkiSfTN9P60FE6uA1axxyECafqwcgEmxafODtRpP+94rbAUboWCCXTAqjL+KyclCkXg0lKKLDHBmIzhsV3+eDwlApHWfvs1/x2dyW17Rjs7Nqb6vi/xX+YXT0d67TzNEUFfYFABRN9ABN/YSCWsVxIJXvMulg4RpsGD4g5E4GG5gwj3Jjjfr0yEtsu9fS5Zn
template:
metadata:
creationTimestamp: null
name: sample-secret
namespace: default
ご覧の通り、暗号化されるのは、ファイル全体ではなくSecretの各キーに対する値です。
Gitにはこのファイルをコミットします。
Secretのyamlはコミットしないでください。
Appリポジトリでの作業
cloudbuild.yamlの定義
Appリポジトリの特定のブランチのプッシュをトリガーに、CloudBuildを実行します。
各ステップは以下のように定義します。
timeout: 900s
steps:
- id: build and push app docker image
name: gcr.io/kaniko-project/executor:v1.0.0
args:
- --destination=gcr.io/$PROJECT_ID/app:$COMMIT_SHA
- --dockerfile=Dockerfile
- --cache=true
- --cache-ttl=6h
waitFor: ['-']
- id: build and push migration docker image
name: gcr.io/kaniko-project/executor:v1.0.0
args:
- --destination=gcr.io/$PROJECT_ID/migration:$COMMIT_SHA
- --dockerfile=Dockerfile.migration
- --cache=true
- --cache-ttl=6h
waitFor: ['-']
- 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
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:${_GITHUB_ACCOUNT}/${_MANIFEST_REPOSITORY}.git
volumes:
- name: 'ssh'
path: /root/.ssh
- id: switch branch
name: 'gcr.io/cloud-builders/gcloud'
dir: ${_MANIFEST_REPOSITORY}
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 ${_TARGET_BRANCH} && git switch ${_TARGET_BRANCH} && \
git fetch origin ${_DEFAULT_BRANCH} && git merge --ff origin/${_DEFAULT_BRANCH}
volumes:
- name: 'ssh'
path: /root/.ssh
- id: generate manifest for grpc-gateway
name: 'gcr.io/cloud-builders/gcloud'
dir: ${_MANIFEST_REPOSITORY}/${_GRPC_GATEWAY_MANIFEST_DIR}
entrypoint: /bin/sh
args:
- '-c'
- |
set -x && \
sed "s/PROJECT_ID/$PROJECT_ID/g" ${_GRPC_GATEWAY_TEMPLATE_FILE} | \
sed "s/COMMIT_SHA/$COMMIT_SHA/g" >| ${_GRPC_GATEWAY_GENERATE_FILE}
waitFor: ['switch branch']
- id: generate manifest for grpc-server
name: 'gcr.io/cloud-builders/gcloud'
dir: ${_MANIFEST_REPOSITORY}/${_GRPC_SERVER_MANIFEST_DIR}
entrypoint: /bin/sh
args:
- '-c'
- |
set -x && \
sed "s/PROJECT_ID/$PROJECT_ID/g" ${_GRPC_SERVER_TEMPLATE_FILE} | \
sed "s/COMMIT_SHA/$COMMIT_SHA/g" >| ${_GRPC_SERVER_GENERATE_FILE}
waitFor: ['switch branch']
- id: generate manifest for job
name: 'gcr.io/cloud-builders/gcloud'
dir: ${_MANIFEST_REPOSITORY}/${_JOB_MANIFEST_DIR}
entrypoint: /bin/sh
args:
- '-c'
- |
set -x && \
sed "s/PROJECT_ID/$PROJECT_ID/g" ${_JOB_TEMPLATE_FILE} | \
sed "s/COMMIT_SHA/$COMMIT_SHA/g" >| ${_JOB_GENERATE_FILE}
waitFor: ['switch branch']
- id: generate manifest for migration
name: 'gcr.io/cloud-builders/gcloud'
dir: ${_MANIFEST_REPOSITORY}/${_MIGRATION_MANIFEST_DIR}
entrypoint: /bin/sh
args:
- '-c'
- |
set -x && \
sed "s/PROJECT_ID/$PROJECT_ID/g" ${_MIGRATION_TEMPLATE_FILE} | \
sed "s/COMMIT_SHA/$COMMIT_SHA/g" >| ${_MIGRATION_GENERATE_FILE}
waitFor: [ 'switch branch' ]
- id: push generated manifests to ${_TARGET_BRANCH} branch
name: 'gcr.io/cloud-builders/gcloud'
dir: ${_MANIFEST_REPOSITORY}
entrypoint: /bin/sh
args:
- '-c'
- |
set -x && \
git add \
${_GRPC_GATEWAY_MANIFEST_DIR}/${_GRPC_GATEWAY_GENERATE_FILE} \
${_GRPC_SERVER_MANIFEST_DIR}/${_GRPC_SERVER_GENERATE_FILE} \
${_JOB_MANIFEST_DIR}/${_JOB_GENERATE_FILE} \
${_MIGRATION_MANIFEST_DIR}/${_MIGRATION_GENERATE_FILE} && \
git commit \
--author="Cloud Build Service Account <***@cloudbuild.gserviceaccount.com>" \
-m "Deploying images
- gcr.io/$PROJECT_ID/app:$COMMIT_SHA
Built from commit $COMMIT_SHA of ${_APP_REPOSITORY} repository" && \
git push origin ${_TARGET_BRANCH}
volumes:
- name: 'ssh'
path: /root/.ssh
substitutions:
_GITHUB_ACCOUNT: istsh
_APP_REPOSITORY: gitops-sample-app
_MANIFEST_REPOSITORY: gitops-sample-manifests
_GRPC_GATEWAY_MANIFEST_DIR: k8s/ops/base/proxy
_GRPC_GATEWAY_TEMPLATE_FILE: deployment.yaml.tpl
_GRPC_GATEWAY_GENERATE_FILE: deployment.yaml
_GRPC_SERVER_MANIFEST_DIR: k8s/ops/base/server
_GRPC_SERVER_TEMPLATE_FILE: deployment.yaml.tpl
_GRPC_SERVER_GENERATE_FILE: deployment.yaml
_JOB_MANIFEST_DIR: k8s/ops/base/job
_JOB_TEMPLATE_FILE: job.yaml.tpl
_JOB_GENERATE_FILE: job.yaml
_MIGRATION_MANIFEST_DIR: k8s/ops/base/migration
_MIGRATION_TEMPLATE_FILE: job.yaml.tpl
_MIGRATION_GENERATE_FILE: job.yaml
# _DEFAULT_BRANCH: <Defined on the edit page for the Cloud Build trigger>
# _TARGET_BRANCH: <Defined on the edit page for the Cloud Build trigger>
ステップは以下の順序・内容で実行されます
- Dockerfileをビルドし、イメージをContainerRegistryにプッシュ。タグは
$COMMIT_SHA
。 - SecretManagerからGitHub SSH認証鍵を取得し、
/root/.ssh/
に配置。 - GitHubに接続する為の設定。
- Manifestリポジトリをクローン。
-
${_TARGET_BRANCH}
ブランチにスイッチし、${_DEFAULT_BRANCH}
ブランチをマージ。
マニフェストファイルの変更をmainブランチにマージしておけば、このステップで各ブランチに取り込まれます。 - deployment.yaml.tplの内容を一部変更し、deployment.yamlを作成または変更。
・PROJECT_ID ->$PROJECT_ID
・COMMIT_SHA ->$COMMIT_SHA
- 作成または変更したファイルをコミットし、
Manifest
リポジトリにプッシュ
CloudBuildのトリガーを追加する
トリガーとなるブランチや、上記のyamlのパスを設定します。
また、例として、環境変数には以下の値を設定します。
デプロイ戦略の表に対応するブランチ名を設定してください。
設定が完了したら、CloudBuildを実行してみましょう!
成功すると、Manifestリポジトリのブランチに、以下のようなコミットがされているはずです。
Author: Cloud Build Service Account <***@cloudbuild.gserviceaccount.com>
Date: Wed Oct 7 06:56:18 2020 +0000
Deploying images
- gcr.io/gitops-sample-dev/app:0b63ec0d43355956a9972ee7e031b9d180824ffb
Built from commit 0b63ec0d43355956a9972ee7e031b9d180824ffb of gitops-sample-app repository
ArgoCDのセットアップ
インストール
$ # argocdコマンドのインストール
$ brew install argocd
$ kubectl create namespace argocd
$ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v1.7.7/manifests/install.yaml
Service(argocd-server)のタイプをLoadBalancerに変更
$ kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'
$ kubectl get svc argocd-server -n argocd
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
argocd-server LoadBalancer 10.4.5.30 104.198.42.124 80:31795/TCP,443:30291/TCP 104m
argocdコマンドでログイン
初期パスワードはargocd-server
のPod名になっています。
$ kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o name | cut -d'/' -f 2
argocd-server-69b7fcb95d-g8tpt
$ argocd login 104.198.42.124
WARNING: server is not configured with TLS. Proceed (y/n)? y
Username: admin
Password:
'admin' logged in successfully
Context '104.198.42.124' updated
adminユーザーのパスワードを変更
初期パスワードのままだと、Pod名を調べられる人は誰でも管理者権限でログインできてしまうので、最初に変更しておきましょう。
$ argocd account update-password
*** Enter current password:
*** Enter new password:
*** Confirm new password:
Password updated
Context '104.198.42.124' updated
ArgoCDの管理画面にアクセス
$ kubectl port-forward svc/argocd-server -n argocd 8080:443
http://localhost:8080 でアクセスできます。
アプリケーションを登録する
CREATE APPLICATION
をクリックして、アプリケーションを登録します。
全てのPodが起動し、ヘルスチェックが通ると、以下のような画面になります。
Sync Status
を見ると、どの順序でyamlが適用されていったかわかるようになっています。
まとめ
長めの記事になってしまいましたが、GitOpsを導入する上で必要な手順と工夫点をまとめました。
CDツールはいろいろあるので、それぞれのプロダクトに合ったものを採用していただきたいですが、基本的にやることや気を付けることは変わらないと思います。
今後GitOpsを導入するプロダクトのお役に立てれば幸いです。
付録
SealedSecret
鍵の扱い
バックアップ
クラスターにある秘密鍵と証明書は、Secretになっています。
これを削除してしまったり、クラスターそのものを削除してしまった場合に、SealedSecretを復号できなくなってしまいます。
それを防ぐ為に、このSecretは必ずバックアップを取りましょう。
$ # ラベルを指定して、Secretを取得
$ kubectl get secret -n kube-system \
-l sealedsecrets.bitnami.com/sealed-secrets-key=active \
-o yaml > sealed-secrets-key.yaml
生成されたSecretのyamlはこちら。
apiVersion: v1
items:
- apiVersion: v1
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVyakNDQXBhZ0F3SUJBZ0lSQUpsYnRkeUI5bmRqZ1ljZFI1Qkh0dk13RFFZSktvWklodmNOQVFFTEJRQXcKQURBZUZ3MHlNREE1TWpReE5ESXhNRGxhRncwek1EQTVNakl4TkRJeE1EbGFNQUF3Z2dJaU1BMEdDU3FHU0liMwpEUUVCQVFVQUE0SUNEd0F3Z2dJS0FvSUNBUUR5cVJQT3pCSzk4UFZvVlJBYVJJV1gyS3dSdmJIZVBMTnhxY2Q4CmQ3UlM2a3pDZUhINzFFa0E2NzlyQWoxZVUzQ3g0dG8zTlNPWkhqNEFIVk1OS256WTVWYXRFY1F4WEZOWjdRci8Kb2hKTFNuS1VHVG55UCszZU55S29wY3hYeUlibkdlUmh0elVQOTRPdzAyWld5MHEreDdyS29uZk11ZUNMTlNsRwpqeXVKelN3UW9HZUFCbXZYL2NUc29pTHNRdHJoK1Jvc29ZL3VIYU1KNDlYaytiSzRrWFJBeEZPenh3dUZGd0JZCk9NVEZnS3d3WWlaM1l2RytOYjBwWGx6MyttZ1NTVkxRem9CallPT0tMRloxWnBBMXRKYWJIcVhwdUF6b2FJOFUKL0svQjV1cWxLNngrMnhtYUFvcjUxTHZubEdxWER4K2hOZXRDd0NUMVB4Uk5PS0ErNTRiTmVaUjh5alVGMDM5MQpUOXM4a1ZKT2RjN3VTTVdkbytCSUVGM2tJcjdiNThrcThiRTdVcERUOUVybG8va2xhNmZOV2hSaldGR3VHdjgrClJCaUZYZFVxZHZTeVNOTC91VndUVHN2SWpZQ2RqajJsMDZJM2lvQWxsUjQvejNDa21ORW5hQ1oxVmEyclRMc0IKRW5yRHhmRGJsUnhMVHpENWRZVHJ1MTd0MTdadHc3emp1Q3RRWlViM2NzWlJER1EwTzlYcWxZbjZJQzVaRjRxeQpXOFF1b3N6Wlo1cVAwSVFXOUZsK3Nvb3lCbjJhdmNtNnZoMFBvOVZYNGl1U2NLcnBKUkFaNHlIYnRIQlJxTXZOCklFYXBua2dZVk1XMGF0UkN1c2gzTktIaFNCakYyM1ZXMWV4Y01lbFdFWVc4TWhub0d2S1RnZjI5ZW9oMjllVXYKdlZNeVhRSURBUUFCb3lNd0lUQU9CZ05WSFE4QkFmOEVCQU1DQUFFd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBTgpCZ2txaGtpRzl3MEJBUXNGQUFPQ0FnRUFaWkRrblBvWVRrTXltTU01cHdlZ2JLSmRwQlRDRDgvbUR3dUZBS1ZRCkhia0lCMVR1NmVwOVgzbzYxQVF4ZWF3Ukd5NVRvS29pVHNqUTRhcCsrNUJqYmpWZHdBREpZWVd6d2twR0taenAKTDFXV3IyTlQzOXYvR1dPOHl3ZnlUb3BQcjZPN3h5dVRPYk4vb0JjdC9lVVZVV3gzemZyM1hxb3FLZ3IzTDlTLwpuYU5abEc1ZFcvNTNEcEFTSHNEMVgrRWZOQXQ5KzZtMXZpZm5FRmxFQmVWeU02Mm9NQ3NPbFdNVGNEcnBIN0JrCkthSTM4Zno2aTQzd3pJK1ZncDAzVUx0aVVsMnZ4ZVdJbHdFcXVGeUZoTDFkeFBqR0JFNHAraGhCNGR3a3hGVEgKelk1bVFweUhJNWdvK1FvQk1pclZzcFZXeW9HNHo3WUJNdGZSamYweWdWUXlXd2RPbUpZMDF3NVM4ektMMG9tYgpmZU5wbWdFY1NkNWo3SEIydklnVWJGVWxlaERIZkszcXFGR2FsOXYwMXZWckJ5aklnRW9TUGhUOVFlVkh6TXk1CnI4Ump1TkZiUWZidXVWazRCd1hHeW9tYUJUdkdqVXhqMWRHRlRPNjdTMHMvMFBzOVpGM29ONkdZbFRQZDQ2WjYKeHJ5b2FqMXA2V2s3VVhBaGlWd3E3VFhMZEhPcVIxZnl5RFF3eEJLYTArTkI0NHlqcXp6Tmk5TzRuYnovM2hLagpHK1JpcWdFaFFkTUg5MVFkcGVvUUZKeUFmQzVQcnBxUkVoQUY3dG5mUExqOTZRTlJGS0pXeW5MRXFma3cxN0tGCnlvbWFUMmVVZ05Ock11emczdXB4UXNsakp6d2RQLzZxYklmb1Uwa0V1TXo3V3gvQ09WeGtqWDdlckZNT0N2cUIKcWQ0PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS0FJQkFBS0NBZ0VBOHFrVHpzd1N2ZkQxYUZVUUdrU0ZsOWlzRWIyeDNqeXpjYW5IZkhlMFV1cE13bmh4Cis5UkpBT3UvYXdJOVhsTndzZUxhTnpVam1SNCtBQjFURFNwODJPVldyUkhFTVZ4VFdlMEsvNklTUzBweWxCazUKOGovdDNqY2lxS1hNVjhpRzV4bmtZYmMxRC9lRHNOTm1Wc3RLdnNlNnlxSjN6TG5naXpVcFJvOHJpYzBzRUtCbgpnQVpyMS8zRTdLSWk3RUxhNGZrYUxLR1A3aDJqQ2VQVjVQbXl1SkYwUU1SVHM4Y0xoUmNBV0RqRXhZQ3NNR0ltCmQyTHh2alc5S1Y1YzkvcG9Fa2xTME02QVkyRGppaXhXZFdhUU5iU1dteDZsNmJnTTZHaVBGUHl2d2VicXBTdXMKZnRzWm1nS0srZFM3NTVScWx3OGZvVFhyUXNBazlUOFVUVGlnUHVlR3pYbVVmTW8xQmROL2RVL2JQSkZTVG5YTwo3a2pGbmFQZ1NCQmQ1Q0srMitmSkt2R3hPMUtRMC9SSzVhUDVKV3VuelZvVVkxaFJyaHIvUGtRWWhWM1ZLbmIwCnNralMvN2xjRTA3THlJMkFuWTQ5cGRPaU40cUFKWlVlUDg5d3BKalJKMmdtZFZXdHEweTdBUko2dzhYdzI1VWMKUzA4dytYV0U2N3RlN2RlMmJjTzg0N2dyVUdWRzkzTEdVUXhrTkR2VjZwV0oraUF1V1JlS3NsdkVMcUxNMldlYQpqOUNFRnZSWmZyS0tNZ1o5bXIzSnVyNGRENlBWVitJcmtuQ3E2U1VRR2VNaDI3UndVYWpMelNCR3FaNUlHRlRGCnRHclVRcnJJZHpTaDRVZ1l4ZHQxVnRYc1hESHBWaEdGdkRJWjZCcnlrNEg5dlhxSWR2WGxMNzFUTWwwQ0F3RUEKQVFLQ0FnQnNOcTVZcUhVck0wdWRiV0d5OVIvR2FaL0NnWi9TaGF0WVl1aE5QMnl4RlQrSjhnQ1MxMFovSEtMTwphNzlHVTF1TVdLZ0x1cXpYV2I4NGVkdFJvY0x2VHNicWcyUEV4M0Y4UnRPQzBKbnI3WlZQS2pqSEtXOUFpOEh2CkI1RXJESWZzZzRWdmRpNDVvcDJkdTRpRjZENjYrWUw1WHA2aU03cEpHam4vOTFUcExSQWJrZ3pWOFFjaTJVNTYKWUl2R1pNSUx4L1MrTm9aakgrQlhScjFhVVdnOEd0R0hHSVpqUTc2RmFZNkR5VDBtL296TFB0bjhuNmxDcytCWgpsSFZOT09RMFUzS1ZINkh0cjRXSlZ1QnZsbjkxRThXZUEzcmwwV2dnTkpDcVFVMTM0U3grNEEwYXZVYWJnY3JNClF1eFJCOFRJL0x1VVB1RmRjU2FLSDhsRDdwNjIwR1dzcDBXditMMWJlY3N6YkphcHRhMzhsUnNrLy8rUmpCMW8KUWszRXRkNnZWblZxVTF1Q0FDdlYzcWplY25Kd244QkJ1OElyOXdXMHFSR1RQUUhKak5pMzdYVC95RVJGNjJtVApHMEVodmVjQnZqdE5QcVE5OUdGc2l0YUtFeGhSK3FWTWZDZktVQ2dMZmRGYTFvU0x3NFVJY2NrZzZ1Ymxoc2YyCmxIaHBDWlpzL3piQzFxbDJob3NHRHR2YnNzOEIwbTJWdW9HalFuMHF2SEFZeEMrZG9QK3FZTlB6VjRxS1VweFUKV0RXZEZ5QXRvZ0M4bVgyMXZvbHNvU3grRWdrVW5ZV0Z6WmViQXpleTFuZGxXNnVQcmJBQUM0aVQ0VUFRZko1aAo3Q0hIUlo4M2V4ZGo4L1Fnd0FBZHByZXplV3VTNFdyUVJYOVl1WGJudXE3OEFLWEFtUUtDQVFFQSs3a3l1Z3lVCmxRYXlsNFV4Qmp5ajdSTCtTWGErbXYwWkhUQVd5dXhra2FsSHNCYms1U0hNT2RBbWFrR1hJdEhTckxZMEd3Yi8KRys2WC9YQWYybFZtZUlBRUM3WlJiRk9kRHE1TXBZZkdSWmpBZWNRamRJQjQvMWNMNnExT2dRMjZtaFNaalJKegpFQnVocVQwNHJIcmovQ2lyT2lER3dMUDJlejg2aXViOHB6YWtkeEMwMERsLzliTDNPWWJmWG1wL29BcDZYem9pCnExcUpzdzIzTFJSNlNkd29xQmlZdHczZ1hJK3dPV3ozci9uNUFJOHNuSUVwdGNrTjVLYWQ0NnlYOEpzSFYvVjcKNnBqeWU3cWpkRVp3TktGRGlDeWhVVFA0bit1WW5TblhjeTdteTUwNkh0V2M4SHNNekRyclFFeFZsSkY3UitPMQo3ZmhnbG55V0tBcGVDd0tDQVFFQTlzaDJXb2hyOWpVUlk1M0s2NVFpa0JxQkUySCs2aGhSc0pXaGhBbUlwUUlmCmFTVVorNThIL0JmMGx5czdCNTUxU24xVTlWNmxQTFNOQm5lb25OWnIzZ0hwaGNhN2o5Rm45Y2tpdHVTZzhDN3MKSGg1RzEvOVptSDlvRTdPRjRwSzdCT1JVUGVHRUdsbE1QdWpNYVV4a3BpamJhdmFXd2htU2NNN1E4bVFxKzhYUwpIVkpoMW5uUmpwY1B0ZzBwSVFnZUtkTFViMytRZ1RXV3B6RWg1V2NKNjY2OCtNWDBJRENoWUdOOVFINklWNDNmCm9ZZE1reE1WajVoQXQ3dVZ4UzdrQmY4a2o0RndmbmhIc3hta3NpMUp0aTBMUUZJWUhLaEh3RjhRTjYzM1RWNFUKbENNcm9Sb3NzWVpwbGRUR0c3TE5IYm05NFd6MStJUVlIRkxNL0crNk53S0NBUUJIY1FCY25VVnVKa0I1a0d1aApnWVJrdkljL0FseUdVRjdZVWRXbU1nRTQ5ZnBLbDdUTzh4Q3JOOUF2Ui81RSs1ZjNQSjc5TExjcWprVEV3UlhKCk9ZT2puM0dHZ1hBS2RwQ1VvaE5PeDRJV0xvd3lBdUN0SitrdlR0MHE5WlRhTzdOQ0Y2YWN5eWVzNHFxM0JaRVkKSXFpaDRFajRibVQ2UEJrd1VYbWtBbnFpV25mQzh4TGVKZk9USC8vYWE5VHBUd0I1dzMrSGwxQlBvWnFESHRsbQpDZkhMRkpqVlhHVzdUa3ptK0VEamkzR2dtQ2w3WnljYUkrNWFrWDFINzZIUUJDUDdQWVNRQ2pQcEdROTQzamVWCndJZ0g1OXpxd1AvbnRBQlVJdUZsZVlLVVJqTnFobTBBWTAvdlVIMEpXWTk2NkM5Qnd5aGg5dGFqZTJLdVV4MnEKcjRhUkFvSUJBRTQvaWFkekVpaGk4enlPejhTYWw4cnhYSFAwNG9yL1l3ZUdxa3ZmYWdCSUNBV2l5ZlpLbXBHSQpWdm1IcjVQZTNubmIrNUJCamtzTlJKb0VYdVk3NXIvaUExVnppZzB6N0s2Mk05ZWg3cFc1aXd5UnRRelAzbXpJCkdRd0dKREdQTE5XRVFHSE9tOEJ2Q0FuNmJyWUVqdlZRaHlJSFJnNE5aYTEzSmpGMUtWdmpnWmZpZ3pzcUxSUDEKT2ZvVGVqTUxDK1ZmQUJUS2NkdEdUcHA4cmE2N1dSM3RyZVdEZnNDbUtzVVJScW1vZFRxdHRYYldHNldXcTRONApWeXpxd2JaZ0E5VVM3VmpEUmhRVHQwaEduVDRSdGdtWmhyUENVL3JpTUw0Q1puWUJKeVRVNjlsOHZWeTdtK2crCklrUnJ6dFVCZ0tBb1FOaTFYOWlJcWN4eFJLWFdGMGtDZ2dFQkFNQXVjWEs4eGo5VEJuWkdGMW5mR2pOWlFQWkcKb2UvMEdOaWpHVUtUZkN1dXZNcU16Tm1JeVY2VlVFNXhiaWVFNElIbE5EQXROSG5uYTBnNWUxb0cycDdISWo2cApWNG10QVlMYUU0dTg4c3hFMFdKNHVEUVN3aWg3b0Q5UWkrTUN1R1ZZRTY3LzNDbElZdFhaK2dpSWRRY3ZEcDNwCk9WVzlXYjhENk1mSTQ0Ukp4RlFQb2hheU9LazAvOXVuck1vQlAzZXdpcVRLY2EwNFZ2V2dONHVaU3pMQ0wwaDQKeUJiL3AwdUNlcW1XOUhLSWlsdXc0NmN3OVdOZnZPVDZYZHB4b25LNysya2RieTQ4VFk0QVpNa3JEU21JYzkwTApXeFFlb2NlYWkxUlRIRG9Ock44dzB5Qk1WOGNKYjdsRW9yY21xV0tteEZ6cjNoQjRyRktrc3hlZE93ST0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K
kind: Secret
metadata:
creationTimestamp: "2020-09-24T14:21:09Z"
generateName: sealed-secrets-key
labels:
sealedsecrets.bitnami.com/sealed-secrets-key: active
name: sealed-secrets-key4wlwz
namespace: kube-system
resourceVersion: "3522389"
selfLink: /api/v1/namespaces/kube-system/secrets/sealed-secrets-key4wlwz
uid: 1fb9a39d-1b5f-4a4b-a47f-ebd56d02303d
type: kubernetes.io/tls
kind: List
metadata:
resourceVersion: ""
selfLink: ""
local環境用の鍵はそのままコミットしても構いませんが、ステージングや本番などの鍵はそうはいきません。
何もせずにコミットしてしまうと、SealedSecretからSecretを生成できてしますので、SealedSecretを使う意味がなくなってしまいます。
KMSなどを使用して暗号化してからコミットしましょう。
バックアップから証明書を取得
上記のsealed-secrets-key.yaml
から証明書を取得します。
ここでは、yqコマンドを使用します。
$ yq r sealed-secrets-key.yaml 'items.*.data."tls.crt"' | base64 --decode > cert.pem
鍵のリストア
バックアップした鍵をクラスターにリストアします。
ここでは、鍵のSecretがない場合の手順を紹介しますが、残っている場合は事前に削除してください。
$ # 鍵のSecretを生成
$ kubectl apply -f sealed-secrets-key.yaml
$ # Secretを生成しただけでは反映されないので、ControllerのPodを再生成する
$ # このPodはDeploymentによって生成されているので、Podを削除するとすぐに生成される
$ kubectl delete pods -n kube-system -l name=sealed-secrets-controller
$ # ControllerのPodが作り直されたことを確認
$ kubectl get pods -n kube-system -l name=sealed-secrets-controller
NAME READY STATUS RESTARTS AGE
sealed-secrets-controller-867447b788-zhrg4 1/1 Running 0 18s
Secretの内容を更新する
運用していると、Secretを更新したい場面があると思います。
しかし、SecretはSealedSecretとして暗号化されているので、簡単には更新できません。
そこで、SealedSecretを復号してから更新する手順を紹介します。
もちろんクラスターからSecretを取得しても構いません。
$ # バックアップした鍵のyamlを渡して、SealedSecretを復号する
$ kubeseal \
--controller-namespace=kube-system \
--controller-name=sealed-secrets-controller \
< sealed-secret.yaml \
--recovery-unseal \
--recovery-private-key sealed-secrets-key.yaml -o yaml > secret.yaml
Secretの値を更新し、再度SealedSecretのyamlを生成すれば完了です。
ArgoCD
Hookについて
Hookの定義方法
Hookはアノテーションとして定義します。
ここではJobのyamlを紹介します。
apiVersion: batch/v1
kind: Job
metadata:
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
argocd.argoproj.io/sync-wave: "3"
name: migration
labels:
job-name: migration-job
Hookの種類
ArgoCDのHookは5種類あります。
Hook | 説明 |
---|---|
PreSync | マニフェストの適用前に実行される。 |
Sync | すべてのPreSync Hookが成功として完了した後で、マニフェストの適用と同時に実行される。 |
Skip | マニフェストの適用をスキップする。 |
PostSync | すべてのSync Hookが成功として完了し、すべてのリソースが正常な状態になった後に実行される。 |
SyncFail | PreSyncやSync, PostSyncが失敗したときに実行される。(ver1.2以降) |
Hookの削除
Hookを何らかの方法で削除するには、hook-delete-policy
をアノテーションに定義します。
Policyは3種類あります。
Policy | 説明 |
---|---|
HookSucceeded | Hookが成功した後に削除される。 |
HookFailed | Hook失敗した後に削除される。 |
BeforeHookCreation | 次の新しいHookが作成される直前に削除される。(ver1.3以降) |
hook-delete-policy
を定義しないと、次回の実行時にHookが残ったままなので、PreSyncそのものが失敗してしまいます。必ず定義しましょう。
HookSucceeded
は失敗した場合に削除されずに残ってしまいます。HookFailed
も同様なので、BeforeHookCreation
が有用なのではないでしょうか。
同一Hookでの実行順序の指定
sync-wave
をアノテーションに定義します。
これを定義すると、その値の昇順にHookが実行されます。
ただし、注意点が2つあります。
1つ目は、値は数値ではなく文字列で定義すること。
2つ目は、公式ドキュメントにもあるように、sync-waveの初期値は0です。同一Hookでsync-waveを設定する場合に、どれかに設定漏れがあると、それが意図せず先に実行されることがあるので注意してください。
GitHubのSSOで入れるようにする
ArgoCDは初期ユーザーとしては、管理者権限を持つadmin
しかいないようです。
しかし関係者全員にadminのパスワードを共有するのは望ましくないと思います。
そこで、GitHubのSSOで入れるように設定します。Organizationもチェックするので、組織に属するGitHubアカウントなら誰でも入れます。
また、GitHubアカウントでログインする場合は、ReadOnly
にすると良いでしょう。
GitHubの設定
GitHubの Organization > Settings > OAuth Apps
で以下のように登録します。
callback URLは /api/dex/callback
で終わるように入力してください。
ConfigMapを編集する
argocd-cm
という名前のConfigMapを編集します。
data
を以下のように入力して、ClientIDとClientSecretを登録してください。
$ kubectl edit configmap argocd-cm -n argocd
...
data:
dex.config: |
connectors:
- type: github
id: github
name: GitHub
config:
clientID: f227da4f3f260e45410d
clientSecret: c67c6c389f393f11319fc12d49e2059c36e18bea
url: http://104.198.42.124
...
続いて、argocd-rbac-cm
という名前のConfigMapを編集します。
admin以外のユーザーを全てReadOnly
にします。
$ kubectl edit configmap argocd-rbac-cm -n argocd
...
data:
policy.default: role:readonly
...
最後にargocd-server
のPodを再起動します。
argocd-server
はDeploymentになっているので、Podを削除するとすぐに新しいPodが作られます。
$ kubectl delete pods -n argocd -l app.kubernetes.io/name=argocd-server