0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ロードバランサーなしKubernetesでHTTPS公開!NGINX Gateway Fabricとcert-managerでLet's Encryptを自動化

0
Posted at

はじめに

こんにちは!医療ITエンジニアの三浦です。

今回は、Ubuntu 26.04 LTS上に構築したシングルノードKubernetes環境で、 NGINX Gateway Fabric(NGF) と cert-manager を使用して、Let's EncryptのSSL/TLS証明書を自動取得・更新する環境を構築します。

AWSやAzureなどのクラウド環境では、ロードバランサーがHTTP/HTTPS通信を受け付ける構成が一般的です。

一方、オンプレミスやVPSなど、ロードバランサーを用意しない環境では、KubernetesのNodePort 80/443で外部通信を受け付けることができます。

今回は以下の構成を目指します。

  • Gateway APIでHTTP/HTTPSのルーティングを管理
  • NGINX Gateway FabricでTLS終端
  • cert-managerでLet's Encrypt証明書を取得・自動更新
  • NodePortで外部通信を受け付ける
  • HTTPSでNginxのWebページを公開

今回はNodePortを80/443に直接割り当て、ホストの80/443ポートで通信を受け付けます。

環境

今回使用する環境は以下のとおりです。

項目 環境
OS Ubuntu 26.04 LTS
コンテナランタイム containerd
Kubernetes v1.37系
CNI Calico
Gateway API v1.7.0
Gateway Controller NGINX Gateway Fabric
パッケージ管理 Helm 3
証明書管理 cert-manager
証明書発行元 Let's Encrypt

前回の記事「Ubuntu 26.04 LTSにContainerd、Kubernetes 1.37、Calico 3.33をインストール」で、containerd・Kubernetes・Calicoの構築とNginxのNodePort公開まで確認しました。

本記事はその続編です。Gateway API、Helm、NGINX Gateway Fabric、cert-managerは今回新たに導入します。 前回のNginx検証用DeploymentとServiceは削除済みであることを前提とします。

1. 今回の構成

今回の構成は以下のようになります。

Internet
   |
   | HTTPS :443 / HTTP :80
   |
[Public IP]
   |
   | NodePort :80 / :443
   |
Ubuntu 26.04
   |
Kubernetes (Single Node)
   |
   +-- NGINX Gateway Fabric
   |      |
   |      +-- NodePort 80 (HTTP)
   |      +-- NodePort 443 (HTTPS)
   |      |
   |      +-- Gateway
   |             |
   |             +-- HTTP Listener :80
   |             +-- HTTPS Listener :443
   |                    |
   |                    +-- TLS Termination
   |                    |
   |                    +-- HTTPRoute
   |                           |
   |                           +-- Nginx Service
   |                                  |
   |                                  +-- Nginx Pod
   |
   +-- cert-manager
          |
          +-- Let's Encrypt
          |
          +-- Certificate
          |
          +-- TLS Secret

NGFがHTTPS通信のTLS終端を担当し、復号後のHTTPリクエストをバックエンドのNginxに転送します。
cert-managerはLet's Encryptと通信して証明書を取得し、KubernetesのSecretとして保存します。
NGFはそのSecretを参照してTLS終端を行います。

NodePortとGateway Listener

今回はNodePortとGateway Listenerの両方をHTTP 80、HTTPS 443に設定します。NodePortはKubernetesのServiceを介して通信を転送する仕組みであり、NGFのPodがホストのポートを直接bindするわけではありません。

Let's EncryptのHTTP-01認証では、インターネットから対象ドメインのTCP 80番に到達できる必要があります。DNS、ファイアウォール、外部ネットワークの設定も確認してください。

2. NodePortの範囲を拡張

KubernetesのNodePortは通常 30000-32767 の範囲で割り当てられます。今回は80/443を直接指定するため、kube-apiserverの許可範囲を拡張します。以下はkubeadmで構築したクラスタの例です。

sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml

spec.containers[].command の既存の引数を残し、以下を追加します。

- --service-node-port-range=80-32767

保存後、kubeletがStatic Podの変更を検知し、kube-apiserverを再作成します。

kubectl get pods -n kube-system
kubectl get --raw='/readyz'; echo

注意: この変更はクラスタ全体のNodePort割当範囲を広げます。80~29999のポートも割り当て可能になるため、他のServiceやホスト上のサービスとの競合、ファイアウォール設定に注意してください。設定前にマニフェストをバックアップし、APIサーバーの一時停止を許容できるタイミングで作業します。

3. Gateway API・Helm・NGINX Gateway Fabricのインストール

前回の記事で構築したKubernetes環境に、今回新たにGateway API、Helm、NGINX Gateway Fabric(NGF)をインストールします。Gateway APIはv1.7.0、NGFのNamespaceは nginx-gateway、Helmリリース名は my-ngf とします。

Gateway APIのインストール

Gateway APIのCRDをインストールします。

kubectl apply --server-side \
  -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.7.0/standard-install.yaml

インストールを確認します。

kubectl get crd | grep gateway.networking.k8s.io

Helmのインストール

今回使用するHelm 3をインストールします。

curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
helm version

NGINX Gateway Fabricのインストール

NGFのNamespaceを作成します。

kubectl create namespace nginx-gateway

NodePort 80/443を指定してNGFをインストールします。この操作より前に、第2章のNodePort範囲拡張を完了させてください。

helm install my-ngf oci://ghcr.io/nginx/charts/nginx-gateway-fabric \
  --namespace nginx-gateway \
  -f - <<'EOF'
nginx:
  service:
    type: NodePort
    nodePorts:
      - port: 80
        listenerPort: 80
      - port: 443
        listenerPort: 443
EOF

※ これは今回新規に設定するNGFのHelm Valuesです。NGFのChartバージョンによってValuesのスキーマが異なる可能性があるため、実際に使用するChartの helm show values oci://ghcr.io/nginx/charts/nginx-gateway-fabric で nodePorts の形式を確認してください。

NGFの起動を待機します。

kubectl wait --timeout=5m -n nginx-gateway \
  deployment/my-ngf-nginx-gateway-fabric \
  --for=condition=Available

導入状態と設定を確認します。

helm list -n nginx-gateway
helm get values my-ngf -n nginx-gateway
kubectl get pods -n nginx-gateway
kubectl get gatewayclass

このコマンドで新規インストールしたNGFの状態を確認します。既に別途NGFを導入している環境では、重複インストールを避けてください。

Gatewayリソースは第5章で作成します。NGFのデータプレーンServiceはGatewayの作成に応じて生成されるため、NodePortの割り当てはGateway作成後に確認します。

4. cert-managerのインストール

次に、TLS証明書の取得・更新を管理するcert-managerをインストールします。
今回はGateway APIを使用するため、Gateway API連携を有効化します。

helm repo add jetstack https://charts.jetstack.io
helm repo update

helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set crds.enabled=true \
  --set config.gatewayAPI.enabled=true \
  --wait

インストール後、Podの状態を確認します。

kubectl get pods -n cert-manager

以下のコンポーネントが稼働していることを確認します。

  • cert-manager
  • cert-manager-cainjector
  • cert-manager-webhook

Gateway APIのCRDはcert-managerの起動前にインストールしておく必要があります。
後からGateway APIをインストールした場合は、cert-managerを再起動します。

kubectl rollout restart deployment/cert-manager \
  -n cert-manager

5. Gatewayリソースの作成

続いて、NGFでHTTP/HTTPS通信を受け付けるGatewayを作成します。
今回新たに、default NamespaceにGatewayを作成します。
証明書を保存するSecretも同じNamespaceに配置します。

kubectl apply -f - <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: prod-gateway
  namespace: default
spec:
  gatewayClassName: nginx
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: All

    - name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: my-app-tls
      allowedRoutes:
        namespaces:
          from: All
EOF

この設定では、HTTPとHTTPSの2つのListenerを作成します。
HTTPSのListenerでは、my-app-tls というSecretをTLS証明書として参照します。
現時点ではSecretが存在しないため、HTTPS Listenerが正常な状態にならない場合があります。
後ほどcert-managerで証明書を発行すると、Secretが自動生成されます。
Gatewayの状態を確認します。

kubectl get gateway -n default
kubectl describe gateway prod-gateway -n default

NGFのServiceも確認します。

kubectl get svc prod-gateway-nginx -n default

Gateway用のデータプレーンServiceで、以下のNodePortが割り当てられていることを確認します。

80:80/TCP
443:443/TCP

6. Let's EncryptのClusterIssuerを作成

cert-managerでLet's Encryptの証明書を取得するには、IssuerまたはClusterIssuerを作成します。
今回は複数のNamespaceから共通利用できる ClusterIssuer を使用します。
Let's Encryptには、本番環境とステージング環境があります。
本番環境には、認証失敗の回数や同一証明書の発行回数にレート制限があります。設定ミスのまま試行を繰り返すと、しばらく証明書を発行できなくなることがあります。
そのため、最初はステージング環境で証明書発行をテストし、問題がないことを確認してから本番環境に切り替えます。
この章ではステージング用と本番用の2つのClusterIssuerを作成しておき、実際の発行テストと切り替えは第7章で行います。

ステージング環境用のClusterIssuer

kubectl apply -f - <<'EOF'
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    email: your-email@example.com
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-staging-account-key
    solvers:
      - http01:
          gatewayHTTPRoute:
            serviceType: ClusterIP
            parentRefs:
              - name: prod-gateway
                namespace: default
                kind: Gateway
                sectionName: http
EOF

your-email@example.com は自分のメールアドレスに変更してください。
gatewayHTTPRoute を指定することで、cert-managerはGateway APIのHTTPRouteを利用してHTTP-01認証を実行します。

認証時には一時的なHTTPRoute、Service、Podが作成されます。
これらを経由してLet's Encryptからの認証リクエストに応答します。
serviceType: ClusterIP は認証用Serviceをクラスタ内部に限定する設定です。外部公開はNGFが担当します。

本番環境用のClusterIssuer

続けて、本番用のClusterIssuerも作成しておきます。
ステージング用との違いは server と privateKeySecretRef のみです。
実際に本番証明書を発行するのは、第7章でステージング証明書の発行を確認してからです。

kubectl apply -f - <<'EOF'
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    email: your-email@example.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - http01:
          gatewayHTTPRoute:
            serviceType: ClusterIP
            parentRefs:
              - name: prod-gateway
                namespace: default
                kind: Gateway
                sectionName: http
EOF

ClusterIssuerの確認

作成したClusterIssuerを確認します。

kubectl get clusterissuer
NAME                  READY   AGE
letsencrypt-prod      True    10s
letsencrypt-staging   True    30s

両方の READY が True になっていることを確認します。

詳細も確認しておきます。

kubectl describe clusterissuer letsencrypt-staging

Statusに The ACME account was registered with the ACME server と表示されていれば、Let's EncryptへのACMEアカウント登録は成功しています。
なお、ここで確認できるのは「Let's Encryptと通信でき、アカウントを登録できた」ことまでです。
ClusterIssuerがReadyでも、ドメイン認証が成功したわけではありません。
実際のHTTP-01認証は、次章のCertificate作成時に行われます。

7. Certificateリソースの作成

続いて、取得する証明書を定義します。
今回は example.com の証明書を取得する例です。
実際に所有しているドメインに変更してください。

事前にDNSのAレコードを設定し、対象ドメインがサーバーのグローバルIPアドレスを参照するようにしておきます。
また、TCP 80番ポートからNGFのHTTP Listenerにアクセスできる必要があります。

ステージング証明書の取得

まずはステージング用のClusterIssuerを指定して、Certificateを作成します。

kubectl apply -f - <<'EOF'
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: my-app-tls
  namespace: default
spec:
  secretName: my-app-tls
  dnsNames:
    - example.com
  issuerRef:
    name: letsencrypt-staging
    kind: ClusterIssuer
EOF

証明書の発行処理中は、以下のリソースで進行状況を確認できます。

kubectl get certificaterequest -n default
kubectl get order -n default
kubectl get challenge -n default
kubectl get httproute -n default

認証中は cm-acme-http-solver-xxxxx という一時的なHTTPRouteが作成されます。
認証が完了すると、ChallengeやこのHTTPRouteは自動的に削除されます。

ステージング証明書の確認

本番証明書に切り替える前に、以下の3点を確認します。

1. Certificateが発行済みであること

kubectl get certificate my-app-tls -n default
NAME         READY   SECRET       AGE
my-app-tls   True    my-app-tls   1m

READY が True になっていることを確認します。
あわせて、Challengeが残っていないことも確認します。

kubectl get challenge -n default

No resources found と表示されれば、認証が完了して一時リソースが片付いた状態です。
READY が True にならない場合は、詳細を確認します。

kubectl describe certificate my-app-tls -n default
kubectl describe challenge -n default

2. Secretにステージング証明書が保存されていること

kubectl get secret my-app-tls -n default \
  -o jsonpath='{.data.tls\.crt}' | base64 -d \
  | openssl x509 -noout -issuer -subject -dates

issuerに (STAGING) が含まれていれば、ステージング環境から発行された証明書です。

3. NGFがその証明書でTLS終端していること

外部のPCから、443番ポートで提供されている証明書を確認します。

openssl s_client \
  -connect example.com:443 \
  -servername example.com \
  </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer

ここでもissuerに (STAGING) が含まれていれば、外部から443番ポートに到達でき、GatewayのHTTPS ListenerがSecretを読み込めていることまで確認できます。

HTTPRouteは第9章で作成するため、この時点でHTTPSにアクセスするとHTTPのレスポンスは404になりますが、TLSの確認には影響しません。

この3点を確認できれば、DNS、TCP 80番ポートへの到達、HTTP-01認証、Secretの生成、Gatewayからの参照まで一通り動作しています。
なお、ステージング環境の証明書はブラウザから信頼されません。
確認ができたら、本番用ClusterIssuerに切り替えます。

本番証明書への切り替え

CertificateのissuerRefを本番用ClusterIssuerに変更します。

kubectl patch certificate my-app-tls \
  -n default \
  --type=merge \
  -p '{"spec":{"issuerRef":{"name":"letsencrypt-prod","kind":"ClusterIssuer"}}}'

cert-managerがIssuerの変更を検知し、本番用の証明書を発行します。

kubectl get certificate my-app-tls -n default -w

READY が一度 False になり、再び True になれば発行完了です(Ctrl+C で監視を終了します)。

本番証明書の確認

Secretの証明書が本番環境のものに置き換わったことを確認します。

kubectl get secret my-app-tls -n default \
  -o jsonpath='{.data.tls\.crt}' | base64 -d \
  | openssl x509 -noout -issuer -subject -dates

issuerから (STAGING) がなくなり、O = Let's Encrypt になっていれば本番証明書です。
外部からも確認します。

openssl s_client \
  -connect example.com:443 \
  -servername example.com \
  </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer

NGFはSecretの変更を検知して設定を更新するため、NGFの再起動は不要です。
Secretには証明書と秘密鍵が保存されており、NGFはGatewayで指定したこのSecretを参照してTLS終端を行います。

8. NginxのWebサーバーをデプロイ

証明書の準備ができたら、バックエンドとなるNginxをデプロイします。
今回は動作確認用のシンプルな構成とします。

kubectl create deployment nginx-demo \
  --image=nginx:stable \
  -n default

Serviceを作成します。

kubectl expose deployment nginx-demo \
  --port=80 \
  --target-port=80 \
  --type=ClusterIP \
  -n default

状態を確認します。

kubectl get pods -n default
kubectl get svc nginx-demo -n default

この段階では、Nginxはクラスタ内部からのみアクセスできます。
外部からアクセスするには、Gateway APIのHTTPRouteを作成します。

9. HTTPRouteの作成

HTTPSのリクエストをNginxに転送するHTTPRouteを作成します。
example.com の部分は実際に所有しているドメインに変更してください。

kubectl apply -f - <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: nginx-route
  namespace: default
spec:
  parentRefs:
    - name: prod-gateway
      sectionName: https
  hostnames:
    - example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: nginx-demo
          port: 80
EOF

この設定では、以下の通信経路になります。

HTTPS Request
    |
    v
NGINX Gateway Fabric
    |
    | TLS Termination
    |
    v
HTTPRoute
    |
    v
nginx-demo Service
    |
    v
Nginx Pod :80

HTTPRouteの状態を確認します。

kubectl get httproute -n default
kubectl describe httproute nginx-route -n default

Accepted=True と ResolvedRefs=True になっていることを確認します。

HTTPからHTTPSへのリダイレクト

通常のHTTPアクセスをHTTPSへリダイレクトするHTTPRouteも作成します。

kubectl apply -f - <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: https-redirect
  namespace: default
spec:
  parentRefs:
    - name: prod-gateway
      sectionName: http
  hostnames:
    - example.com
  rules:
    - filters:
        - type: RequestRedirect
          requestRedirect:
            scheme: https
            statusCode: 301
EOF

これにより、HTTPでアクセスした場合はHTTPSにリダイレクトされます。
ただし、Let's EncryptのHTTP-01認証でもHTTPを使用します。

cert-managerが作成する認証用HTTPRouteとリダイレクト用HTTPRouteが競合しないことを確認してください。
HTTP-01認証の一時的なRouteは、特定の認証パスに対して作成されるため、通常のPathPrefixルートより優先される構成です。

10. 動作確認

ブラウザで以下のURLにアクセスします。

https://example.com

Nginxの初期ページが表示されれば成功です。
また、curlでも確認できます。

curl -I https://example.com

HTTPからHTTPSへのリダイレクトも確認します。

curl -I http://example.com

301リダイレクトが返されることを確認します。
証明書の内容はOpenSSLでも確認できます。

openssl s_client \
  -connect example.com:443 \
  -servername example.com \
  </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer -subject -dates

Issuer、Subject、有効期限などを確認できます。

11. 証明書の自動更新

cert-managerを使用するメリットの1つが、TLS証明書の自動更新です。
証明書の有効期限を監視し、更新が必要になると自動的に新しい証明書の発行処理を実行します。

更新後の証明書は、同じKubernetes Secretに保存されます。
NGFはSecretの変更を検知し、新しい証明書を使用するように設定を更新します。

通常、証明書更新のたびに手動でNGFを再起動する必要はありません。
証明書の状態は以下のコマンドで確認できます。

kubectl get certificate -n default

詳細を確認します。

kubectl describe certificate my-app-tls -n default

更新予定時刻や発行状態を確認できます。
また、cert-managerのログも確認できます。

kubectl logs -n cert-manager \
  deployment/cert-manager

証明書更新時にもHTTP-01認証が必要になるため、TCP 80番ポートは継続して外部からアクセスできる状態にしておきます。

12. トラブルシューティング

証明書が発行されない

まず、Certificateの状態を確認します。

kubectl describe certificate my-app-tls -n default

続いて、Challengeを確認します。

kubectl get challenge -n default
kubectl describe challenge -n default

主な原因として、以下が考えられます。

  • DNSが正しいグローバルIPを参照していない
  • TCP 80番ポートが外部に公開されていない
  • GatewayのHTTP Listenerが存在しない
  • HTTP-01認証用HTTPRouteがGatewayに接続できない
  • ファイアウォールで通信が遮断されている
  • cert-managerから認証用Serviceへ通信できない

GatewayがReadyにならない

kubectl describe gateway prod-gateway -n default

GatewayのConditionsを確認します。
特に、TLS Secretの存在、GatewayClass、Listenerの設定を確認してください。

HTTPSで502エラーになる

kubectl get pods -n default
kubectl get svc -n default
kubectl describe httproute nginx-route -n default

NginxのPodが正常に稼働しているか、Serviceのポート番号が正しいかを確認します。

まとめ

今回は、シングルノードKubernetes環境でNGINX Gateway Fabricとcert-managerを使用し、Let's Encryptの証明書を自動取得・更新する環境を構築しました。

今回のポイントは以下のとおりです。

  • Gateway APIでHTTP/HTTPSのルーティングを管理できる
  • NGFでTLS終端を実現できる
  • cert-managerで証明書の取得・更新を自動化できる
  • NodePort 80/443を使用してロードバランサーなしでWebサービスを公開できる
  • NodePort範囲を拡張して80/443を直接割り当てられる

クラウドのマネージドロードバランサーを使用しなくても、Kubernetesの標準的なリソースを組み合わせることでHTTPS環境を構築できます。

ただし、シングルノード構成はホスト障害時にサービス全体が停止するため、高可用性が必要な本番システムでは冗長化も検討する必要があります。

今後は、NGFを利用したmTLS(クライアント証明書認証)や、Blue/Greenデプロイメントにも挑戦していきたいと思います。

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?