ExternalDNSとcert-managerでお家KubernetesとHTTPSな通信をする
はじめに
以前 kubespray を使ってお家 kubernetes を作成しました(前回の記事)。
その後遊んでいたのですが、kubectl port-forward
を使って通信している現状を変え、
通常のサーバのようにhogohoge.your.domain
でアクセスできないかな~といろいろ試していた所、うまくいったのでその記録を残しておきたいと思います。
完成系
以下のようなリソースを作成すると自動的にドメインおよび TLS が設定され、hogohoge.your.domain
と HTTPS 通信を行えるようになります。
apiVersion: apps/v1kind: Deploymentmetadata: name: ingress-helloworldspec: selector: matchLabels: app: ingress-helloworld replicas: 1 template: metadata: labels: app: ingress-helloworld spec: containers: - name: ingress-helloworld image: gcr.io/google-samples/hello-app:1.0 ports: - containerPort: 8080 resources: limits: cpu: 10m memory: 30Mi requests: cpu: 10m memory: 30Mi---apiVersion: v1kind: Servicemetadata: name: ingress-helloworld labels: app: ingress-helloworldspec: ports: - port: 8080 protocol: TCP selector: app: ingress-helloworld---apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: ingress-helloworld annotations: cert-manager.io/cluster-issuer: letsencrypt-productionspec: ingressClassName: nginx tls: - hosts: - annotation.your.domain # あなたのドメインに書き換えてください secretName: nginx-annotation-tls rules: - host: annotation.your.domain # あなたのドメインに書き換えてください http: paths: - path: / pathType: Prefix backend: service: name: ingress-helloworld port: number: 8080
MetalLB のインストール
MetalLB, bare metal load-balancer for Kubernetes (universe.tf)
まずkubectl port-forward
を使わずともクラスター内のサービスと通信できるように、
MetalLB をインストールします。インストール方法はいろいろあるようですが今回は Helm を使ってインストールします。
ここでいろいろ追加されるので訳が分からなくなるのを防ぐために Namespace を分けておきます。
helm repo add metallb https://metallb.github.io/metallbhelm repo updatehelm install metallb metallb/metallb -n metallb-ns --create-namespace
続いて MetalLB が動作できるように設定を追加します。L2 モードと BGP モードというものがあるようなのですが、 私が BGP というものを詳しく知らないので L2 モードで設定してきます。
下のような設定ファイルを作成します。ただしip-address-pool.yaml
の spec.addresses は各自の環境に合わせて変更してください。
詳しくは以下リンクを確認お願いします。
https://metallb.universe.tf/configuration/
ip-address-pool.yaml
apiVersion: metallb.io/v1beta1kind: IPAddressPoolmetadata: name: primary namespace: metallb-nsspec: addresses: - 192.168.1.200-192.168.1.254
l2-advetisement.yaml
apiVersion: metallb.io/v1beta1kind: L2Advertisementmetadata: name: l2-primary namespace: metallb-nsspec: ipAddressPools: - primary
適当なサービスを作成して IPAddressPool に設定されたアドレスが割り当てられるかチェックします。
metallb.yaml
apiVersion: v1kind: Servicemetadata: name: sample-lbspec: type: LoadBalancer ports: - port: 80 selector: {}
kubectl apply -f metallb.yamlkubectl get service sample-lb
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEsample-lb LoadBalancer 10.233.57.250 192.168.1.201 80:31052/TCP 27s
EXTERNAL-IP にアドレスが書いてあれば OK です。作成したサービスは片付けます。
kubectl delete -f metallb.yaml
Ingress-Nginx Controller のインストール
Installation Guide - Ingress-Nginx Controller (kubernetes.github.io)
ついでに L7 のロードバランサーも入れておきます。これも Helm を使ってインストールします。
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginxhelm repo updatehelm install ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx --create-namespace
インストールが完了したら動作しているか確かめるために、以下のようなリソースを作成します。
参考: Minikube 上で NGINX Ingress コントローラーを使用して Ingress をセットアップする | Kubernetes
ingress-nginx.yaml
apiVersion: apps/v1kind: Deploymentmetadata: name: ingress-helloworldspec: selector: matchLabels: app: ingress-helloworld replicas: 1 template: metadata: labels: app: ingress-helloworld spec: containers: - name: ingress-helloworld image: gcr.io/google-samples/hello-app:1.0 ports: - containerPort: 8080---apiVersion: v1kind: Servicemetadata: name: ingress-helloworld labels: app: ingress-helloworldspec: ports: - port: 8080 protocol: TCP selector: app: ingress-helloworld---apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: ingress-helloworldspec: rules: - host: helloworld.info http: paths: - path: / pathType: Prefix backend: service: name: ingress-helloworld port: number: 8080 ingressClassName: nginx
kubectl apply -f ingress-nginx.yaml
Ingress が作成されていることを確かめたら curl でアクセスします。HOSTS が設定されていればいけると思います。
kubectl get ingressNAME CLASS HOSTS ADDRESS PORTS AGEingress-helloworld nginx helloworld.info 192.168.1.200 80 48s
curl -H 'Host:helloworld.info' 192.168.1.200Hello, world!Version: 1.0.0Hostname: ingress-helloworld-7dcf585646-j7jct
ホストを設定せずにアクセスすると Not Found になるので L7 レベルで分散が行われているようです。
curl 192.168.1.200<html><head><title>404 Not Found</title></head><body><center><h1>404 Not Found</h1></center><hr><center>nginx</center></body></html>
確認ができたら作成したリソースを片付けます。
kubectl delete -f ingress-nginx.yaml
ExternalDNS
先ほどまでで L2 および L7 での分散およびアクセスが行えるようになりましたが、いまだに IP アドレスを使わないといけません。 これはお洒落じゃないですね(圧)。そこでドメインを自動的に発行できるように ExternalDNS を利用します。
ExternalDNS は作成された Ingress や Service の情報を見て、いい感じに DNS レコードを作成してくれるツールです。 そのためここら先は各自がドメインを持っている必要があります。持っていない方は適当なドメインを作成しておいてください。
私は DNS Server として Cloudflare を利用しているので以下の手順に沿って行います。
external-dns/docs/tutorials/cloudflare.md at master · kubernetes-sigs/external-dns · GitHub
その他対応しているプロバイダー一覧は以下です。
kubernetes-sigs/external-dns: Configure external DNS servers (AWS Route53, Google CloudDNS and others) for Kubernetes Ingresses and Services (github.com)
API トークン発行
はじめに Cloudflare の API を利用するための API トークンを発行します。下のページの Create Token をクリックしてください。
なんかいっぱいありますが、一番下の Custom token を選択します。
公式ドキュメントにあるようにいくつか権限を与えて作成します。
When using API Token authentication, the token should be granted Zone Read, DNS Edit privileges, and access to All zones.
ここで作成されたトークンを忘れないようにメモしておきます。
ExternalDNS デプロイ
続いて ExternalDNS をデプロイします。今回は以下のようなリソースを作成しました。Secret の部分は先ほど作成したトークンに置き換えてください。
external-dns.yaml
apiVersion: v1kind: Namespacemetadata: name: external-dns---apiVersion: v1kind: Secretmetadata: name: cloudflare-secret namespace: external-dnsdata: # echo 'YourSecret' | base64 token: YourToken---apiVersion: v1kind: ServiceAccountmetadata: name: external-dns namespace: external-dns---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: external-dnsrules: - apiGroups: [""] resources: ["services", "endpoints", "pods"] verbs: ["get", "watch", "list"] - apiGroups: ["extensions", "networking.k8s.io"] resources: ["ingresses"] verbs: ["get", "watch", "list"] - apiGroups: [""] resources: ["nodes"] verbs: ["list", "watch"]---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: external-dns-viewerroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: external-dnssubjects: - kind: ServiceAccount name: external-dns namespace: external-dns---apiVersion: apps/v1kind: Deploymentmetadata: name: external-dns namespace: external-dnsspec: replicas: 1 strategy: type: Recreate selector: matchLabels: app: external-dns template: metadata: labels: app: external-dns spec: serviceAccountName: external-dns containers: - name: external-dns image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --log-level=info - --log-format=text - --source=service # ingress is also possible - --source=ingress - --policy=sync # ingressが消えた際にレコードを消してほしくない場合はupsert-onlyにする - --events - --interval=1m # より早く同期したい場合は10sなどにする - --domain-filter=YourDomain # (optional) limit to only example.com domains; change to match the zone created above. - --provider=cloudflare - --cloudflare-dns-records-per-page=5000 # (optional) configure how many DNS records to fetch per request livenessProbe: failureThreshold: 2 httpGet: path: /healthz port: http scheme: HTTP initialDelaySeconds: 10 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 5 resources: limits: cpu: 50m memory: 50Mi requests: cpu: 50m memory: 50Mi env: - name: CF_API_TOKEN valueFrom: secretKeyRef: name: cloudflare-secret key: token
kubectl apply -f external-dns.yaml
起動したかどうか確かめます。
kubectl get all -n external-dnsNAME READY STATUS RESTARTS AGEpod/external-dns-5855c77d8f-vnd62 1/1 Running 0 27s
NAME READY UP-TO-DATE AVAILABLE AGEdeployment.apps/external-dns 1/1 1 1 27s
NAME DESIRED CURRENT READY AGEreplicaset.apps/external-dns-5855c77d8f 1 1 1 27s
動作チェック
適当な Ingress を作成し記載したドメインが登録されるか調べます。
dns-sample.yaml
apiVersion: apps/v1kind: Deploymentmetadata: name: nginx-deploymentspec: selector: matchLabels: app: nginx replicas: 3 template: metadata: labels: app: nginx spec: containers: - name: nginx image: docker.io/nginx:latest ports: - containerPort: 80 resources: limits: cpu: 10m memory: 30Mi requests: cpu: 10m memory: 30Mi---apiVersion: v1kind: Servicemetadata: name: nginx-service labels: app: nginxspec: selector: app: nginx ports: - port: 80 protocol: TCP name: http---apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: nginx-ingressspec: ingressClassName: nginx rules: - host: nginx.your.domain http: paths: - path: / pathType: Prefix backend: service: name: nginx-service port: number: 80
kubectl apply -f dns-sample.yaml
1 分ほどすると DNS レコードが登録されているようなログが流れてきました。
kubectl logs -n external-dns deployments/external-dns -f
# 略time="2023-07-02T07:33:33Z" level=info msg="Changing record." action=CREATE record=nginx.xxxx.xxxx ttl=1 type=A zone=xxxxtime="2023-07-02T07:33:34Z" level=info msg="Changing record." action=CREATE record=nginx.xxxx.xxxx ttl=1 type=TXT zone=xxxxtime="2023-07-02T07:33:34Z" level=info msg="Changing record." action=CREATE record=a-nginx.xxxx.xxxx ttl=1 type=TXT zone=xxxx
確認しに行くと確かに DNS レコードが増えていることが確認できます。
この状態で nginx.your.domain にアクセスすると Nginx の Welcome メッセージが表示されます。
最後に作成した Ingress を削除し DNS レコードが削除されることを確かめます。
kubectl delete -f dns-sample.yaml
cert-manager
先ほどまでで DNS レコードが自動で作成されるようになりました。かなりいい感じですね。 ですが、まだ TLS 証明書を発行していないため通信は暗号化されておらずセキュアな通信ではありません。 そこで証明書の発行や更新を自動で行ってくれる cert-manager を利用します。
cert-manager - cert-manager Documentation
かなり多くの認証局を利用できるようですが、無料で利用できる Let’s Encrypt を使います。
インストール
Helm - cert-manager Documentation
今回も Helm を使ってインストールします。CRD をインストールする方法として kubectl を使う方法もあるようですが、
Recommended for ease of use & compatibility
と書かれている Helm を使う方法でいきます。
詳しくは上記のリンクを参照してください。
helm repo add jetstack https://charts.jetstack.iohelm repo updatehelm install \ cert-manager jetstack/cert-manager \ --namespace cert-manager \ --create-namespace \ --set installCRDs=true
2~3 分程かかったのでコーヒーなどを飲んでいるといいと思います。
Issuer 作成
Cloudflare - cert-manager Documentation
今回は先ほど作成した ExternalDNS と連携して TLS 証明書を発行するようにするため Cloudflare 用の設定を行います。 ExternalDNS と同じようにこちらもいくつか権限を与えて作成します。詳しくは上記ドキュメントを参照してください。
先ほどメモしたトークンを使って以下のようなIssuer
を作成します。
Issuer
は実際に証明書を発行するリソースで、これにはIssuer
とClusterIssuer
の 2 つがあります。
違いはネームスペースをまたいで利用できるかどうかです。普通の環境ではIssuer
を使うべきですが、
お家 kubernetes で私しか使わないため今回はClusterIssuer
にしています。
cluster-issuer.yaml
apiVersion: v1kind: Secretmetadata: name: cloudflare-secret namespace: cert-managerdata: # echo 'YourSecret' | base64 token: YourToken---apiVersion: cert-manager.io/v1kind: ClusterIssuermetadata: name: letsencrypt-staging namespace: cert-managerspec: acme: email: YourEmail server: https://acme-staging-v02.api.letsencrypt.org/directory privateKeySecretRef: name: letsencrypt-staging-private-key solvers: - dns01: cloudflare: apiTokenSecretRef: name: cloudflare-secret key: token---apiVersion: cert-manager.io/v1kind: ClusterIssuermetadata: name: letsencrypt-production namespace: cert-managerspec: acme: email: YourEmail server: https://acme-v02.api.letsencrypt.org/directory privateKeySecretRef: name: letsencrypt-production-private-key solvers: - dns01: cloudflare: apiTokenSecretRef: name: cloudflare-secret key: token
spec.acme.privateKeySecretRef
に指定した名前の Secret が作成されそこに TLS の秘密鍵が保存されるようです。
また、Let’s Encrypt には制限が強い production と弱い staging の 2 つがあるので、それを使い分けられるように 2 つIssuer
を作成します。
- NGINX イングレスのセキュリティ保護 - 証明書マネージャーのドキュメント (cert-manager.io)
- ステージング環境 - Let’s Encrypt - フリーな SSL/TLS 証明書 (letsencrypt.org)
これらのリソースを apply します。
kubectl apply -f cluster-issuer.yaml
動作チェック
最後に動作チェックします。すべての設定が完了したので一番初めに示したリソースを利用できるはずです。
cert-sample.yaml
apiVersion: apps/v1kind: Deploymentmetadata: name: ingress-helloworldspec: selector: matchLabels: app: ingress-helloworld replicas: 1 template: metadata: labels: app: ingress-helloworld spec: containers: - name: ingress-helloworld image: gcr.io/google-samples/hello-app:1.0 ports: - containerPort: 8080 resources: limits: cpu: 10m memory: 30Mi requests: cpu: 10m memory: 30Mi---apiVersion: v1kind: Servicemetadata: name: ingress-helloworld labels: app: ingress-helloworldspec: ports: - port: 8080 protocol: TCP selector: app: ingress-helloworld---apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: ingress-helloworld annotations: cert-manager.io/cluster-issuer: letsencrypt-productionspec: ingressClassName: nginx tls: - hosts: - annotation.your.domain # あなたのドメインに書き換えてください secretName: nginx-annotation-tls rules: - host: annotation.your.domain # あなたのドメインに書き換えてください http: paths: - path: / pathType: Prefix backend: service: name: ingress-helloworld port: number: 8080
kubectl apply -f cert-sample.yaml
作成には時間がかかる場合があります。うまくいっていれば以下のような出力を得られるはずです。
curl https://annotation.your.domainHello, world!Version: 1.0.0Hostname: ingress-helloworld-6f7dc7d764-qd9l6
以下のコマンドを実行することで発行された証明書の情報を確認できます。
kubectl describe certificate
今回は証明書の発行に annotation を用いましたが、直接証明書を表すCertificate
を作成してそれを適用させることもできます。
詳しくは公式ドキュメントをご覧ください。
- Frequently Asked Questions (FAQ) - cert-manager Documentation
- Securing Ingress Resources - cert-manager Documentation
最後にリソースを片付けます。自動で作成される Secret は削除されないので手動で消します。
kubectl delete -f cert-sample.yamlkubectl delete secrets nginx-annotation-tls
終わりに
すばらしい OSS のおかげで内部の挙動をほぼわかっていなくてもここまで行うことができました。 ひとまず、お家 kubernetes が 1 ステップ成長したような気がしてうれしいです。
次の目標として以下を考えています。
- node や pod のメトリクスを自動収集してかっこいいダッシュボードで見られるようにする
- devcontainer のように kubernetes 内で開発できるようにする
- 外部からのアクセスをお家 kubernetes で処理できるようにする
まだまだ先は遠いですが 1 つづつこなして、僕の考えた最強の kubernetes に近づけていきたいと思います。
余談
普段の作業ログを Obsidian を使って記録しているのですが、画像をコピペした時に作られる場所をカスタムできるのをはじめて知りました。 めちゃくちゃ便利なこの機能をもっと早く知りたかった…