ExternalDNSとcert-managerでお家KubernetesとHTTPSな通信をする
はじめに
以前 kubespray を使ってお家 kubernetes を作成しました。
その後遊んでいたのですが、kubectl port-forward
を使って通信している現状を変え、
通常のサーバのようにhogohoge.your.domain
でアクセスできないかな~といろいろ試していた所、うまくいったのでその記録を残しておきたいと思います。
完成系
以下のようなリソースを作成すると自動的にドメインおよびTLSが設定され、hogohoge.your.domain
とHTTPS通信を行えるようになります。
apiVersion: apps/v1
kind: Deployment
metadata:
name: ingress-helloworld
spec:
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: v1
kind: Service
metadata:
name: ingress-helloworld
labels:
app: ingress-helloworld
spec:
ports:
- port: 8080
protocol: TCP
selector:
app: ingress-helloworld
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-helloworld
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
spec:
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/metallb
helm repo update
helm install metallb metallb/metallb -n metallb-ns --create-namespace
続いて MetalLB が動作できるように設定を追加します。L2 モードと BGP モードというものがあるようなのですが、私が BGP というものを詳しく知らないので L2 モードで設定してきます。
下のような設定ファイルを作成します。ただしip-address-pool.yaml
のspec.addressesは各自の環境に合わせて変更してください。詳しくは以下リンクを確認お願いします。
ip-address-pool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: primary
namespace: metallb-ns
spec:
addresses:
- 192.168.1.200-192.168.1.254
l2-advetisement.yaml
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: l2-primary
namespace: metallb-ns
spec:
ipAddressPools:
- primary
適当なサービスを作成してIPAddressPoolに設定されたアドレスが割り当てられるかチェックします。
metallb.yaml
apiVersion: v1
kind: Service
metadata:
name: sample-lb
spec:
type: LoadBalancer
ports:
- port: 80
selector: {}
kubectl apply -f metallb.yaml
kubectl get service sample-lb
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
sample-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-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx --create-namespace
インストールが完了したら動作しているか確かめるために、以下のようなリソースを作成します。
参考: Minikube上でNGINX Ingressコントローラーを使用してIngressをセットアップする | Kubernetes
ingress-nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ingress-helloworld
spec:
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: v1
kind: Service
metadata:
name: ingress-helloworld
labels:
app: ingress-helloworld
spec:
ports:
- port: 8080
protocol: TCP
selector:
app: ingress-helloworld
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-helloworld
spec:
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 ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress-helloworld nginx helloworld.info 192.168.1.200 80 48s
curl -H 'Host:helloworld.info' 192.168.1.200
Hello, world!
Version: 1.0.0
Hostname: 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: v1
kind: Namespace
metadata:
name: external-dns
---
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-secret
namespace: external-dns
data:
# echo 'YourSecret' | base64
token: YourToken
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
namespace: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
rules:
- 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/v1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: external-dns
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
namespace: external-dns
spec:
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-dns
NAME READY STATUS RESTARTS AGE
pod/external-dns-5855c77d8f-vnd62 1/1 Running 0 27s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/external-dns 1/1 1 1 27s
NAME DESIRED CURRENT READY AGE
replicaset.apps/external-dns-5855c77d8f 1 1 1 27s
動作チェック
適当なIngressを作成し記載したドメインが登録されるか調べます。
dns-sample.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
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: v1
kind: Service
metadata:
name: nginx-service
labels:
app: nginx
spec:
selector:
app: nginx
ports:
- port: 80
protocol: TCP
name: http
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress
spec:
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=xxxx
time="2023-07-02T07:33:34Z" level=info msg="Changing record." action=CREATE record=nginx.xxxx.xxxx ttl=1 type=TXT zone=xxxx
time="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.io
helm repo update
helm 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: v1
kind: Secret
metadata:
name: cloudflare-secret
namespace: cert-manager
data:
# echo 'YourSecret' | base64
token: YourToken
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
namespace: cert-manager
spec:
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/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-production
namespace: cert-manager
spec:
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/v1
kind: Deployment
metadata:
name: ingress-helloworld
spec:
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: v1
kind: Service
metadata:
name: ingress-helloworld
labels:
app: ingress-helloworld
spec:
ports:
- port: 8080
protocol: TCP
selector:
app: ingress-helloworld
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-helloworld
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
spec:
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.domain
Hello, world!
Version: 1.0.0
Hostname: 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.yaml
kubectl delete secrets nginx-annotation-tls
終わりに
すばらしいOSSのおかげで内部の挙動をほぼわかっていなくてもここまで行うことができました。
ひとまず、お家kubernetesが1ステップ成長したような気がしてうれしいです。
次の目標として以下を考えています。
- nodeやpodのメトリクスを自動収集してかっこいいダッシュボードで見られるようにする
- devcontainerのようにkubernetes内で開発できるようにする
- 外部からのアクセスをお家kubernetesで処理できるようにする
まだまだ先は遠いですが1つづつこなして、僕の考えた最強のkubernetesに近づけていきたいと思います。
余談
普段の作業ログをObsidianを使って記録しているのですが、画像をコピペした時に作られる場所をカスタムできるのをはじめて知りました。
めちゃくちゃ便利なこの機能をもっと早く知りたかった...
Discussion