📶

ExternalDNSとcert-managerでお家KubernetesとHTTPSな通信をする

2023/07/02に公開

はじめに

以前 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は各自の環境に合わせて変更してください。詳しくは以下リンクを確認お願いします。
https://metallb.universe.tf/configuration/

  • 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

kubernetes-sigs/external-dns: Configure external DNS servers (AWS Route53, Google CloudDNS and others) for Kubernetes Ingresses and Services (github.com)

先ほどまでで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をクリックしてください。

API Tokens | Cloudflare

なんかいっぱいありますが、一番下の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は実際に証明書を発行するリソースで、これにはIssuerClusterIssuerの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を作成します。

これらのリソースを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を作成してそれを適用させることもできます。詳しくは公式ドキュメントをご覧ください。

最後にリソースを片付けます。自動で作成される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