はじめに
こんにちは!医療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デプロイメントにも挑戦していきたいと思います。