メモ - RyuSA

技術的なメモ書きとか試してみたこと

おうちKubernetesをそれっぽく加工する話 - MetalLBとかExternalDNSとかcert-managerとか

おうちKubernetes(を含むベアメタルKubernetes)を「マネージドサービスっぽく」使えるようにしよう!というのが本記事の目標

一応前日譚

ryusa.hatenablog.com

ストレージのプロビジョニング

生のKubernetesで適当なPVCを作成しても何も反応しません。これは当然で、生のKubernetesにはストレージをどのように用意すれば良いのかが定義されていないからです。 プラグインなしでPVCとPVを紐つける場合、ユーザー自身がPVを作成してPVCと紐つける必要があります。このやり方は静的なプロビジョニングと呼ばれています。

例えば

apiVersion: v1
kind: PersistentVolume
metadata:
  name: static-pv
spec:
  storageClassName: manual 
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce 
  hostPath:
    path: "/tmp/static-pv"

というPVと

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: static-pvc
spec:
  storageClassName: manual
  resources:
    requests:
      storage: 1Gi
  accessModes:
    - ReadWriteOnce

こんなPVCを作成することで、PVCstatic-pvcはPVstatic-pvを消費することができます。 さらに何かしらのPodがこのPVCを消費すると、PodからPVCへの書き込みがPVが定義されているノード上の/tmp/static-pvに書き込まれるようになります。

一応この運用をしていくことでアプリケーションが永続化ボリュームを利用することはできます。……が、こんなトイルをPVCを作成するたびにやっていてはキリがない!もっとクラウドサービスっぽくやりたい!

そこで、NFSを利用した動的プロビジョナー「nfs-subdir-external-provisioner」を利用してPVCから動的にPVを作成して消費する動的プロビジョニングのセットアップについて紹介します。

f:id:RyuSA:20210521200137p:plain

nfs-subdir-external-provisioner

github.com

nfs-subdir-external-provisionerは「すでに稼働している」NFSサーバーと接続し、PVCに合わせてNFSがストレージを用意してくれる動的プロビジョナーです。Helmでサクッとデプロイできるのも良いところです。 今回は下記のような環境で構築をします。

  • NFSサーバー
    • 192.168.100.101上で起動
    • /exports/ディレクトリを公開している

NOTE: 本音を言うと自宅にNFS搭載のNASが欲しかったのですが予算がないため購入を断念。代わりに家に転がっていたSSDUbuntuサーバーに追加でマウントしてNFSサーバーを構築しました。

インストール

インストールはHelmチャートが用意されているのでそれをそのまま利用するのが良いと思います。

# Helmリポジトリを追加
> helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/

> helm repo update
# helm show values nfs-subdir-external-provisioner/nfs-subdir-external-provisioner で設定できる項目一覧を確認できます

> cat <<EOF > values.yaml
nfs:
  server: 192.168.100.101
  path: /exports/kubernetes/ # 専用のサブディレクトリを切っておきました(個人的な趣向)

storageClass:
  provisionerName: nfs
  defaultClass: true # デフォルトのStorageClassに設定
  name: nfs

resources:
  limits:
    cpu: 100m
    memory: 128Mi
  requests:
    cpu: 100m
    memory: 128Mi
EOF

# kube-system名前空間内にインストール
> helm install nfs-subdir-external-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner -f values.yaml -n kube-system

動作確認

適当にPVCとそれを消費するPodを用意して動作確認をしてみます。

> cat <<EOF > dynamic-pvc-pod.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: dynamic-pvc
spec:
  resources:
    requests:
      storage: 1Gi
  accessModes:
    - ReadWriteOnce
---
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
    - image: nginx
      volumeMounts:
        - mountPath: /data
          name: pvc
  restartPolicy: Always
  volumes:
    - name: pvc
      persistentVolumeClaim:
        claimName: dynamic-pvc
EOF

> kubectl apply -f dynamic-pvc-pod.yaml

> kubectl get pods nginx   
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          3m6s

実際にPodにPVCが消費されて、NFSディレクトリ配下にサブディレクトリが作成されていれば動作確認完了です。

ロードバランサーの作成

生のKubernetes上にデプロイされているPodの中に外からアクセスするには、kubectl port-forwardを利用するかtype: NodePortのServiceを利用するしか方法がありません。カッコ悪いですし、可用性にも欠けます。 もっとクールに、クラウドサービスのようなロードバランサーがほしいところです。

そこで、MetalLBとNGINX Ingress Controllerというツールを導入して、クラウドサービスのようなロードバランサーを自動で作成させてみましょう。

MetalLB

github.com

ベアメタルKubernetes定番のMetalLBです。細かい挙動に関しては解説しませんが、このMetalLBを利用することでtype: LoadBalancerなServiceを利用してKubernetesクラスタの外部から内部へアクセスすることができるようになります。

f:id:RyuSA:20210521213335p:plain

インストール

インストールするために、まず環境のネットワークで予約できるIPアドレス帯を確認しておいてください。今回は192.168.100.200から192.168.100.250までのIPアドレスが予約できていると仮定して話を進めます。

# MetalLBが利用するConfigmapを作成、MetalLBは起動時にこのConfigを読み込みます
> cat <<EOF > metallb-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |
    address-pools:
    - name: my-ip-space
      protocol: layer2
      addresses:
      - 192.168.100.200-192.168.100.250 # 予約されたIPアドレスを指定する
EOF

# metallb-system名前空間を作成します(metallbが起動するデフォルトの名前空間)
> kubectl create namespace metallb-system

# リソースをデプロイします、今回は検証時最新だったMetalLB v0.9.6を利用しています
> kubectl apply -f metallb-config.yaml -f https://raw.githubusercontent.com/metallb/metallb/v0.9.6/manifests/metallb.yaml

動作確認

metallb-system名前空間上のPodがすべてRunningになったところで、適当なServiceを作成して動作確認をしましょう。

> cat <<EOF > lb.yaml
apiVersion: v1
kind: Service
metadata:
  name: lb
spec:
  ports:
  - port: 80
  selector: {}
  type: LoadBalancer
EOF

> kubectl apply -f lb.yaml

> kubectl get svc lb
NAME   TYPE           CLUSTER-IP      EXTERNAL-IP      PORT(S)        AGE
lb     LoadBalancer   10.97.232.211   192.168.100.201  80:31932/TCP   11s

作成したServiceにEXTERNAL-IPが振られていれば、正常に動作しています。

NGINX Ingress Controller

L7ロードバランサーも欲しくなってきましたね?欲しくなりませんか??

github.com

NGINX Ingress ControllerはKubernetesIngressリソースを管理するIngressコントローラーです。個人的にIngressコントローラーの中で一番シンプルで使いやすいコントローラーだと思います。

NGINX Ingress Controllerはその名の通りNGINXを利用してIngressを実装しているControllerで、Ingressリソースを作成するとそのIngressの要求に合わせたNGINXが設定されます。

f:id:RyuSA:20210521214257p:plain

インストール

Helmでシンプルにインストールできます。

# Helmリポジトリを登録します
> helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx

> helm repo update

# helm show values ingress-nginx/ingress-nginx で指定できるValueを確認できます
> helm install ingress-nginx ingress-nginx/ingress-nginx

動作確認

ingress-nginx-controllerのPodが起動した後に、Ingressリソースを作成して動作確認してみます。

> cat <<EOF > ingress-sample.yaml
# 適当なPod
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  containers:
  - image: nginx
     name: nginx
     ports:
     - containerPort: 80
        name: http
---
# Podに紐つけるService
apiVersion: v1
kind: Service
metadata:
  name: ingress-sample
spec:
  ports:
  - port: 80
     name: http
  selector:
    app: nginx
---
# Serviceに紐つけるIngress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: sample
spec:
  rules:
  - http:
      paths:
      - backend:
         service:
            name: ingress-sample
            port:
              number: 80
        path: /
        pathType: Prefix
EOF

# IngressとServiceとPodを作成
> kubectl apply -f ingress-sample.yaml

# Ingressリソースの状態を確認してみる
> kubectl get ingress                                                                   
NAME      CLASS    HOSTS   ADDRESS           PORTS   AGE
ingress   <none>   *       192.168.100.202   80      64s

# 実際にアクセスしてみる 
> curl 192.168.100.202                                                                  
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    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>

しっかりIngressのAddressからPodへアクセスできることを確認して動作確認完了です。

ドメインTLS

ここまでで、おうちKubernetes(ベアメタル環境)においてもロードバランサーを経由してKubernetes外部から内部へアクセスできるようになりました。しかし、今のままではIPアドレスでアクセスせざるをえないためやっぱりカッコ悪いです。 またTLSを利用したセキュアな接続も実現できないため、安心安全なKubernetes生活にはまだ程遠いです。

そこで、External DNSとcert-managerを利用してServiceやIngressに対して自動的にドメインを発行&TLS化を行ってみます。

External DNS

github.com

External DNSは外部のDNSサーバーやDNSサービスに対してリクエストを行い、DNSレコードの変更を行ってくれるツールです。Kubernetesのリソースを監視して、リソースの状態に合わせて動的にDNSレコードの変更を行うこともできます。 自分でDNSサーバーを構築して利用する方法もありますが、後でTLS化を行うことも踏まえてPublicなDNSであるCloudflare DNSを利用することにします。

f:id:RyuSA:20210521220308p:plain

インストール

External DNSを利用する前に、ご自身のドメインを持っていることをご確認ください。ドメインをCloudflareに登録したのち、Cloudflare DNSAPIキーを取得してきます。

support.cloudflare.com

APIキーを元に、SecretリソースとExternal DNSをデプロイします。

# マニフェストを作成
# YOUR_DOMAIN/YOUR_API_KEY/YOUR_EMAIL を適切な値に置き換えてください
> cat <<EOF > externaldns.yaml
apiVersion: v1
kind: Secret
metadata:
  name: cloudflare-secret
  namespace: kube-system
type: Opaque
stringData:
  api-key: YOUR_API_KEY
  email: YOUR_EMAIL
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-dns
  namespace: kube-system
---
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: kube-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-dns
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: external-dns
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: external-dns
    spec:
      serviceAccountName: external-dns
      containers:
      - name: external-dns
        image: k8s.gcr.io/external-dns/external-dns:v0.7.6
        args:
        - --source=service
        - --source=ingress
        - --domain-filter=YOUR_DOMAIN
        - --provider=cloudflare
        env:
        - name: CF_API_KEY
          valueFrom:
            secretKeyRef:
              name: cloudflare-secret
              key: api-key
        - name: CF_API_EMAIL
          valueFrom:
            secretKeyRef:
              name: cloudflare-secret
              key: email
        resources: 
          limits:
            cpu: 50m
            memory: 50Mi
          requests:
            memory: 50Mi
            cpu: 10m
EOF

# External DNSをデプロイ
> kubectl apply -f externaldns.yaml

動作確認

デプロイしExternalDNSが起動した後、Ingressを作成してドメインが自動的に登録されていることを確認してみます。

> cat <<EOF > externaldns-sample.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  containers:
  - image: nginx
    name: nginx
    ports:
    - containerPort: 80
      name: http
---
apiVersion: v1
kind: Service
metadata:
  name: ingress
spec:
  ports:
  - port: 80
    name: http
  selector:
    app: nginx
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress
spec:
  rules:
  - host: nginx.YOUR_DOMAIN # 自身のドメインに置き換えてください
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: nginx
            port:
              number: 80
EOF

# Ingressたちをデプロイ
> kubectl apply -f externaldns-sample.yaml

デプロイ完了後、しばらくするとnginx.YOUR_DOMAINドメイン名に対してPrivateIPアドレスが登録されることを確認して動作確認完了です。(dig nginx.YOUR_DOMAINなど)

cert-manager

github.com

cert-managerはTLS証明書の発行やローテーションを自動化するためのツールで、Let's EncryptやHashicorp Vaultなどの認証局と連携することができます。cert-managerはCRDとしてデプロイされるIssuer(認証局)とCertificate(証明書)を元にTLS証明書を発行しSecretsに保存してくれます。

f:id:RyuSA:20210521223114p:plain

インストール

Helmでシンプルにインストールできます。

# Helmチャートを追加
> helm repo add jetstack https://charts.jetstack.io

> helm repo update

# cert-managerをインストール
> helm install cert-manager jetstack/cert-manager \
  --namespace kube-system \
  --set installCRDs=true # ビルドインのCRDをデプロイ

動作確認

今回は、CloudflareのDNSとLet's Encryptを利用して動作確認をします。先のセクションでExternal DNSをデプロイしたのでそれに合わせてリソースをデプロイします。

まずはIssuerを用意します。

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
  namespace: kube-system
spec:
  acme:
    email: YOUR_EMAIL
    privateKeySecretRef:
      name: letsencrypt-staging-privatekey
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    # server: https://acme-v02.api.letsencrypt.org/directory 本番環境はこちらのURL
    solvers:
    - dns01:
        cloudflare:
          apiKeySecretRef:
            name: cloudflare-secret
            key: api-key
          email: YOUR_EMAIL

ClusterIssuerはクラスタ全体で管理できるIssuerです。マルチテナントな環境では利用しにくいですが、今回はおうちKubernetesを対象としているのでClusterIssuerを利用しています。(Issuerも同じように定義できます) なお、spec.acme.privateKeySecretRef.nameで指定されたSecretにTLS証明書に利用する秘密鍵が保管されます。

次にCertificateをデプロイします。

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: nginx-tls-staging
spec:
  dnsNames:
  - nginx.YOUR_DOMAIN
  issuerRef:
    group: cert-manager.io
    kind: ClusterIssuer
    name: letsencrypt-staging
  secretName: nginx-tls-staging

Certificateをデプロイすると、cert-managerがIssuerの情報とともに実際にTLS証明書を取得してきてくれます。取得してきたTLS証明書はCertificateのspec.secretNameで指定したSecretに保存されます。

実際にIngressTLS証明書を指定してデプロイすると、エンドポイントのTLS化がされていることを確認できると思います。

> cat <<EOF > secure-nginx.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  containers:
  - image: nginx
    name: nginx
    ports:
    - containerPort: 80
      name: http
---
apiVersion: v1
kind: Service
metadata:
  name: secure-nginx
spec:
  ports:
  - port: 80
    name: http
  selector:
    app: nginx
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: secure-nginx
spec:
  rules:
  - host: nginx.YOUR_DOMAIN # 自身のドメインに置き換えてください
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: nginx
            port:
              number: 80
  tls:
  - hosts:
    - nginx.YOUR_DOMAIN
    secretName: nginx-tls-staging
EOF

# Ingressたちをデプロイ
> kubectl apply -f secure-nginx.yaml

# 証明書の発行を待ち、実際にアクセスしてみる
> curl https://nginx.YOUR_DOMAIN
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    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>

おわりに

さっくりとおうちKubernetesをセットアップして、マネージドサービスっぽいKubernetesにお着替えさせてあげることができました。

一方で、Secretsの暗号化がされていなかったり監視機構が働いていなかったりと安心安全なKubernetes生活はまだ遠いです。この辺りも少しずつ改善していこうかなと思います。