💪

NGINXの運用をちょっとだけ楽にするKubernetes Controllerを実装してみた

2022/09/26に公開

Motivation

以前[Controller編] Server-Side Applyを試すの記事にて、Server-Side Apply(以降SSA)を試してみました。

https://zenn.dev/junya0530/articles/473f9719ee2803

そこで、今回はSSAを利用して、NGINXの運用をちょっとだけ楽にするControllerを実装してみました。また、Controllerで利用可能なさまざまな機能も色々と盛り込んでいるので、今後のチュートリアル的な出来になったかなと思います。

コードの場所

以下のリポジトリにコードを配置しています。

https://github.com/jnytnai0613/ssa-nginx-controller

機能紹介

以下の作業を自動化します。

  1. 以下のkuberndtes Resourceを作成する。
    • ConfigMap: default.confとindex.html
    • Deployment: NGINXサーバー
    • Service: NGINXへのルーティングを行う
    • Ingress: 外部からのアクセスをServiceにルーティングする
    • Secret: IngressのSSL終端に必要なCA証明書、サーバー証明書、サーバー証明書の秘密鍵が含まれる
    • Secret: Ingressへのアクセスに必要なクライアント証明書と秘密鍵が含まれる
  2. Resource名の変更
  3. 名称変更後、旧Resourceを削除
  4. Resource定義の変更
  5. default.conf変更時の自動再読み込み (inotifywaitで監視)

kuberndtes Resourceの自動作成

まずCustomResource定義を準備します。

apiVersion: ssanginx.jnytnai0613.github.io/v1
kind: SSANginx
metadata:
  name: ssanginx-sample
  namespace: ssa-nginx-controller-system
spec:
  # Deployment定義
  deploymentName: nginx
  deploymentSpec:
    replicas: 3
    strategy:
      type: RollingUpdate
      rollingUpdate:
        maxSurge: 30%
        maxUnavailable: 30%
    template:
      spec:
        containers:
          - name: nginx
            image: nginx:latest
  # ConfigMap定義
  # NGINX設定ファイルdefault.confと静的コンテンツの定義を記載する
  configMapName: nginx
  configMapData:
    default.conf: |
      server {
            listen 80 default_server;
            listen [::]:80 default_server ipv6only=on;
            root /usr/share/nginx/html;
            index index.html index.htm mod-index.html;
          server_name localhost;
      }
    mod-index.html: |
      <!DOCTYPE html>
      <html>
      <head>
      <title>Welcome to nginx!!</title>
      <style>
      html { color-scheme: light dark; }
      body { width: 35em; margin: 0 auto;
      font-family: Tahoma, Verdana, Arial, sans-serif; }
      </style>
      </head>
      <body>
      <h1>Welcome to nginx!!</h1>
      <p>If you see this page, the nginx web server is successfully installed and
      working. Further configuration is required.</p>
      <p>For online documentation and support please refer to
      <a href="http://nginx.org/">nginx.org</a>.<br/>
      Commercial support is available at
      <a href="http://nginx.com/">nginx.com</a>.</p>
      <p><em>Thank you for using nginx.</em></p>
      </body>
      </html>
  # Serviceの定義
  serviceName: nginx
  serviceSpec:
    type: ClusterIP
    ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  # Ingressの定義
  ingressName: nginx
  ingressSpec:
    rules:
    - host: nginx.example.com
      http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: nginx
              port:
                number: 80
  #  Ingress TLSを有効にするかどうか
  ingressSecureEnabled: true

CustomResourceを適用することで、以下のように各Resourceが自動作成されます。

$ kubectl -n ssa-nginx-controller-system get all,cm,secret,ingress
NAME                                                           READY   STATUS    RESTARTS   AGE
pod/nginx-686484b946-bnhgc                                     1/1     Running   0          32m
pod/nginx-686484b946-fhx4c                                     1/1     Running   0          32m
pod/nginx-686484b946-kzgpc                                     1/1     Running   0          32m
pod/ssa-nginx-controller-controller-manager-5658b4d48f-j7kn9   2/2     Running   0          33m

NAME                                                              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/nginx                                                     ClusterIP   10.96.81.98     <none>        80/TCP     32m
service/ssa-nginx-controller-controller-manager-metrics-service   ClusterIP   10.96.44.121    <none>        8443/TCP   33m
service/ssa-nginx-controller-webhook-service                      ClusterIP   10.96.245.134   <none>        443/TCP    33m

NAME                                                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx                                     3/3     3            3           32m
deployment.apps/ssa-nginx-controller-controller-manager   1/1     1            1           33m

NAME                                                                 DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-686484b946                                     3         3         3       32m
replicaset.apps/ssa-nginx-controller-controller-manager-5658b4d48f   1         1         1       33m

NAME                                            DATA   AGE
configmap/kube-root-ca.crt                      1      71m
configmap/nginx                                 2      32m
configmap/ssa-nginx-controller-manager-config   1      33m

NAME                         TYPE                DATA   AGE
secret/ca-secret             Opaque              3      32m
secret/cli-secret            Opaque              2      32m
secret/webhook-server-cert   kubernetes.io/tls   3      33m

NAME                              CLASS   HOSTS               ADDRESS         PORTS     AGE
ingress.networking.k8s.io/nginx   nginx   nginx.example.com   10.96.168.155   80, 443   32m

IngressのSSL終端

CustomResourceの.spec.ingressSecureEnabledフィールドをtrueとすることで、以下のSecretが自動作成されます。

NAME                         TYPE                DATA   AGE
secret/ca-secret             Opaque              3      32m
secret/cli-secret            Opaque              2      32m

また、Ingressにも以下TLS設定が自動で追加されます。

$ kubectl -n ssa-nginx-controller-system get ingress nginx -ojson | jq '.metadata.annotations'
{
  "nginx.ingress.kubernetes.io/auth-tls-secret": "ssa-nginx-controller-system/ca-secret",
  "nginx.ingress.kubernetes.io/auth-tls-verify-client": "on",
  "nginx.ingress.kubernetes.io/rewrite-target": "/"
}
  • .spec.tlsフィールドを追加します。
    Secret ca-secretはControllerによって自動作成されたものが、自動指定されます。
    また、hostsはCustomResourceの.spec.ingressSpec.rules[].hostに指定されている値が自動で使用されます。
$ kubectl  -n ssa-nginx-controller-system get ingress nginx -ojson | jq '.spec.tls'
[
  {
    "hosts": [
      "nginx.example.com"
    ],
    "secretName": "ca-secret"
  }
]

Ingressを利用した接続

まずSecret cli-secretよりクライアント証明書と秘密鍵をダウンロードします。

$ kubectl -n ssa-nginx-controller-system get secrets cli-secret -ojsonpath='{.data.client\.crt}' | base64 -d > client.crt

$ kubectl -n ssa-nginx-controller-system get secrets cli-secret -ojsonpath='{.data.client\.key}' | base64 -d > client.key

その後、以下のコマンドにてアクセス可能です。

curl --key client.key --cert client.crt https://nginx.example.com:443/ --resolve nginx.example.com:443:<IP Address> -kv

なお、各種証明書と秘密鍵は以下goファイルの関数をControllerから呼び出して生成しています。
https://github.com/jnytnai0613/ssa-nginx-controller/blob/main/pkg/pki/certificate.go

curlに-vオプションを与えていますので、certificate.goで指定しているCNが確認できます。

*  subject: C=JP; O=Example Org; OU=Example Org Unit; CN=server
*  start date: Sep 26 03:21:24 2022 GMT
*  expire date: Dec 31 00:00:00 2031 GMT
*  issuer: C=JP; O=Example Org; OU=Example Org Unit; CN=ca

Resourceの名前変更

CustomResourceのXXXXXNameを変更し、再適用することでResourceの名前変更が可能です。
変更した名前で新Resourceが作成され、変更前の旧Resourceは削除される流れとなっています。
ちなみにOwnerReferenceからIndexを貼って、絞り込んでList化して、目的の旧Resourceを探して削除しています。

// add IndexOwnerKey index to deployment object which SSANginx resource owns
if err := mgr.GetFieldIndexer().IndexField(ctx, &appsv1.Deployment{}, constants.IndexOwnerKey, func(obj client.Object) []string {
	// grab the deployment object, extract the owner...
	deployment := obj.(*appsv1.Deployment)
	owner := metav1.GetControllerOf(deployment)
	if owner == nil {
		return nil
	}
	if owner.APIVersion != apiGVStr || owner.Kind != constants.CrKind {
		return nil
	}
	return []string{owner.Name}
}); err != nil {
	return err
}

Resource定義の変更

CustomResourceのXXXXXSpecを変更することで定義変更が可能です。
差分計算はClient-Sideで行い、定義変更を検知しています。
また、差分計算はequality.Semantic.DeepEqual()を利用することで実現しています。

default.conf変更時の自動再読み込み

NGINX Pod内では以下のshellスクリプトが実行され、inotifywaitにてdefault.confの変更を監視しています。

oldcksum=`cksum /etc/nginx/conf.d/default.conf`
inotifywait -e modify,move,create,delete -mr --timefmt '%Y/%m/%d %H:%M:%S' --format '%T' /etc/nginx/conf.d/ | \
while read date time; do
  newcksum=`cksum /etc/nginx/conf.d/default.conf`
  if [ "${newcksum}" != "${oldcksum}" ]; then
    echo "At ${time} on ${date}, config file update detected."
    oldcksum=${newcksum}
    service nginx restart
  fi
done

ConfigMapの変更などでdefault.confの変更が検知されると、以下のようにNGINXプロセスが再起動され、変更が反映されます。

At 03:24:21 on 2022/09/26, config file update detected.
2022/09/26 03:24:21 [notice] 301#301: signal 15 (SIGTERM) received from 315, exiting
2022/09/26 03:24:21 [notice] 302#302: exiting
2022/09/26 03:24:21 [notice] 303#303: exiting
2022/09/26 03:24:21 [notice] 303#303: exit
2022/09/26 03:24:21 [notice] 302#302: exit
2022/09/26 03:24:21 [notice] 301#301: signal 17 (SIGCHLD) received from 302
2022/09/26 03:24:21 [notice] 301#301: worker process 302 exited with code 0
2022/09/26 03:24:21 [notice] 301#301: signal 29 (SIGIO) received
2022/09/26 03:24:21 [notice] 301#301: signal 17 (SIGCHLD) received from 303
2022/09/26 03:24:21 [notice] 301#301: worker process 303 exited with code 0
2022/09/26 03:24:21 [notice] 301#301: exit
2022/09/26 03:24:21 [notice] 317#317: using the "epoll" event method
2022/09/26 03:24:21 [notice] 317#317: nginx/1.23.1
2022/09/26 03:24:21 [notice] 317#317: built by gcc 10.2.1 20210110 (Debian 10.2.1-6)
2022/09/26 03:24:21 [notice] 317#317: OS: Linux 5.10.104-linuxkit
2022/09/26 03:24:21 [notice] 317#317: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2022/09/26 03:24:21 [notice] 318#318: start worker processes
2022/09/26 03:24:21 [notice] 318#318: start worker process 319
2022/09/26 03:24:21 [notice] 318#318: start worker process 320
Restarting nginx: nginx.

最後に

今回実装したControllerには以下を盛り込んでいます。

  • Server-Side Apply
  • Client-Sideでの差分計算
  • Owner Referenceを利用したResouce間の親子関係の構築
  • Indexを利用したResouceのList化
  • Admission Webhook(Validation)

これらはControllerを実装する上でも多用する機能です。
もし、今後Controllerを実装する際には参考になれば幸いです。

Discussion