Argo CD と Kustomize で dev と prod 環境を作る
- 公開日
- カテゴリ:ArgoCD
- タグ:Kubernetes,ArgoCD,Kustomize,学習メモ

同じアプリを dev と prod の 2 つの環境に入れたい。環境ごとに違うのは namespace・Pod の数・ページの文言・ホスト名だけで、Deployment や Service の形は同じ。YAML をディレクトリごとコピーして 2 組にすると、Deployment の形を変えるたびに 2 か所を直すことになる。
この記事では、Kustomize で共通の YAML と環境ごとの差分を分けて Git に置き、Argo CD の Application を環境ごとに作って、http://dev.localhost/ と http://prod.localhost/ で別の環境が開く状態にする。2 つの環境は、手元の kind のクラスタの中の別 namespace として作る。
contents
- 前提
- 環境を分けるときの問題
- Kustomize
- base を作る
- overlay を書く
- Secret は別のディレクトリに置く
- Application を環境ごとに作る
- 反映を確認する
- 直す場所で変わる範囲が決まる
- 全体像
- 用語メモ
- 参考 URL
前提
次の状態から始める。
- クラスタに Argo CD が入っていて、GitHub のリポジトリを登録してある
- Argo CD の
argocd-repo-serverに sops のプラグイン(sidecar)が入っていて、*.enc.yamlがあるディレクトリの Secret を復号できる - Ingress Controller(Contour)が入っていて、Ingress を作ると
http://localhost/から Pod に届く - nginx のページを公開する YAML が 4 つ手元にある。ConfigMap
web-html(index.htmlの中身)、Deploymentweb(nginx。ConfigMap と Secretweb-secretをマウントする)、Serviceweb、Ingressweb(/を Servicewebに渡す) - バージョンは Argo CD v3.5.5、kubectl v1.36.1(Kustomize v5.8.1 を同梱)
この記事で使う Argo CD の用語。
| 言葉 | 意味 |
|---|---|
| Sync | Git の YAML をクラスタに apply すること。自分で kubectl apply を実行する代わりに、Argo CD が実行する |
| Application | 「GitHub のどのディレクトリ(path)を、どの namespace に入れるか」を書いた Argo CD のデータ(argocd namespace に置く) |
argocd-repo-server | Argo CD の中で、Git からファイルを取得して、クラスタに入れる YAML を作るプログラム(argocd namespace の Pod)。クラスタと比べて apply するのは別の argocd-application-controller |
| プラグイン(Config Management Plugin) | argocd-repo-server に「このディレクトリの YAML はこのコマンドで作る」を追加する仕組み。この記事の環境では sops のプラグインが入っていて、*.enc.yaml があるディレクトリを担当している |
| Argo CD の画面 | ブラウザで開く管理画面。kubectl port-forward svc/argocd-server -n argocd 8080:443 を実行して https://localhost:8080 を開く |
環境を分けるときの問題
dev と prod で必要な YAML はほとんど同じで、違いは次の 4 か所だけ。
| 違うところ | dev | prod |
|---|---|---|
| namespace | dev | prod |
Pod の数(replicas) | 1 | 2 |
ページの文言(ConfigMap の index.html) | Hello from dev | Hello from prod |
| Ingress のホスト名 | dev.localhost | prod.localhost |
環境ごとに YAML 一式をディレクトリに分けて持つと、同じ内容のファイルが環境の数だけできる。Deployment に Volume のマウントを 1 つ追加するだけでも全部の環境の deployment.yaml を直すことになり、どれかを忘れると環境ごとに違うものが動く。環境が増えるほど直す場所も増える。
Kustomize
Kustomize は、共通の YAML に環境ごとの差分を重ねて、完成した YAML を出力するプログラム。
- 入力は普通の YAML。テンプレートではなく、共通の YAML はそのまま
kubectl apply -fできる形のまま置く - 出力も普通の YAML。出力するだけで apply はしない。apply するのは kubectl か Argo CD
- 差分は「どの YAML の、どこを、何に変えるか」を別のファイルに書く。共通の YAML には触らない
Kustomize を別途インストールする必要はない。kubectl と Argo CD のどちらにも、Kustomize のプログラムが最初から同梱されている。
| どこ | 何が使うか | 確かめ方 |
|---|---|---|
| 手元の kubectl の中 | kubectl kustomize <ディレクトリ>(出力を見る)、kubectl apply -k <ディレクトリ>(出力を apply する) | kubectl version --client の Kustomize Version |
クラスタの argocd-repo-server の中 | Argo CD が Git を読んで YAML を作るときに使う | kubectl -n argocd exec deploy/argocd-repo-server -c argocd-repo-server -- kustomize version |
Argo CD 側の設定は何も変えない。Application の path のディレクトリに kustomization.yaml があれば、Argo CD は Kustomize で YAML を作る(公式ドキュメント)。Application の YAML に Kustomize のことは書かない。
この記事で使う Kustomize の言葉は 3 つ。
| 言葉 | 意味 | この記事でのディレクトリ |
|---|---|---|
kustomization.yaml | 「このディレクトリをどう組み立てるか」を書くファイル。これがあるディレクトリを Kustomize は読める | base/、overlays/dev/、overlays/prod/ に 1 つずつ |
| base | 共通の YAML を置くディレクトリ。環境のことは何も知らない | base/ |
| overlay | base を読み込んで差分を重ねるディレクトリ。環境ごとに 1 つ。差分の書き方の 1 つが patch(変えたい部分だけ書いた YAML) | overlays/dev/、overlays/prod/ |
base を作る
リポジトリに base/ ディレクトリを作り、手元にある 4 つの YAML をそのまま置く。中身は変えない。
base/
├── configmap.yaml
├── deployment.yaml
├── service.yaml
└── ingress.yaml
base/kustomization.yaml を追加し、Kustomize に読ませるファイルを並べる。書いていないファイルは読まれない。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- configmap.yaml
- deployment.yaml
- service.yaml
- ingress.yaml
kubectl kustomize <ディレクトリ> で、Kustomize が組み立てた YAML を標準出力に出す。クラスタには何もしない。
$ kubectl kustomize base
apiVersion: v1
data:
index.html: |
<h1>Hello from Git</h1>
kind: ConfigMap
metadata:
name: web-html
---
apiVersion: v1
kind: Service
metadata:
name: web
(省略)
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 1
(省略)
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
(省略)
4 ファイルが --- でつながって 1 本になった。差分をまだ書いていないので、中身は元のファイルと同じ。キーの並びが変わっているのは Kustomize がアルファベット順に並べ直すためで、意味は変わらない。
overlay を書く
overlays/dev/ と overlays/prod/ を作り、base との差分 4 か所だけを書く。
dev の overlay
overlays/dev/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev
resources:
- ../../base
patches:
- path: configmap.yaml
- target:
kind: Ingress
name: web
patch: |-
- op: add
path: /spec/rules/0/host
value: dev.localhost
| キー | 何をするか |
|---|---|
namespace: dev | 出力する全部の YAML に metadata.namespace: dev を追加する |
resources: - ../../base | 読み込む元。ファイルではなく、kustomization.yaml があるディレクトリを書ける。これが base |
patches: - path: configmap.yaml | 同じディレクトリの configmap.yaml を patch として重ねる。patch は「変えたい部分だけ書いた YAML」で、kind と name が一致するものに重なる |
patches: - target: ... patch: - op: add ... | もう 1 つの patch の書き方。「この kind・name の YAML の、この場所に、この値を追加する」。/spec/rules/0/host は spec.rules の 0 番目に host を追加する、の意味。リストの中の 1 項目を変えるときはこちらを使う(上の書き方だと rules のリストが丸ごと置き換わり、backend が消える) |
overlays/dev/configmap.yaml(patch):
apiVersion: v1
kind: ConfigMap
metadata:
name: web-html
data:
index.html: |
<h1>Hello from dev</h1>
base の configmap.yaml との違いは index.html の文言だけ。patch には変える部分しか書かないので、ほかのキーは base のものが残る。replicas は dev では書かず、base の 1 のままにする。
prod の overlay
overlays/prod/ も同じ 2 ファイルで、dev との違いは次の 4 か所。
| ファイル | dev | prod |
|---|---|---|
kustomization.yaml の namespace | dev | prod |
kustomization.yaml の replicas | なし(base の 1) | - name: web / count: 2 |
kustomization.yaml の Ingress の patch の value | dev.localhost | prod.localhost |
configmap.yaml の index.html | Hello from dev | Hello from prod |
replicas は kustomization.yaml のキーで、patch を書かずに Deployment の replicas だけを変えられる。
replicas:
- name: web
count: 2
build して差分を確認する
$ kubectl kustomize overlays/dev
apiVersion: v1
data:
index.html: |
<h1>Hello from dev</h1>
kind: ConfigMap
metadata:
name: web-html
namespace: dev
---
(Service、Deployment は省略。どれも namespace: dev が付く)
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
namespace: dev
spec:
rules:
- host: dev.localhost
http:
paths:
- backend:
service:
name: web
port:
number: 80
path: /
pathType: Prefix
4 つとも namespace: dev が付き、ConfigMap の文言が変わり、Ingress の rules[0] に host が追加された。backend はそのまま残っている。
dev と prod の出力を diff で比べると、違いは最初の表の 4 か所だけだとわかる。
$ diff <(kubectl kustomize overlays/dev) <(kubectl kustomize overlays/prod)
4c4
< <h1>Hello from dev</h1>
---
> <h1>Hello from prod</h1>
8c8
< namespace: dev
---
> namespace: prod
(namespace の差分 3 か所は省略)
28c28
< replicas: 1
---
> replicas: 2
63c63
< - host: dev.localhost
---
> - host: prod.localhost
base の deployment.yaml は 1 つしかないのに、replicas: 1 の出力と replicas: 2 の出力ができた。
Secret は別のディレクトリに置く
Deployment がマウントする Secret web-secret は、sops で暗号化して Git に置く。ただし、暗号化した secret.enc.yaml は Kustomize のディレクトリには置けない。
argocd-repo-server は、Application の path のディレクトリを読むとき、クラスタに入れる YAML を作る道具をディレクトリごとに 1 つだけ選ぶ(Argo CD のソース util/app/discovery/discovery.go)。順番は、プラグインの discover に当たるか → kustomization.yaml があれば Kustomize、Chart.yaml があれば Helm → どちらもなければディレクトリの YAML をそのまま読む。
overlays/dev/ に secret.enc.yaml を置くと、sops のプラグインが選ばれて Kustomize は動かない。プラグインは *.yaml を順に出力するので、kustomization.yaml が「kind: Kustomization のデータ」としてそのまま apply されようとして失敗する。逆に base/ に置いて kustomization.yaml に並べると、Kustomize は暗号文のまま出力し、パスワードが ENC[...] という文字列になる。
sops のプラグインと Kustomize は同じディレクトリに同居できないので、この記事では次のようにする。
- 実務では、ksops(Kustomize の中から sops を呼ぶ仕組み。
argocd-repo-serverに ksops を入れ、argocd-cmにkustomize.buildOptions: --enable-alpha-plugins --enable-execを設定する)を使えば、Secret も overlay の中に置けて、Application は環境ごとに 1 つで済む。クラスタ側で Secret を作る方式(External Secrets Operator、Sealed Secrets)なら、Kustomize のディレクトリに置けるデータ(参照だけ)になる - この記事では、すでに入っている sops のプラグインをそのまま使い、Argo CD 側は何も変えない。Secret は
secrets/dev/、secrets/prod/に分け、環境ごとに Application をもう 1 つ書く。Argo CD 本体に手を入れる手順を増やさないためと、「Argo CD はディレクトリごとに道具を 1 つ選ぶ」が Application のpathの違いとして見えるため
dev と prod で違うパスワードの Secret を平文で書く。secrets/dev/secret.yaml(prod は password が hello-prod):
apiVersion: v1
kind: Secret
metadata:
name: web-secret
type: Opaque
stringData:
password: hello-dev
sops -e で暗号化し、平文は消す。暗号化の設定(.sops.yaml)はリポジトリのルートに置いてあるものがそのまま効く。
$ sops -e secrets/dev/secret.yaml > secrets/dev/secret.enc.yaml
$ sops -e secrets/prod/secret.yaml > secrets/prod/secret.enc.yaml
$ rm secrets/dev/secret.yaml secrets/prod/secret.yaml
Secret の YAML には namespace を書かない。Application の destination.namespace に入る。
ここまでで、リポジトリは次の形になる。push しておく。
.sops.yaml sops の設定(公開鍵)
base/ 共通の YAML。Application は指さない
├── kustomization.yaml
├── configmap.yaml
├── deployment.yaml
├── service.yaml
└── ingress.yaml
overlays/
├── dev/ Application web-dev が指す(Kustomize)
│ ├── kustomization.yaml
│ └── configmap.yaml patch
└── prod/ Application web-prod が指す(Kustomize)
├── kustomization.yaml
└── configmap.yaml patch
secrets/
├── dev/
│ └── secret.enc.yaml Application web-secret-dev が指す(sops のプラグイン)
└── prod/
└── secret.enc.yaml Application web-secret-prod が指す(sops のプラグイン)
push しても、この時点ではクラスタで何も起きない。これらのディレクトリを指す Application がまだないため。
Application を環境ごとに作る
Application の YAML
環境ごとに Application を 2 つ(アプリと Secret)、合わせて 4 つ作る。Application 1 つにつき 1 ファイルで、applications/ ディレクトリに置く。dev のアプリの Application はこれ。
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: web-dev
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/example-user/argocd-study.git
targetRevision: HEAD
path: overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: dev
syncPolicy:
automated:
selfHeal: true
prune: true
syncOptions:
- CreateNamespace=true
| 項目 | 何をするか |
|---|---|
path: overlays/dev | ディレクトリに kustomization.yaml があるので、Argo CD は Kustomize で YAML を作る。Kustomize を使うことは Application に書かない |
destination.namespace: dev | 作った YAML を入れる namespace。overlay の namespace: dev と同じ値にする |
syncOptions: - CreateNamespace=true | destination.namespace の namespace がなければ Argo CD が作る(公式ドキュメント)。これがないと dev namespace がないので Sync が失敗する |
automated + selfHeal + prune | push すれば人が操作しなくても反映され、クラスタ側で変えても戻り、Git から消せばクラスタからも消える |
finalizers | この Application を削除したとき、Argo CD が入れた Deployment などの中身も一緒に消す印(公式ドキュメント)。付けないと Application だけが消えて中身が残る |
残りの 3 つは、name と path と destination.namespace が違うだけ。
| Application | path | destination.namespace | YAML を作る道具 |
|---|---|---|---|
web-dev | overlays/dev | dev | Kustomize |
web-secret-dev | secrets/dev | dev | sops のプラグイン |
web-prod | overlays/prod | prod | Kustomize |
web-secret-prod | secrets/prod | prod | sops のプラグイン |
Application の YAML は Git に置かず、手元から kubectl apply で入れる。アプリの YAML は Git に置いて反映させるのに対して、これは本来の GitOps から外れている点。Argo CD には、Application を入れる Application を 1 つ作って Git で管理するやり方(App of Apps、ApplicationSet)があるが、この記事ではアプリの環境分けに絞り、そこまでは踏み込まない。
apply する
$ kubectl apply -f applications/
application.argoproj.io/web-dev created
application.argoproj.io/web-prod created
application.argoproj.io/web-secret-dev created
application.argoproj.io/web-secret-prod created
$ kubectl get applications -n argocd
NAME SYNC STATUS HEALTH STATUS
web-dev Synced Healthy
web-prod Synced Healthy
web-secret-dev Synced Healthy
web-secret-prod Synced Healthy
automated なので、apply して数秒〜十数秒で 4 つとも Synced / Healthy になる。Deployment は Secret web-secret をマウントするので、Secret の Application の Sync が後になった分だけ、Pod は Secret ができるまで ContainerCreating で待ってから起動する。
反映を確認する
namespace が 2 つでき、それぞれに同じ名前のものが入っている。
$ kubectl get all,ingress,secret -n dev
NAME READY STATUS RESTARTS AGE
pod/web-7dd4f56c74-sxbjm 1/1 Running 0 2m12s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/web ClusterIP 10.96.133.212 <none> 80/TCP 2m12s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/web 1/1 1 1 2m12s
NAME DESIRED CURRENT READY AGE
replicaset.apps/web-7dd4f56c74 1 1 1 2m12s
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress.networking.k8s.io/web <none> dev.localhost 127.0.0.1 80 2m12s
NAME TYPE DATA AGE
secret/web-secret Opaque 1 2m2s
$ kubectl get all,ingress,secret -n prod
NAME READY STATUS RESTARTS AGE
pod/web-7dd4f56c74-567cf 1/1 Running 0 2m29s
pod/web-7dd4f56c74-db4bq 1/1 Running 0 2m28s
(省略。deployment.apps/web は 2/2、Ingress の HOSTS は prod.localhost)
devは Pod 1 つ、prodは Pod 2 つ。Deployment の名前はどちらもwebで、namespace が違うのでぶつからない- Ingress の
HOSTSがdev.localhost/prod.localhostになっている - Secret
web-secretはweb-secret-devが入れたもの。同じ namespace に、2 つの Application が別々に入れたデータが並んでいる
URL で環境が分かれていることを確認する。dev.localhost のようなホスト名は、macOS では何も設定しなくても 127.0.0.1 に解決される。Envoy はリクエストの Host ヘッダーを見て、dev.localhost なら dev namespace の Ingress、prod.localhost なら prod namespace の Ingress の先に渡す。
$ curl http://dev.localhost/
<h1>Hello from dev</h1>
$ curl http://prod.localhost/
<h1>Hello from prod</h1>
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost/
404
ホスト名なしの http://localhost/ は、どちらの Ingress にも当たらないので 404 になる。
直す場所で変わる範囲が決まる
overlay を直すと片方の環境だけが変わり、base を直すと両方が変わる。
prod だけ Pod を 3 つにする
overlays/prod/kustomization.yaml の count: 2 を count: 3 にして push する。変更は 1 行。
$ kubectl get pods -n prod
NAME READY STATUS RESTARTS AGE
web-7dd4f56c74-567cf 1/1 Running 0 11m
web-7dd4f56c74-db4bq 1/1 Running 0 11m
web-7dd4f56c74-tp2bl 1/1 Running 0 21s
$ kubectl get pods -n dev
NAME READY STATUS RESTARTS AGE
web-7dd4f56c74-sxbjm 1/1 Running 0 11m
prod の Pod が 3 つになり、dev は 1 つのまま。base/deployment.yaml には触っていない。
両方の image を上げる
base/deployment.yaml の image: nginx:1.27 を nginx:1.28 にして push する。
$ kubectl get deployment web -n dev -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
web 1/1 1 1 13m nginx nginx:1.28 app=web
$ kubectl get deployment web -n prod -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
web 3/3 3 3 13m nginx nginx:1.28 app=web
dev と prod の両方で Pod が入れ替わった。直したファイルは 1 つで、最初に困っていた「2 か所直す」がなくなった。
ただし、実務では image のタグを base に置かない。base を直すと dev と prod が同時に変わり、dev で確かめてから prod に上げる、ができないため。タグは overlay ごとに kustomization.yaml の images キーで指定し、dev の overlay を先に上げて確認してから prod の overlay を上げる。
images:
- name: nginx
newTag: "1.28"
base に置くのは、全環境で必ず同じであるべきもの(Deployment の形、Volume のマウント、Service のポートなど)。この記事で base の image を直したのは、base を直すと両方に効くことを見るため。
全体像
| Git のディレクトリ | 中身 | YAML を作る道具 | Application | 入る namespace |
|---|---|---|---|---|
base/ | 共通の YAML 4 つ + kustomization.yaml | (overlay から読まれる) | なし | なし |
overlays/dev/ | kustomization.yaml + patch | Kustomize(argocd-repo-server の中) | web-dev | dev |
overlays/prod/ | 同上(replicas・ホスト名・文言が違う) | Kustomize | web-prod | prod |
secrets/dev/ | secret.enc.yaml | sops のプラグイン(sidecar) | web-secret-dev | dev |
secrets/prod/ | secret.enc.yaml | sops のプラグイン | web-secret-prod | prod |
Application を 4 つ作って、両方の環境が開く状態。Secret の Application と sidecar は図では省いている。(図の角が二重の箱は動くプログラム、それ以外の箱は etcd に保存されたデータ)
用語メモ
| 用語 | 意味 |
|---|---|
| Kustomize | 共通の YAML に差分を重ねて、完成した YAML を出力するプログラム。kubectl に同梱されていて、kubectl kustomize と kubectl apply -k で使う |
kustomization.yaml | ディレクトリをどう組み立てるかを書くファイル。resources(読み込む元)、namespace、replicas、patches などのキーを書く |
| base | 共通の YAML を置くディレクトリ。環境のことは知らない |
| overlay | base を読み込んで、環境ごとの差分を重ねるディレクトリ |
| patch | 変えたい部分だけを書いた YAML(strategic merge)、または「この場所にこの値」の指示(JSON patch)。overlay の patches に書く |
| finalizer | Kubernetes のデータを削除するとき、先に片付けをさせるための印。Argo CD の Application に resources-finalizer.argocd.argoproj.io を付けると、削除時に中身も消える |
まとめ
- Kustomize は、共通の YAML(base)に環境ごとの差分(overlay)を重ねて YAML を出力する。入力も出力も普通の YAML で、
kubectl kustomizeで手元でも同じ結果を見られる - Argo CD は、Application の
pathにkustomization.yamlがあれば Kustomize で YAML を作る。Application には Kustomize のことを書かず、環境ごとに Application を 1 つ作ってdestination.namespaceとCreateNamespace=trueで namespace を分ける - Argo CD はディレクトリごとに YAML を作る道具を 1 つ選ぶ。sops のプラグインと Kustomize は同居できないので、この記事では Secret を別のディレクトリ・別の Application にした。実務では ksops で overlay の中に置ける
参考 URL
- [official] Kustomize - Argo CD
- [official] Tool Detection - Argo CD
- [official] Sync Options - Argo CD
- [official] App Deletion - Argo CD
- [official] Kustomizeを使ったKubernetesオブジェクトの宣言的管理 | Kubernetes
- [official] The Kustomization File - Kustomize
- [official] KSOPS - GitHub
[Prev] Argo CD で Secret を暗号化して反映する

