🕌

Gateway APIとNGINX Gateway Fabricを自前のKubernetesクラスタで

2023/11/12に公開

まとめ

今回はおうちの自前Kubernetesクラスタで、2023年10月にv1.0.0リリースを迎えたKubernetes Gateway APIを動かしてみました。

  • 自前のKubernetesクラスタなのでパブリッククラウドサービスのようにServiceのexposeに難あり
    • MetalLB導入でLAN上、クラスタ外からアクセスできるようにした
  • GitOps (Flux)でやりたい
    • TLS証明書の内容をそのままgitに載せずにすむようSOPS暗号化復号化レポジトリを用意した
  • Gateway APIを試したい
    • NGINX Gateway FabricがGateway Controllerとして利用できた
    • Gatewayとアプリでnamespaceおよびgitレポジトリを分けて作ってみた(=管理担当チームが分かれているイメージ)

本題であるGateway APIでどうこうし始める前にセットアップについて紹介していきます。おおまかに以下をカバーしています。

  • セットアップの紹介
    • Kubernetesクラスタ
    • GitLab + Flux
    • metallb
    • SOPS
  • Gateway APIをクラスタへ導入
  • NGINX Gateway FabricをコントローラとしてGateway APIを利用

セットアップの紹介

自前の環境でいろいろ自由に試して遊んでみたいと思い、おうちのLANにDockerやKubernetesで動かしているものをあれこれ持っています。

遊びで好き勝手にKubernetesクラスタをいじり始めると、気軽にあれこれマニフェストを投入してしまいます。そうなると次第にクラスタの状態が分からなくなってしまったので、今はFluxとGitLabを利用したGitOps環境をセットアップしてクラスタの状態を把握できるようにしています。

最初にGitLabを自前の環境で用意したときの投稿が、以下1つ目のリンクです。そしてKubernetesで遊び始めて、自前のGitlabとkubernetesクラスタを、Fluxを通じて管理するGitOps環境を用意したときの投稿が2つ目のリンクとなります。興味があればご覧くださいませ。

特に、今回のポストでは事前準備にあたる部分はおおまかな流れの紹介だけになります。そのような箇所の詳細に関してはそれぞれの公式ドキュメントなどを参照されるか、以下のシリーズの中でも確認できます。

なお、SOPSのセットアップに関しては私の過去の投稿でカバーしていないので、本投稿で詳細を紹介していきます。

Series TOP: Dockerで作るおうちLAN遊び場

Series TOP: おうちでGitOpsシリーズ

Kubernetesクラスタのセットアップ紹介

クラスタの構築はKubernetes公式ドキュメントを参考に普通に作りました。公式ドキュメントをなぞっただけですが、先のリンクにある投稿でも書いたことがあるので、ここではおおまかにやったことをリストだけします。

なお公式ドキュメントですが、こちらのページから追うのが良いと思います。

https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/

  • Raspberry Pi 4b+ setup
    • Raspberry Pi OS Lite (Debian 12), headless, static IP address
    • disable swap
    • enable cgroup memory
    • follow k8s doc to load network related modules and prep sysctl conf
    • install containerd
      • configure it to use systemd cgroup
      • prep systemd service unit
    • install runc
    • install CNI plugin
    • install kubeadm, kubelet, and kubectl
  • and setup a few more amd64 arch nodes similar to the pi
  • Setup a kubernetes cluster
    • kubeadm init on Raspberry Pi and also on another amd64 node as the control plane nodes
      • prepare a kubeadm config file to kubeadm init to create a cluster
        • edit controlPlaneEndpoint and networking.podSubnet
      • copy over the cert/key files to the second node to join as the second control plane node
    • join rest of the nodes to the cluster
  • Apply a network addon manifest
  • the Kubernetes cluster is now ready and running

GitOps - GitLabとFlux

続いて簡単にGitOpsのセットアップもカバーしていきます。以下の手順でセットアップ後、"flux"というレポジトリ内、./clusters/my-cluster/以下に用意したマニフェストがFluxによって自動で拾われ、クラスタに適用されるようになります。

  • Control planeなどにflux CLIをインストール
  • GitLab上でkubernetesクラスタを管理するためのレポジトリを作成
    • create a group "gitops"
    • create a project "flux" under the "gitops" group
    • repo url = https://{gitlab server}/gitops/flux.git
  • gitopsグループの管理メニューより、ownerロールでaccess tokenを払い出し
  • トークン認証を用いてflux CLIでbootstrapする
  • GitOps環境完成

最近、GitLabのaccess tokenの設定可能有効期限が短くなりました。私の以前の投稿では、access tokenの期限を長く設定してセットアップを進めていましたが、今回は期限の短いトークンでbootstrapしています。具体的には以下のコマンドでbootstrapしており、bootstrapプロセス中にfluxとGitLabでやり取りしてdeploy tokenを別途作るようになりました。

❯ flux bootstrap gitlab \
  --deploy-token-auth \
  --hostname={gitlab server} \
  --owner=gitops \
  --repository=flux \
  --path=./clusters/my-cluster \
  --branch=main

► connecting to https://{gitlab server}
► cloning branch "main" from Git repository "https://{gitlab server}/gitops/flux.git"
✔ cloned repository
► generating component manifests
✔ generated component manifests
✔ committed sync manifests to "main" ("fca8daa1a033c5a4e002e0ecfe85bc3b411d4310")
► pushing component manifests to "https://{gitlab server}/gitops/flux.git"
► installing components in "flux-system" namespace
✔ installed components
✔ reconciled components
► checking to reconcile deploy token for source secret
✔ configured deploy token "flux-system-main-flux-system-./clusters/my-cluster" for "https://{gitlab server}/gitops/flux"
► determining if source secret "flux-system/flux-system" exists
► generating source secret
► applying source secret "flux-system/flux-system"
✔ reconciled source secret
► generating sync manifests
✔ generated sync manifests
✔ committed sync manifests to "main" ("4055a74b2f3039d53f30f5e7e3047a0cd82eb18b")
► pushing sync manifests to "https://{gitlab server}/gitops/flux.git"
► applying sync manifests
✔ reconciled sync configuration
◎ waiting for Kustomization "flux-system/flux-system" to be reconciled
✔ Kustomization reconciled successfully
► confirming components are healthy
✔ helm-controller: deployment ready
✔ kustomize-controller: deployment ready
✔ notification-controller: deployment ready
✔ source-controller: deployment ready
✔ all components are healthy

❯ flux version
flux: v2.1.2
helm-controller: v0.36.2
kustomize-controller: v1.1.1
notification-controller: v1.1.0
source-controller: v1.1.2

❯ flux get all
NAME                     	REVISION          	SUSPENDED	READY	MESSAGE
gitrepository/flux-system	main@sha1:4055a74b	False    	True 	stored artifact for revision 'main@sha1:4055a74b'

NAME                     	REVISION          	SUSPENDED	READY	MESSAGE
kustomization/flux-system	main@sha1:4055a74b	False    	True 	Applied revision: main@sha1:4055a74b

MetalLBの導入

次にMetalLBの導入です。

Google Cloud Platform, GCPなどのパブリッククラウドサービスでは、serviceのマニフェストでLoadBalancerを要求するとクラスタ外からアクセスできるIPアドレスがセットされますが、自前のただのkubernetesクラスタにはそういった機能がありません。

MetalLBはLANでkubernetesクラスタ外からクラスタ内のサービスにアクセスできるIPアドレスの割り振り、及びクラスタ外、実LAN環境へアドレスを広報してくれるサービスです。使い方としては、Serviceマニフェストで.spec.typeで"LoadBalancer"として作成すると、MetalLBがIPアドレスプールからingress IPとしてセットしてくれます。

なお以前の投稿ではkustomizationで導入したのですが、今回はhelmを利用します。
そのあたりの手順だけは詳細をカバーしていきます。

おおまかな流れは次の通りです。

  • create and push .sourceignore for flux to ignore helm values.yaml files
  • prepare a directory for metallb in the gitops repo
  • prepare a namespace manifest
  • prepare helm values.yaml file in the directory
  • prepare flux HelmRepository and HelmRelease manifests
  • push to apply
  • create IPAddressPool and L2Advertisement manifests
  • push to apply

Flux向け .sourceignore とディレクトリの用意

まず./clusters/my-cluster以下に、fluxに無視させるファイルを指定するための.sourceignoreファイルと、metallb用のディレクトリを用意します。

# clone if not done so
# git clone https://{gitlab server}/gitops/flux.git
cd flux/clusters/my-cluster

# create .sourceignore file here

# and then create metallb dir and move there
mkdir metallb && cd metallb

雑ですが、作成した.sourceignoreファイルの内容はこちらです。
今後Helmを利用し、コンフィグ用にvalues.yamlファイルをいじる時に、
それらコンフィグファイル自体はfluxによってクラスタへ導入してもらいたい内容ではないため、無視させます。

# clusters/my-cluster/.sourceignore
**/values.yaml
**/*values.yaml

metallb namespace

namespaceマニフェストを用意します。

# clusters/my-cluster/metallb/ns.yaml
---
kind: Namespace
apiVersion: v1
metadata:
  name: metallb-system

Helm repoやvalues.yaml

次に、MetalLBのhelmレポジトリよりvalues.yamlファイルを用意します。
わざわざ2つのファイルdefault-values.yamlvalues.yamlを用意しているのですが、
diff values.yaml default-values.yamlなどで変更内容確認したいという狙いでこのようにしています。

# add the metallb helm repository
helm repo add metallb https://metallb.github.io/metallb

# generate the values.yaml file using the helm repository
helm show values metallb/metallb > default-values.yaml
cp default-values.yaml values.yaml

なお、今回は何もここで設定変更しないです。意味無いですね。

Flux向けのHelmRepositoryとHelmReleaseマニフェスト

Flux向けに外部sourceとしてHelmRepositoryマニフェスト、
そしてクラスタへの導入用にHelmReleaseマニフェストを用意します。

Flux CLIでマニフェストファイルが用意できるのですが、
スクリプトからファイル作成することにします。

スクリプト内でvalues.yamlを参照してマニフェスト生成するよう指定していますが、
先に触れた通り、何も設定変更していないので意味無しです!

# clusters/my-cluster/metallb/generate.sh

#!/bin/bash

# helm source
flux create source helm metallb \
  --url=https://metallb.github.io/metallb \
  --interval=1h0m0s \
  --namespace=metallb-system \
  --export > metallb.yaml

# helm release
flux create helmrelease metallb \
  --source=HelmRepository/metallb \
  --chart=metallb \
  --values=./values.yaml \
  --interval=10m \
  --namespace=metallb-system \
  --target-namespace=metallb-system \
  --export >> metallb.yaml

このスクリプトを走らせるとFlux向けのマニフェストファイルができます。"values"は長いので、こちらは省略した出力です。

chmod u+x generate.sh
./generate.sh

❯ cat metallb.yaml
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
  name: metallb
  namespace: metallb-system
spec:
  interval: 1h0m0s
  url: https://metallb.github.io/metallb
---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: metallb
  namespace: metallb-system
spec:
  chart:
    spec:
      chart: metallb
      reconcileStrategy: ChartVersion
      sourceRef:
        kind: HelmRepository
        name: metallb
  interval: 10m0s
  targetNamespace: metallb-system
  values:
  ...

ここまででPUSH

一旦ここまでで、.sourceignoreやmetallbのns.yamlmetallb.yamlをレポジトリにpushして、fluxによってクラスタへ適用してもらいます。

適用した結果、helm関連のリソースは次のようになっていると思います。
実働リソースに関しては省略しますが、kubectl get all -n metallb-systemでmetallb用にpodなどが作られていることが確認できると思います。

# kubectl
❯ kubectl get helmrepo -n metallb-system
NAME      URL                                 AGE     READY   STATUS
metallb   https://metallb.github.io/metallb   4d20h   True    stored artifact: revision 'sha256:bd1fd831e1b997b949fd46a0f4bdf2525393933b682df5f0914fd5707287e281'
❯ kubectl get hc -n metallb-system
NAME                     CHART     VERSION   SOURCE KIND      SOURCE NAME   AGE     READY   STATUS
metallb-system-metallb   metallb   *         HelmRepository   metallb       4d20h   True    pulled 'metallb' chart with version '0.13.12'
❯ kubectl get hr -n metallb-system
NAME      AGE     READY   STATUS
metallb   4d20h   True    Release reconciliation succeeded

# flux
❯ flux get sources helm -n metallb-system
NAME   	REVISION       	SUSPENDED	READY	MESSAGE
metallb	sha256:bd1fd831	False    	True 	stored artifact: revision 'sha256:bd1fd831'
❯ flux get sources chart -n metallb-system
NAME                  	REVISION	SUSPENDED	READY	MESSAGE
metallb-system-metallb	0.13.12 	False    	True 	pulled 'metallb' chart with version '0.13.12'
❯ flux get helmrelease -n metallb-system
NAME   	REVISION	SUSPENDED	READY	MESSAGE
metallb	0.13.12 	False    	True 	Release reconciliation succeeded

現時点で、レポジトリはこんな感じです。

❯ tree
.
 |-clusters
 | |-my-cluster
 | | |-flux-system
 | | | |-kustomization.yaml
 | | | |-gotk-sync.yaml
 | | | |-gotk-components.yaml
 | | |-metallb
 | | | |-generate.sh
 | | | |-ns.yaml
 | | | |-metallb.yaml
 | | | |-values.yaml
 | | | |-default_values.yaml
 | | |-.sourceignore
 |-README.md
 |-.git

 ❯ which tree
 tree: aliased to find . -not -path "*/\.git/*" -not -path "*/venv/*" | sed -e "s/[^-][^\/]*\// |/g" -e "s/|\([^ ]\)/|-\1/"

その他の設定

あとは追加設定マニフェストを用意してプッシュします。
アドレスに関しては、同じサブネットより未使用かつDHCPでもカバーされていないレンジを好きに指定すれば良いと思います。
ここでは192.168.1.200-192.168.1.220をmetallbが払い出せるレンジとして指定しています。

# clusters/my-cluster/metallb/config.yaml
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: lan-addr-pool
  namespace: metallb-system
spec:
  addresses:
  - 192.168.1.200-192.168.1.220
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: l2adv-addr-pool
  namespace: metallb-system

プッシュすると、それぞれ以下のようにクラスタ内に適用されていることが確認できると思います。

❯ kubectl get IPAddressPool -n metallb-system
NAME            AGE
lan-addr-pool   4d21h
❯ kubectl get L2Advertisement -n metallb-system
NAME              AGE
l2adv-addr-pool   4d21h

以上でMetalLBの導入は完了です。

SOPS

https://fluxcd.io/flux/guides/mozilla-sops/

次にSOPSを導入します。一言でいうと暗号化のためのツールです。

GitOpsでkubernetesクラスタを管理すると、
secretマニフェストの内容もそのままファイルでレポジトリに載ります。
正直に言えば、自分で管理しているGitLabサーバとkubernetesクラスタで
おうちのLAN上で遊んでいるので心配はないのですが、
本来ならばどうすべきかというところも知りたかったのでSOPSを利用したsecret暗号復号環境をセットアップしました。

おおまかな流れは次のとおりです。
また、細かい流れもこれから紹介していきますが、
上のリンク、fluxのドキュメント通りにやっているだけとなります。
私はクラスタのcontrol planeノード、ラズパイで実施しました。

  • install gnupg and sops cli
  • generate rsa 4096 gpg keypair
  • create a secret on flux-system namespace using the generated private key
  • create a new project on GitLab
    • "gitops/my-secrets"
    • generate deploy token with repo-read privilege
  • pass on the deploy token to flux as secret
  • setup the repository as an additional git source for flux to watch
    • configure to use SOPS to decrypt the manifests in the repository
    • configure to use the private key secret generated in the third step
    • set ./clusters/my-cluster as the path for flux to watch
  • create and push the SOPS configuration file on this new repository
  • test
  • import the gpg keypair on any other machine that needs to encrypt manifest files

GNUPGとSOPS CLIのインストール

https://www.gnupg.org/

https://github.com/mozilla/sops

GnuPGとSOPSをインストールする必要があります。

# on rpi, the control plane node

# gnupg, if not installed
sudo apt install gnupg

# sops
mkdir ~/dnld && cd ~/dnld
curl -LO https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.darwin.arm64
sudo mv sops-v3.8.1.darwin.arm64 /usr/local/bin/sops
sudo chmod u+x /usr/local/bin/sops

# check sops
sops --version

キー生成

gpgでキーを生成します。

変数としてKEY_NAMEとKEY_COMMENTをセットしますが、内容は自由にしてください。
私は自分のドメイン名で変数をセットして進めました。

# export env vars
export KEY_NAME="my-cluster.yourdomain.com"
export KEY_COMMENT="flux secrets"

# generate keypair
gpg --batch --full-generate-key <<EOF
%no-protection
Key-Type: 1
Key-Length: 4096
Subkey-Type: 1
Subkey-Length: 4096
Expire-Date: 0
Name-Comment: ${KEY_COMMENT}
Name-Real: ${KEY_NAME}
EOF

プライベートキーをSecretとしてFluxへ渡す

生成したキーペアのプライベートキーをSecretとしてflux-system namespace上に作成します。

まずはプライベートキーのfinger printを取得しますが、
gpg --list-secret-keys "${KEY_NAME}"の出力結果、
以下の例では"123451234512345..."などの英数字文字列がfinger printです。

そしてKEY_FPという変数をその文字列でセットします。

# find the private key finger print
gpg --list-secret-keys "${KEY_NAME}"

# output should be like this

gpg: checking the trustdb
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: depth: 0  valid:   1  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 1u
sec   rsa4096 2023-11-08 [SCEA]
      12345123451234512345
uid           [ultimate] KEY_NAME_here (flux secrets)
ssb   rsa4096 2023-11-08 [SEA]

# set env var using the finger print
export KEY_FP=123451234512345

そして次のコマンドで"sops-gpg"という名称のsecretを、
flux-system namespace上に作成します。

# generate secret
gpg --export-secret-keys --armor "${KEY_FP}" |
kubectl create secret generic sops-gpg \
  --namespace=flux-system \
  --from-file=sops.asc=/dev/stdin

Secret用の別レポジトリの用意

次に、一度ターミナルから離れ、GitLabに移りましょう。

"gitops"グループ下に"my-secrets"という新プロジェクトを作成しましょう。
このレポジトリがflux用のsecret暗号復号専用レポジトリとなります。

deploy tokenからsecretを作成

この新レポジトリのdeploy tokenを作成しましょう。スコープはread_repositoryとします。

ユーザ名と文字列が払い出されますので控えておきます。次のステップで使います。

Secret用レポジトリをFluxの追加gitソースとして設定

まずfluxのために、deploy tokenのsecretを作成します。

# create a flux git secret named "deploy-token-my-secrets"
flux create secret git deploy-token-my-secrets \
  --url=https://{gitlab server}/gitops/my-secrets \
  --namespace=flux-system \
  --username=DEPLOY_TOKEN_USERNAME_HERE \
  --password=DEPLOY_TOKEN_PASSWORD_HERE

次に、"gitops/flux"レポジトリ上に専用ディレクトリを用意して、
flux用のGitRepositoryマニフェスト、Kustomizationマニフェストを用意します。

ここでも、flux CLIでマニフェストファイルを用意するスクリプトを用意しておきます。

どうやってマニフェストファイルを生成したのか、いちいちコマンドを控えておかないと忘れてしまいますので!

# git clone the new "my-secrets" repo
cd clusters/my-cluster
mkdir my-secrets && cd my-secrets

# create "generate.sh" script file
chmod u+x generate.sh
./generate.sh

generate.shファイルの中身は次のとおりです。

1つ目のセクションは
"my-secrets"という名前のGitRepositoryを作成するためのマニフェスト生成コマンドですが、
このレポジトリにアクセスするために、先程作成したsecretを参照しています。

2つ目のセクションは
"my-secrets"という名前のKustomizationを作成するためのマニフェスト生成コマンドですが、
ソースとして上で作成したgit sourceを指定、
pathは./clusters/my-clusterを指定、
そしてdecryptはSOPSを使うこと、
decryptに使うsecretは少し前のステップで作成した"sops-gpg"という名称の
generic secretを指定しています。

cat generate.sh
#!/bin/bash

# git source
flux create source git my-secrets \
  --url=https://{gitlab server}/gitops/my-secrets.git \
  --namespace=flux-system \
  --branch=main \
  --secret-ref=deploy-token-my-secrets \
  --export >> my-secrets.yaml

# kustomization
flux create kustomization my-secrets \
  --namespace=flux-system \
  --source=GitRepository/my-secrets \
  --path="./clusters/my-cluster" \
  --prune=true \
  --interval=10m \
  --decryption-provider=sops \
  --decryption-secret=sops-gpg \
  --export >> my-secrets.yaml

git pushしてしばらくすると、以下のようにgit sourceとkustomizationが作成されていることが確認できると思います。

❯ flux get source git
NAME       	REVISION          	SUSPENDED	READY	MESSAGE
flux-system	main@sha1:05a8e81b	False    	True 	stored artifact for revision 'main@sha1:05a8e81b'
my-secrets 	main@sha1:7d4ffbda	False    	True 	stored artifact for revision 'main@sha1:7d4ffbda'
❯ flux get kustomization
NAME                     	REVISION          	SUSPENDED	READY	MESSAGE
flux-system              	main@sha1:05a8e81b	False    	True 	Applied revision: main@sha1:05a8e81b
my-secrets               	main@sha1:7d4ffbda	False    	True 	Applied revision: main@sha1:7d4ffbda

SOPS設定ファイル

最後のステップとして、secret専用レポジトリとして用意した"gitops/my-secrets"内、
./clusters/my-cluster内に、
.sops.yamlという名称でSOPS設定ファイルを用意します。

ファイル内容は次のとおりで、"pgp: ..."のところには、
上のステップで確認した、生成したGPGプライベートキーのfinger printを記載してください。

# ./clusters/my-cluster/.sops.yaml
creation_rules:
  - path_regex: .*.yaml
    encrypted_regex: ^(data|stringData)$
    pgp: 1234512345...KEY_FP_HERE

完成!そしてテスト!

以上でGPG、SOPSを用いたGitOps用暗号復号レポジトリの完成です。

引き続きfluxのドキュメントどおりに試験してみましょう。

まず"my-secrets"レポジトリをcloneし、
テスト用にsecretマニフェストファイルを作成します。

# cd {wherever you want to git clone "my-secrets" repo}
git clone https://{gitlab server}/gitops/my-secrets.git
cd my-secrets/clusters/my-cluster

# create a test secret file "basic-auth.yaml" here
kubectl -n default create secret generic basic-auth \
--from-literal=user=admin \
--from-literal=password=change-me \
--dry-run=client \
-o yaml > basic-auth.yaml

中身は次のとおりです。dataのvalueがbase64 encodeされている状態です。decodeは容易です。

# see how this secret manifest file looks likecat basic-auth.yaml
apiVersion: v1
data:
  password: Y2hhbmdlLW1l
  user: YWRtaW4=
kind: Secret
metadata:
  creationTimestamp: null
  name: basic-auth
  namespace: default

# can be easily decoded
echo Y2hhbmdlLW1l | base64 --decode

ではこのファイルをsopsで暗号化しましょう。実行コマンドと結果は次のとおりです。

# now, encrypt it
sops --encrypt --in-place basic-auth.yaml

# and check the file again
# values in the data section are all encryptedcat basic-auth.yaml
apiVersion: v1
data:
    password: ENC[AES256_GCM,data:a3McsktUMZwJuec1,iv:6JfvLCswr4rLPEkSWcFwhfcRNz2hqSywfgMjt9pxMgA=,tag:0sXQlOONDTACj5molxHDtA==,type:str]
    user: ENC[AES256_GCM,data:cYfdqW7GnGY=,iv:iPqbulmWXCVjdoyKwx6tGsd04Tnk1tU4ZE61bqNMp+A=,tag:3fgwzlvMJBNt+xLMLONmhw==,type:str]
kind: Secret
metadata:
    creationTimestamp: null
    name: basic-auth
    namespace: default
sops:
    kms: []
    gcp_kms: []
    azure_kv: []
    hc_vault: []
    age: []
    lastmodified: "2023-11-09T04:42:52Z"
    mac: ENC[AES256_GCM,data:F5ydPO6F+vH/V28FEDW8SOLZRaqZdsqLDy8cwkf3DxM1GTsfg77Ww1uS0/BO+xNEJq2y92ldQP95CWdipUG+gK77uv+sMrTHXXUfomB2Fpd1hSOxhoL7MsX+mpKymc6OjFfEqOqYTZ6RyoHQ/3UZQM2lfL76VRlOcVYYMEzjs4s=,iv:u8cKwG3eGssFy0TNTilNlc7khrfQEn0OYN0i/WnM5KI=,tag:Vb9j1/7ztfVetR7hNpxrjg==,type:str]
    pgp:
        - created_at: "2023-11-09T04:42:52Z"
          enc: |-
            -----BEGIN PGP MESSAGE-----

            hQIMA2trso+bhZyNAQ/+K+uI52xtfFtgARXkobixrc+SG99vRKEGCKCRJVjMPLrH
            FF9C5DJBRbOYadF4PNLL8vMl23ebdUkrqCDzd2GJkxRXzs/6TwajVQ/l3hXxuD6K
            KOHnOkPWwo3gyBaG2GAu+rjt/DN9mKa32uhzmxBtcUSGPGRGU2XwRoso0B4WhEbl
            nUo9d5QS4x8njH3kf8PspzUEI9F8pJ2CVsYuAwq0r83Lw2gs6DGZWiT0pTogWsOT
            KLmSFTkPvu6B7aN6fdz1f/IxmVi6aykq6M3v7ChxCZxfyLZMTSOHS5N4bu4V3R4r
            naf7O94/q3Wq8bGMv/Pyq+r0g+dTHVaxzO0dtxUA6pJs/D5gFEyKpnSjlxUQXfFF
            I1sDiu0O/zuyZUSt3x6Kb2FbilsvpdJSJ4YMzpFMkGsepEO9KXEO1y7mIjCMqKsJ
            8Arr83YsnRskmBHU3V5C6nEjwlQCKatuPoW4oJReHyH1NZJ2N64aP+sAHpwNt1pN
            V6Ge/9yP28CpYLvREV2g9k0glLcbi9evClpz1ziyqHdizS4n2VRflSakEFUSyK0r
            43O/1kKr6AxKr1ukewScV3IMt/CNbAAkinPAe63Zqi6DTMNHLZP/Rcr86OsaRLlb
            KtotGHeTh9Az/mvbqeXraT0Gq0Xr9KEZjTXT7nsE7GRi/1d+KYfGwS3hKM9bDu3S
            XAFGXDdU7/Ik3eFRPFtpimeRGVJpjEsF2jrMSKDYHmi6has/U0mmsjQy/HpaD44Q
            wigKrzdDMRkNveMzgaAcbcuC+n7o/dvff9sFepW8MXyBPh5doXY8xnO4YpVa
            =QQiK
            -----END PGP MESSAGE-----
          fp: 8F337122315C3F0A14F9635AE5A930D3A26B2675
    encrypted_regex: ^(data|stringData)$
    version: 3.8.1

これをgit pushするとfluxがsopsで復号化し、
default namespace上にbasic-authという名称のsecretを
作成してくれていることが確認できます。

data以下、valueもkubernetes secret本来のbase64 encode状態になっています。

❯ kubectl get secret basic-auth -o yaml
apiVersion: v1
data:
  password: Y2hhbmdlLW1l
  user: YWRtaW4=
kind: Secret
metadata:
  creationTimestamp: "2023-11-08T14:25:59Z"
  labels:
    kustomize.toolkit.fluxcd.io/name: my-secrets
    kustomize.toolkit.fluxcd.io/namespace: flux-system
  name: basic-auth
  namespace: default
  resourceVersion: "1741119"
  uid: 76c9399b-7670-4eea-a26a-a7646296b797
type: Opaque

Gateway APIを試そう

やっと本題です。なんということでしょう。長かったです。

https://gateway-api.sigs.k8s.io/

それでは、Gateway APIです。listenerやHTTP routingが設定できるものです。Gateway Controllerを併せて導入して利用します。

公式ドキュメントによる一言説明はこちらです。

It is an API (collection of resources) that model service networking in Kubernetes.

登場人物紹介

まずは登場人物を紹介します。

  • Gateway API
    • 公式のインストールマニフェスト/CRDsをクラスタにapplyします
  • Gateway Controller
    • NGINX Gateway Fabric, NGFをコントローラとして導入、利用します
    • 通信の最初の受け口としてのServiceが作られます
    • .spec.type = LoadBalancer
    • 今回のセットアップではMetalLBより実LANからアクセス可能なサブネットよりIPアドレスを割り当てます
    • Serviceの先としては、GatewayおよびGatewayと関連付けられたHTTPRouteの設定に従って通信を処理します
  • Gateway Class
    • 今回はNGFコントローラを導入することでnginxというgateway classが用意されます
    • Gatewayの定義
  • Gateway
    • NGFのserviceがどのように動作すべきかを定義します
    • port, protocol, どういったリソースからアクセス可能であるかの設定など
    • 実際にマニフェストファイルを書くことになります
  • HTTPRoute
    • http通信に関するrouterです
    • 親としてgatewayを指定します
    • upstream/backendとしてアプリケーションのServiceを指定します
    • その他pathなどのルールが指定できます
  • Service
    • アプリケーションへのアクセス口です
    • HTTPRouteの宛先となります
  • Deployment/Pods
    • 実際のアプリケーション

レポジトリリスト

そしてこれから用意するものに関して、こちらがレポジトリの観点からのまとめです。

  • "gitops/flux"
    • the main gitops repository
    • install Kubernetes Gateway API v1 CRDs
    • flux helmrepo and helmrelease to deploy NGINX Gateway Fabric
  • [NEW] "gitops/gateway"
    • external repository to manage "Gateway" manifest
  • [NEW] "gitops/demo-cafe"
    • external repository for a demo application
    • Deployment/Pods
    • Service
    • HTTPRoute
  • "gitops/my-secrets"
    • SOPS enc/dec repo to store secret manifest files
    • stores tls cert/key secret for the gateway to handle https

Namespaceリスト

次にnamespaceのまとめです。

  • nginx-gateway
    • Service listener of the gateway
  • certificate
    • Stores the TLS certificate on "gitops/my-secrets" repo, encrypted
  • gateway
    • Gateway maniest, the definition on how the NGF should behave
    • Uses selector to define which namespace can use the gateway
  • demo-cafe
    • Demo app - deployment and service
    • HTTPRoute

セットアップ手順

セットアップを進めていきますが、おおまかな流れは次のとおりです。

  • create GitLab repositories, "gitops/gateway" and "gitops/demo-cafe"
    • generate read_repo deploy token on each repo
    • create the deploy token secret for each repo
  • add both repositories to flux using git source and kustomization
  • install Kubernetes Gateway API v1.0.0 CRDs
  • install NGINX Gateway Fabric using flux helm repo and helm release
  • create a gateway
  • create demo app deployment and service
  • create HTTPRoute

新プロジェクト作成

"gitops/gateway"と"gitops/demo-cafe"プロジェクトをGitLab上に作成します。my-secretsプロジェクトと同様に、read-repo権限のdeploy tokenをそれぞれ生成し、fluxのgit sourceとkustomizationもセットしましょう。

# new project "gitops/gateway"
project name: gateway
token name: deploy-token-gateway
scopes: read_repository
token username: {TOKEN_USERNAME}
token password: {TOKEN_PASSWORD}

# new project "gitops/demo-cafe"
project name: demo-cafe
token name: deploy-token-demo-cafe
scopes: read_repository
token username: {TOKEN_USERNAME}
token password: {TOKEN_PASSWORD}

deploy tokenのsecret作成

プロジェクトを作り、deploy tokenを生成したら、せっかくなので"gitops/my-secrets"レポジトリで暗号化したマニフェストでsecretを作成しましょう。

# create deploy token secret for each
# on "gitops/my-secrets" repository
cd clusters/my-cluster
flux create secret git deploy-token-gateway \
  --url=https://{gitlab server}/gitops/gateway.git \
  --namespace=flux-system \
  --username={TOKEN_USERNAME} \
  --password={TOKEN_PASSWORD} \
  --export > deploy-token-gateway.yaml

flux create secret git deploy-token-demo-cafe \
  --url=https://{gitlab server}/gitops/demo-cafe.git \
  --namespace=flux-system \
  --username={TOKEN_USERNAME} \
  --password={TOKEN_PASSWORD} \
  --export > deploy-token-demo-cafe.yaml

# encrypt them
sops --encrypt --in-place deploy-token-gateway.yaml
sops --encrypt --in-place deploy-token-demo-cafe.yaml

# commit and push

# confirm the secrets are created
kubectl get secret -n flux-system

fluxに作成したレポジトリを追加

fluxへ追加git sourceとkustomizationをセットします。

なおgatewayのkustomizationに関してはnamespaceを指定しておらず、demo-cafeはkustomizationでtarget-namespaceを"demo-cafe"であると指定しています。ついでに"demo-cafe" namespaceの作成マニフェストもメインの"gitops/flux"レポジトリ上で用意を済ませておきます。

こうすると、"gitops/demo-cafe"レポジトリから別のnamespaceになにか作ろうとしてもできません。

また、"demo-cafe" namespaceには一つgateway-enabled=yesというラベルをセットしておきます。Gateway作成時にまた触れますが、このラベルを利用してどのnamespaceからはgatewayが利用できる・できないを制御します。

# on "gitops/flux" repository
# create a dedicated dir for gateway and demo-cafe
cd clusters/my-cluster
mkdir gateway demo-cafe
cd gateway

# create generate.sh to generate flux manifests using flux CLI
# git source for "gitops/gateway"
cat > generate.sh <<"EOF"
flux create source git gateway \
  --url=https://{gitlab server}/gitops/gateway.git \
  --namespace=flux-system \
  --branch=main \
  --secret-ref=deploy-token-gateway \
  --export >> gateway.yaml

# kustomization
flux create kustomization gateway-kustomization \
  --namespace=flux-system \
  --source=GitRepository/gateway \
  --path="./" \
  --prune=true \
  --interval=1m \
  --export >> gateway.yaml
EOF

chmod u+x generate.sh
./generate.sh

# git commit and push

# and then do the same for "gitops/demo-cafe" repo
cd ../demo-cafe
cat <<"EOF" > generate.sh
# git source
flux create source git demo-cafe \
  --url=https://{gitlab server}/gitops/demo-cafe.git \
  --namespace=flux-system \
  --branch=main \
  --secret-ref=deploy-token-demo-cafe \
  --export > demo-cafe.yaml

# kustomization
flux create kustomization demo-cafe-kustomization \
  --namespace=flux-system \
  --target-namespace=demo-cafe \
  --source=GitRepository/demo-cafe \
  --path="./" \
  --prune=true \
  --interval=1m \
  --export >> demo-cafe.yaml
EOF

# additionally, create the "demo-cafe" namespace
cat <<EOF > ns.yaml
---
apiVersion: v1
kind: Namespace
metadata:
  name: demo-cafe
  labels:
    gateway-enabled: yes
EOF

chmod u+x generate.sh
./generate.sh

# git commit and push

# confirm that the new items are shown for both GitRepository and Kustomization
flux get all

Kubernetes Gateway API v1.0.0 CRD

Gateway APIはメインレポジトリ"gitops/flux"にマニフェストを設置してインストールさせておきます。

# on "gitops/flux" repository
cd clusters/my-cluster/gateway

# download the manifest
curl -LO https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yaml

# git commit and push

インストール完了するとkubectl api-resourcesでこれらが追加されていることが確認できます。

❯ kubectl api-resources | grep gateway.networking
gatewayclasses                    gc                                              gateway.networking.k8s.io/v1             false        GatewayClass
gateways                          gtw                                             gateway.networking.k8s.io/v1             true         Gateway
httproutes                                                                        gateway.networking.k8s.io/v1             true         HTTPRoute
referencegrants                   refgrant                                        gateway.networking.k8s.io/v1beta1        true         ReferenceGrant

NGINX Gateway Fabric

次にgateway controllerとしてNGINX Gateway Fabricを導入します。

# on "gitops/flux" repository
cd clusters/my-cluster
mkdir ngf && cd ngf

# download values.yaml file
helm show values oci://ghcr.io/nginxinc/charts/nginx-gateway-fabric > default_values.yaml
cp default_values.yaml values.yaml

# flux CLI script
cat > generate.sh <<"EOF"
#!/bin/bash

# helm source
flux create source helm nginx-gateway-fabric \
  --url=oci://ghcr.io/nginxinc/charts \
  --interval=1h0m0s \
  --export > nginx-gateway-fabric.yaml

# helm release
flux create helmrelease nginx-gateway-fabric \
  --interval=10m \
  --target-namespace=nginx-gateway \
  --create-target-namespace=true \
  --source=HelmRepository/nginx-gateway-fabric \
  --chart=nginx-gateway-fabric \
  --chart-version=1.0.0 \
  --values=values.yaml \
  --export >> nginx-gateway-fabric.yaml
EOF

chmod u+x generate.sh
./generate.sh

# commit and push

順序が前後してしまいますが、values.yamlは変更せずとも問題なく利用できると思います。ただ私の場合は特定のIPアドレスをgatewayに振りたかったので、以下のような行を追加しています。これはMetalLBに特定のIPアドレスを利用させるためのものです。IPAddressPoolマニフェスト内、.spec.addressesのリストより追加できます。

diff values.yaml default_values.yaml
95,97c81
<   # EDIT: specify metallb static IP address
<   annotations:
<     metallb.universe.tf/loadBalancerIPs: 192.168.1.54
# existing clusters/my-cluster/metallb/config.yaml, IPAddressPool manifest
spec:
  addresses:
  - 192.168.1.200-192.168.1.220
  - 192.168.1.54

Fluxがhelm source, helm chart, helm releaseと処理し終えると、以下のようにpodなどが作られていることが確認できます。

❯ kubectl get all -n nginx-gateway
NAME                                                      READY   STATUS    RESTARTS        AGE
pod/nginx-gateway-nginx-gateway-fabric-8459c977d8-8kx58   2/2     Running   1 (2m25s ago)   2m41s

NAME                                         TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)                                     AGE
service/nginx-gateway-nginx-gateway-fabric   LoadBalancer   10.102.18.210   192.168.1.54   80:31941/TCP,443:32119/TCP   2m41s

NAME                                                 READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx-gateway-nginx-gateway-fabric   1/1     1            1           2m41s

NAME                                                            DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-gateway-nginx-gateway-fabric-8459c977d8   1         1         1       2m41s

Gateway作成

以降の手順ですが、おおよそNGFのGitHubレポジトリ上にある例の通りとなります。主な違いとしては、どのnamespaceに何を置くかが異なり、また先に触れている通りどのnamespaceのリソースがgatewayを利用できるかの制限もかけるため、必要な設定を追加しています。

https://github.com/nginxinc/nginx-gateway-fabric/tree/main/examples/https-termination

Gateway本体

gatewayを"gitops/gateway"レポジトリ上で作成します。

"gateway"というnamespaceと"my-gateway"というgatewayを作成するファイルを用意します。

おおよそ例のままですが、今回の構築ではgatewayは"gateway"というnamespaceにあり、アプリケーションは"demo-cafe"という別namespaceに用意します。Gatewayはデフォルトでは自身と同じnamespaceからのみ利用できます。従って今回はselectorでgateway-enabled=yesとあるnamespaceはgateway利用可能というセットアップにしていきます。

---
kind: Namespace
apiVersion: v1
metadata:
  name: gateway
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
  name: my-gateway
  namespace: gateway
spec:
  gatewayClassName: nginx
  listeners:
  - name: http
    port: 80
    protocol: HTTP
    allowedRoutes:
      namespaces:
        from: Selector
        selector:
          matchLabels:
            gateway-enabled: yes
  - name: https
    port: 443
    protocol: HTTPS
    tls:
      mode: Terminate
      certificateRefs:
        - kind: Secret
          name: tls-secret
          namespace: certificate
    allowedRoutes:
      namespaces:
        from: Selector
        selector:
          matchLabels:
            gateway-enabled: yes

TLS secretの用意

今回用意するgatewayはhttpsも扱う為に、"certificate" namespace上の"tls-secret"を証明書として参照させています。そのsecretは例によって"gitops/my-secrets"上にSOPSで暗号化して作成します。

https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets

---
apiVersion: v1
kind: Namespace
metadata:
  name: certificate
---
apiVersion: v1
kind: Secret
metadata:
  name: tls-secret
  namespace: certificate
type: kubernetes.io/tls
data:
  tls.crt: abcdefgcert...
  tls.key: abcdeftkey...

Secretの.data.tls.crtおよび.data.tls.keyは実際の証明書・鍵情報から作成します。例えばletsencryptのfullchain.pemという証明書のファイルと、privkey.pemという鍵のファイルがある場合は、例えば次のようにしたら良いかと思います。

# at the directory with the tls cert and key files

# first, create the ns portion
cat >tls-secret.yaml <<EOF
---
apiVersion: v1
kind: Namespace
metadata:
  name: certificate
---
EOF

# and then append secret
kubectl create secret tls tls-secret \
  --cert=fullchain.pem \
  --key=privkey.pem \
  --namespace=certificate \
  --dry-run=client \
  -o yaml >> tls-secret.yaml

# and copy over this tls-secret.yaml file 
# to `./clusters/my-cluster/tls-secret.yaml` in "gitops/my-secrets" repo

# encrypt
sops --encrypt --in-place tls-secret.yaml

tls-secret参照許可

Gatewayは"gateway" namespaceにあり、tls-secretは"certificate" namespaceにあります。このgatewayからsecretへの参照許可もマニフェストとして用意する必要があります。

# "gitops/gateway" repo

cat > tls-ref-grant.yaml <<EOF
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: allow-gtw
  namespace: certificate
spec:
  to:
  - group: ""
    kind: Secret
    name: tls-secret
  from:
  - group: gateway.networking.k8s.io
    kind: Gateway
    namespace: gateway
EOF

Gateway完成

すべてfluxに渡して実装してもらうと、以下のようにあれこれできていることが確認できます。

❯ kubectl get secret,refgrant -n certificate
NAME                TYPE                DATA   AGE
secret/tls-secret   kubernetes.io/tls   2      2d5h

NAME                                                 AGE
referencegrant.gateway.networking.k8s.io/allow-gtw   2m58s

❯ kubectl get gtw -n gateway
NAME         CLASS   ADDRESS        PROGRAMMED   AGE
my-gateway   nginx   192.168.1.54   True         3m3s

デモのアプリ

こちらはもう例の通りそのまま使ってみましょう。

元々このマニフェストにはnamespace設定行がないのですが、fluxでkustomizationとしてこの"gitops/demo-cafe"レポジトリをセットした際にtarget-namespace=demo-cafeと指定してあるので、デフォルトでそのnamespace上に作ってくれます。

# "gitops/demo-cafe" repo
# "./cafe.yaml"
curl -LO https://raw.githubusercontent.com/nginxinc/nginx-gateway-fabric/main/examples/https-termination/cafe.yaml

結果は次のとおりです。

❯ kubectl get all -n demo-cafe
NAME                          READY   STATUS    RESTARTS   AGE
pod/coffee-6b8b6d6486-5lw7t   1/1     Running   0          21m
pod/tea-9d8868bb4-9r5bp       1/1     Running   0          21m

NAME             TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
service/coffee   ClusterIP   10.110.249.177   <none>        80/TCP    21m
service/tea      ClusterIP   10.103.212.76    <none>        80/TCP    21m

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/coffee   1/1     1            1           21m
deployment.apps/tea      1/1     1            1           21m

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/coffee-6b8b6d6486   1         1         1       21m
replicaset.apps/tea-9d8868bb4       1         1         1       21m

HTTPRoute作成

こちらも例をダウンロードしてきましょう。今回は少し編集します。

# "gitops/demo-cafe" repo
# download the example httproute manifest
curl -LO https://github.com/nginxinc/nginx-gateway-fabric/raw/main/examples/https-termination/cafe-routes.yaml

編集する点は3点です。Gatewayの名称とhostnameの変更、そしてgateway参照時にnamespace指定です。出来上がり内容としては次のとおりです。

hostnames部分は用意した証明書に合わせてください。

apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: cafe-tls-redirect
spec:
  parentRefs:
  - name: my-gateway
    sectionName: http
    namespace: gateway
  hostnames:
  - "cafe.example.com"
  rules:
  - filters:
    - type: RequestRedirect
      requestRedirect:
        scheme: https
        port: 443
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: coffee
spec:
  parentRefs:
  - name: my-gateway
    sectionName: https
    namespace: gateway
  hostnames:
  - "cafe.example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /coffee
    backendRefs:
    - name: coffee
      port: 80
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: tea
spec:
  parentRefs:
  - name: my-gateway
    sectionName: https
    namespace: gateway
  hostnames:
  - "cafe.example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /tea
    backendRefs:
    - name: tea
      port: 80

Fluxによる処理が完了したら、まずはgatewayの現在の状態を確認します。kubectl get gtw -n gateway -o yamlで見られますが、以下がフィルターした出力です。

❯ kubectl get gtw -n gateway -o=jsonpath='{.items[].status.addresses[].value}{"\n"}{range .items[].status.listeners[*]}{@.name} - {@.attachedRoutes} {@.supportedKinds[].kind}{"\n"}{end}'
192.168.1.54
http - 1 HTTPRoute
https - 2 HTTPRoute

IPアドレスは192.168.1.54、そしてlistenerはhttpとhttpsという名称のものがあります。httpに関してはhttpsへリダイレクトするHTTPRouteが付いており、httpsに関しては"coffee"と"tea"というルールのHTTPRouteが付いています。

完成!!

これでセットアップは完成です。

それではアクセスしてみましょう。以下コマンド例で、cafe.example.com部分は証明書情報、HTTPRouteのhostnameと一致するようにして試してください。またIPアドレスもGatewayのものを指定してください。

# 302 redirect to https
curl --resolve cafe.example.com:80:192.168.1.54 http://cafe.example.com:80/ --include

# 200 for https access to the coffee app 
curl --resolve cafe.example.com:443:192.168.1.54 https://cafe.example.com:443/coffee --include

# and tea app
curl --resolve cafe.example.com:443:192.168.1.54 https://cafe.example.com:443/tea --include

出力例はこのような感じです。自身のサーバアドレスを返してくれていますが、teaアプリのpodに設定されているIPアドレスであることが確認できます。

# accessing tea appcurl --resolve cafe.example.com:443:192.168.1.54 https://cafe.example.com:443/tea
Server address: 10.244.211.139:8080
Server name: tea-9d8868bb4-9r5bp
Date: 11/Nov/2023:15:52:48 +0000
URI: /tea
Request ID: a271a84b8deff6b322f9849e2727e5d2

# tea app pod
❯ kubectl get pods -n demo-cafe -l app=tea -o wide
NAME                  READY   STATUS    RESTARTS   AGE   IP               NODE     NOMINATED NODE   READINESS GATES
tea-9d8868bb4-9r5bp   1/1     Running   0          91m   10.244.211.139   node2   <none>           <none>

以上です!

もう少しあれこれ作って慣れていって、現在Dockerコンテナでリバースプロキシやらせているものをすべて載せ替えられたらいいかなと思っています。

Discussion