Ritolabo
  1. Home
  2. Kubernetes
  3. ArgoCD
  4. Argo CD と Kustomize で dev と prod 環境を作る

Argo CD と Kustomize で dev と prod 環境を作る

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

同じアプリを 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

  1. 前提
  2. 環境を分けるときの問題
  3. Kustomize
  4. base を作る
  5. overlay を書く
    1. dev の overlay
    2. prod の overlay
    3. build して差分を確認する
  6. Secret は別のディレクトリに置く
  7. Application を環境ごとに作る
    1. Application の YAML
    2. apply する
  8. 反映を確認する
  9. 直す場所で変わる範囲が決まる
    1. prod だけ Pod を 3 つにする
    2. 両方の image を上げる
  10. 全体像
  11. 用語メモ
  12. 参考 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 の中身)、Deployment web(nginx。ConfigMap と Secret web-secret をマウントする)、Service web、Ingress web(/ を Service web に渡す)
  • バージョンは Argo CD v3.5.5、kubectl v1.36.1(Kustomize v5.8.1 を同梱)

この記事で使う Argo CD の用語。

言葉意味
SyncGit の YAML をクラスタに apply すること。自分で kubectl apply を実行する代わりに、Argo CD が実行する
Application「GitHub のどのディレクトリ(path)を、どの namespace に入れるか」を書いた Argo CD のデータ(argocd namespace に置く)
argocd-repo-serverArgo 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 か所だけ。

違うところdevprod
namespacedevprod
Pod の数(replicas)12
ページの文言(ConfigMap の index.html)Hello from devHello from prod
Ingress のホスト名dev.localhostprod.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/
overlaybase を読み込んで差分を重ねるディレクトリ。環境ごとに 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 か所。

ファイルdevprod
kustomization.yaml の namespacedevprod
kustomization.yaml の replicasなし(base の 1)- name: web / count: 2
kustomization.yaml の Ingress の patch の valuedev.localhostprod.localhost
configmap.yaml の index.htmlHello from devHello 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=truedestination.namespace の namespace がなければ Argo CD が作る(公式ドキュメント)。これがないと dev namespace がないので Sync が失敗する
automated + selfHeal + prunepush すれば人が操作しなくても反映され、クラスタ側で変えても戻り、Git から消せばクラスタからも消える
finalizersこの Application を削除したとき、Argo CD が入れた Deployment などの中身も一緒に消す印(公式ドキュメント)。付けないと Application だけが消えて中身が残る

残りの 3 つは、name と path と destination.namespace が違うだけ。

Applicationpathdestination.namespaceYAML を作る道具
web-devoverlays/devdevKustomize
web-secret-devsecrets/devdevsops のプラグイン
web-prodoverlays/prodprodKustomize
web-secret-prodsecrets/prodprodsops のプラグイン

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 + patchKustomize(argocd-repo-server の中)web-devdev
overlays/prod/同上(replicas・ホスト名・文言が違う)Kustomizeweb-prodprod
secrets/dev/secret.enc.yamlsops のプラグイン(sidecar)web-secret-devdev
secrets/prod/secret.enc.yamlsops のプラグインweb-secret-prodprod

Application を 4 つ作って、両方の環境が開く状態。Secret の Application と sidecar は図では省いている。(図の角が二重の箱は動くプログラム、それ以外の箱は etcd に保存されたデータ)

図を描画しています…

用語メモ

用語意味
Kustomize共通の YAML に差分を重ねて、完成した YAML を出力するプログラム。kubectl に同梱されていて、kubectl kustomize と kubectl apply -k で使う
kustomization.yamlディレクトリをどう組み立てるかを書くファイル。resources(読み込む元)、namespace、replicas、patches などのキーを書く
base共通の YAML を置くディレクトリ。環境のことは知らない
overlaybase を読み込んで、環境ごとの差分を重ねるディレクトリ
patch変えたい部分だけを書いた YAML(strategic merge)、または「この場所にこの値」の指示(JSON patch)。overlay の patches に書く
finalizerKubernetes のデータを削除するとき、先に片付けをさせるための印。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


[Prev] Argo CD で Secret を暗号化して反映する

Author

rito

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級