Kustomizeは本番環境、ステージング環境、開発環境など環境ごとに異なるYAMLを管理するのに非常に便利です。
しかし、KubernetesビルトインなL7 Loadbalancerの管理APIであるIngressについてはかなり以前から要望があったものの、Kustomizeはビルトインサポートしない方針を取ってきました。
今回はKustomizeの柔軟性のための機能について、Ingressの環境管理を例に整理します。
今回使用したコードはGithubに置いてあります。
Ingressの環境管理のつらみ
IngressはKubernetesビルトインなL7 Loadbalancerの管理リソースです。
Ingress APIはもともとextention/v1beta1
APIとしてKubernetesの初期から存在し、networking.k8s.io/v1beta1
の長い期間を経て、2020年8月リリースのv1.19でやっとGAを迎えnetworking.k8s.io/v1
となりました。
IngressはKubernetesに組み込みのコントローラーがなく、ユーザーが任意のコントローラーをデプロイする必要があります。
そのため、IngressのSpecはホスト名とパスから構成されたIngress Ruleと、そのルールの転送先Serviceを設定するだけの最小限なものとなっています。
加えて、L7ネットワークの機能はネットワークプロダクトごとに実現できることが大きく異なるため、最小限のSpecの代わりにプロダクトごとの設定はIngressのアノテーションで設定することが一般的です。
これにより、各Ingress Controllerのプロダクトはアノテーションで設定できる独自オプションが大量に用意されています。
例えば、Kubernetes SIGとしてホストされているingress-nginxやAWS Load Balancer Controllerのアノテーションのリファレンスにも大量のオプションがあることが見て取れます。
私は普段からAmazon EKSでAWS Load Balancer Controllerを使用していますので、ALBのIngressを例に説明します。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-west-2:ACCOUNTID:certificate/PRODUCTION_ARN
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
alb.ingress.kubernetes.io/ssl-redirect: '443'
name: my-ingress
namespace: default
spec:
rules:
- host: app1.example.com
http:
paths:
- path: /api/v1alpha1/
pathType: Prefix
backend:
service:
name: app1
port:
number: 8080
まずホスト名はapp1.example.com
は環境によって異なる情報です。例えばステージング環境はapp1.stg.example.com
といった別のホスト名にしたいことが多いでしょう。
一方で、パス/api/v1alpha1/
はアプリケーションに非常に依存している情報で、アプリ側のコンテキストルートの設定と合っている必要があります。
なので、Ingressのパス設定含めて十分にテストが必要で、環境依存のあるホスト名のみを環境ごとに変更したいはずです。
次に上記の例ではAWS Load Balancer Controllerの独自の設定としてSSLリダイレクトを設定しており、ALBにアタッチするAmazon ACMの証明書ARNを設定しています。
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-west-2:ACCOUNTID:certificate/PRODUCTION_ARN
というアノテーションです。これも環境ごとに異なります。
使ったことなかったですが、証明書を自動ディスカバリする機能があるようです
https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.3/guide/ingress/cert_discovery/
これに加えて実際に使用する際はALBのHealthCheckを有効化したり、ALBのアクセスログオプションを有効化したりなど、さらに多くのアノテーションが必要になります。多くのアノテーションが必要な中で環境依存がある情報とその他の設定が入り混じっているのは非常につらいです。
Kustomizeの拡張機能
では、KustomizeのビルトインにないからKustomizeが使えないかというとそんなことはありません。Kustomizeには柔軟性のための機能が複数用意されています。
今回は以下の4つのやり方を説明します。
- JSONPatch
- Strategic Merge Patch
- Replacements
- Transformer (KRM function exec plugin)
どの方法でも実現することが可能です。以降の例では全て以下のIngressを生成することとします。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-west-2:ACCOUNTID:certificate/STAGING_ARN # 置換①ステージングのARN
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
alb.ingress.kubernetes.io/ssl-redirect: '443'
name: my-ingress
namespace: default
spec:
rules:
- host: app1.stg.example.com # 置換②ステージングのホスト名
http:
paths:
- path: /api/v1alpha1/
pathType: Prefix
backend:
service:
name: app1
port:
number: 8080
JSONPatch
Kustomizeでは以前よりRFC 6902のJSONPatchをサポートしており、これで生成されたYAMLにJSONPatchの仕様に沿ったパッチを当てることが可能です。
JSONPatchのチュートリアルはまさにIngressで説明されています。
https://github.com/kubernetes-sigs/kustomize/blob/master/examples/jsonpatch.md
先ほどのIngressのホスト名をapp1.stg.example.com
に変更し、アノテーションの証明ARNをステージングの証明書に変更するパッチを作成します。
resources:
- ../base
patchesJson6902:
- target:
group: networking.k8s.io
version: v1
kind: Ingress
name: my-ingress
patch: |-
- op: replace
path: /spec/rules/0/host
value: app1.stg.example.com # ステージングのホスト名
- op: replace
path: /metadata/annotations/alb.ingress.kubernetes.io~1certificate-arn
value: arn:aws:acm:us-west-2:ACCOUNTID:certificate/STAGING_ARN # ステージングの証明書ARN
JSONPatchは配列で複数のオペレーションを記載できるため、1つのリソースに対して複数の置換をする場合も1つのPatchに記載できます。
アノテーションのパスは/が含まれているため、~1
を代わりに使用します。
http://jsonpatch.com/#json-pointer
IngressRuleは配列なので、置換するホスト名を配列のインデックスで指定する必要があるのが少し残念ですが、比較的シンプルに記載できます。
Strategic Merge Patch
Strategic Merge Patchはkubectl apply
でYAMLを適用する際にも使用される、複数のyamlファイルの内容をいい感じにマージする仕様です。こちらもJSONPatchとともに以前よりKustomizeでサポートされてきました。
ただし、Ingressで使用するには少し問題があります。次の例で説明します。
マージする内容のみを切り出したYAMLをstaging_ingress_patch.yaml
として作成します。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-west-2:ACCOUNTID:certificate/STAGING_ARN # ステージングの証明書ARN
name: my-ingress
spec:
rules:
- host: app1.stg.example.com # ステージングのホスト名
http:
paths:
- path: /api/v1alpha1/
pathType: Prefix
backend:
service:
name: app1
port:
number: 8080
そしてkustomization.yamlにStrategic Merge Patchとして先程のファイルを使用するよう記載します。
resources:
- ../base
patchesStrategicMerge:
- staging_ingress_patch.yaml
パッチ対象のstaging_ingress_patch.yaml
を見ると、アノテーションは上書きする証明書ARNのアノテーションのみを記載すれば、この値だけが上書きされていい感じです。
しかし、ホスト名については、非常に冗長な記載になっていることがわかるかと思います。
これは、spec.rules
内の配列の置換になるため、配列が構造体の場合は全てのプロパティを記載する必要があります。
できれば以下のように記載したいですが、これだとhostが上書きされるだけでなくパスのプロパティも上書きされ、無くなってしまいます。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-west-2:ACCOUNTID:certificate/STAGING_ARN # ステージングの証明書ARN
name: my-ingress
spec:
rules:
- host: app1.stg.example.com # ステージングのホスト名
一応実現したいことはできていますが、Ingressのホスト名の置換に実用的とは言えません。
Replacements
YAML内の特定の項目で、別のYAMLの項目を置換する機能です。
以前は似た機能で Vars/VarReference という機能がありましたが現在は非推奨で、代わりにこのReplacementsの利用が推奨されています。
まず、置換する情報をKustomizeでビルドするYAMLに含める必要があります。
この例では、ダミーでingress-config-staging.yaml
というConfigMapを作成し、こちらに置換する情報を記載します。
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-config-staging
data:
host: app1.stg.example.com # ステージングのホスト名
certificate-arn: arn:aws:acm:us-west-2:ACCOUNTID:certificate/STAGING_ARN # ステージングの証明書ARN
次にこのConfigMapをYAMLに含めるためにkustomization.yamlのresources
に追加します。
そしてreplacements
でConfigMapのdata
内の各項目でIngressの値を置換するように設定します。
resources:
- ../base
- ingress-config-staging.yaml
replacements:
# ホスト名の置換
- source:
kind: ConfigMap
name: ingress-config-staging
fieldPath: data.host
targets:
- select:
kind: Ingress
name: my-ingress
fieldPaths:
- spec.rules.0.host
# 証明書ARNの置換
- source:
kind: ConfigMap
name: ingress-config-staging
fieldPath: data.certificate-arn
targets:
- select:
kind: Ingress
name: my-ingress
fieldPaths:
- metadata.annotations.alb\.ingress\.kubernetes\.io/certificate-arn
ホスト名、証明書ARNそれぞれ置換元、置換先のフィールドの設定が必要になり、kustomization.yamlの記載量が増えますが、ConfigMap単体で見ると環境ごとに必要な情報をまとめて記載することができます。
Transformer (KRM function exec plugin)
Kustomize v3で追加された外部コマンドによるYAMLの置換をサポートする機能です。外部コマンドに標準入力から変更するYAMLが渡り、標準出力することでKustomizeのビルド結果にすることができます。
また置換に必要な情報をカスタムリソースの要領でオリジナルのYAMLで記載できます。外部コマンドはこれを元に環境ごとの情報に置換することができます。
今回はIngressの環境ごとの情報を持つオリジナルのYAMLと、その情報を元にIngressを置換するPythonスクリプトを作成します。
今回は以下のようなkind: IngressPatch
というTransformerのYAMLを作成しました。これに置換対象の情報を記載します
apiVersion: kustomize-ingress/v1alpha1 # オリジナル
kind: IngressPatch # オリジナル
metadata:
annotations:
config.kubernetes.io/function: |
exec:
path: ./krmfunc/ingress_patch.py
name: my-ingress
spec:
hosts:
- name: app1.stg.example.com # ステージングのホスト名
target: app1.example.com # 置換対象
annotations:
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-west-2:ACCOUNTID:certificate/STAGING_ARN # ステージングの証明書ARNのアノテーション
kind
, apiVersion
には任意の値が設定できます。このYAMLが自作したものだとわかるものがいいでしょう。
spec
には任意の値を設定でき、今回は置換するホスト名と証明書ARNを記載できるようにします。実際はこのYAMLの設計に設計力が求められそうです。
最後に、このYAMLをTransfomerたらしめる設定として、アノテーションで実行する外部コマンドのパスを指定します。今回はingress_patch.py
というPythonスクリプトに渡すので、そのパスを指定します。
annotations:
config.kubernetes.io/function: |
exec:
path: ./krmfunc/ingress_patch.py
ここで注意点ですが、実行するコマンドのパスは相対パスの場合base
の中に配置する必要があります。
今回はbaseディレクトリ内にkrmfunc
というディレクトリを作成し、ここにスクリプトingress_patch.py
を配置します。
.
├── README.md
├── base
│ ├── ingress.yaml
│ ├── krmfunc
│ │ └── ingress_patch.py
│ └── kustomization.yaml
└── transformers
├── kustomization.yaml
└── staging_ingress_patch.yaml
作成したTransformerのYAMLはkustomization.yamlのtransformers
で読み込むように設定します。
resources:
- ../base
transformers:
- staging_ingress_patch.yaml
これでkustomize build時に外部コマンドが実行されるようになります。
次に、外部コマンドの標準入力に渡ってくる内容ですが、以下のようなResourceList
というYAMLが渡ってきます。
この入力を処理するスクリプトを作成することになります。
apiVersion: config.kubernetes.io/v1alpha1
kind: ResourceList
items:
- apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-west-2:ACCOUNTID:certificate/PRODUCTION_ARN
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
alb.ingress.kubernetes.io/ssl-redirect: "443"
kubernetes.io/ingress.class: alb
kustomize.config.k8s.io/id: |
group: networking.k8s.io
kind: Ingress
name: my-ingress
namespace: default
version: v1
config.kubernetes.io/index: '0'
internal.config.kubernetes.io/index: '0'
internal.config.kubernetes.io/id: '1'
config.k8s.io/id: '1'
name: my-ingress
namespace: default
spec:
rules:
- host: app1.example.com
http:
paths:
- backend:
service:
name: app1
port:
number: 8080
path: /api/v1alpha1/
pathType: Prefix
functionConfig:
apiVersion: kustomize-ingress/v1alpha1
kind: IngressPatch
metadata:
annotations:
config.kubernetes.io/function: |
exec:
path: ./krmfunc/ingress_patch.py
name: my-ingress
spec:
hosts:
- name: app1.stg.example.com
target: app1.example.com
annotations:
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-west-2:ACCOUNTID:certificate/STAGING_ARN
これを見ると、items
にKustomizeでビルドされたYAMLがあり、functionConfig
にTransformerのYAMLであるIngressPatch
があることがわかります。
外部コマンドではfunctionConfigの内容をもとに、itemsのYAMLを置換して標準出力する必要があります。
今回は以下のようなスクリプトをPythonで作成しました。
#!/usr/bin/env python3
from functools import reduce
import os
import sys
import yaml
import logging
def main(args=sys.argv[1:]):
# 標準入力からResourceListのYAMLを読み込み
resource_list = yaml.load(sys.stdin, Loader=yaml.FullLoader)
# functionConfigのspecから置換する情報を取得
target_name = dict_get(resource_list, 'functionConfig.metadata.name')
annotations = dict_get(resource_list, 'functionConfig.spec.annotations', {})
hosts = []
for host in dict_get(resource_list, 'functionConfig.spec.hosts', []):
hosts.append({
'name': dict_get(host, 'name'),
'target': dict_get(host, 'target') })
output = []
for resource in dict_get(resource_list, 'items', []):
if resource['apiVersion'] in ['extensions/v1beta1', 'networking.k8s.io/v1', 'networking.k8s.io/v1beta1'] \
and resource['kind'] == 'Ingress' \
and dict_get(resource, 'metadata.name') == target_name:
# patch annnotation
if len(annotations) > 0:
ann = dict_get(resource, 'metadata.annotations')
if ann is not None:
for key in annotations:
ann[key] = annotations[key]
else:
resource['metadata']['annotations'] = annotations
for host in hosts:
host_name = dict_get(host, 'name')
target = dict_get(host, 'target')
# patch host
for rule in dict_get(resource, 'spec.rules', []):
if target is None or rule['host'] == target:
rule['host'] = host_name
output.append(resource)
# 置換結果を標準出力
sys.stdout.write(yaml.dump_all(output))
def dict_get(dictionary, keys, default=None):
return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)
if __name__ == '__main__':
sys.stdin.reconfigure(encoding='utf-8')
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
main()
これでkustomize buildするとPythonスクリプトが実行され、Ingressのホスト名と証明書ARNが置換されるようになります。
注意点として、kustomize buildする際に外部コマンド実行を明示するオプションが必要となります。
kustomize build transformers/ --enable-alpha-plugins --enable-exec
最後に
今回はKustomizeの4つの機能を実用的にIngressを使用して説明しました。
- JSONPatch
- Strategic Merge Patch
- Replacements
- Transformer (KRM Function plugin)
応用すれば、カスタムリソース等のKustomizeで対応していない様々なリソースを柔軟に管理できます。それぞれ使い所を見極めて活用していきましょう。