はじめに
ConoHa VPS 上に構築した Kubernetes クラスターに Nginx Ingress Controller を導入した後、次のステップとして cert-manager + Let's Encrypt による TLS 証明書の自動取得(HTTPS 化) に挑戦しました。
ドキュメント通りに進めれば簡単なはずが、いくつかのハマりポイントがあったので、詰まった箇所と解決策も含めて記録します。
環境
| 項目 | 内容 |
|---|---|
| インフラ | ConoHa VPS |
| Kubernetes | セルフホスト(kubeadm) |
| Ingress Controller | ingress-nginx v1.12.1 |
| FQDN |
xxx.xxx.xxx.xxx.nip.io(nip.io を利用) |
| cert-manager | Helm でインストール |
nip.io とは?
IPアドレスを含むホスト名を自動的にそのIPに解決してくれる無料のワイルドカードDNSサービスです。独自ドメインなしでFQDNが使えるため、検証環境に最適です。Let's Encrypt の証明書も取得可能です。
全体の流れ
Ingress に「HTTPS使いたい」と書くだけ
↓
cert-manager が Let's Encrypt に証明書を申請
↓
Let's Encrypt が HTTP-01 チャレンジでドメイン所有確認
↓
Nginx Ingress が確認用 URL に自動応答
↓
Let's Encrypt が証明書を発行
↓
cert-manager が Secret に TLS 証明書を保存・自動更新
Step 1:Helm のインストール
cert-manager は Helm でインストールするのが標準的です。
snap install helm --classic
注意:
--classicオプションが必要です。省略するとエラーになります。snap の classic confinement はシステムへのアクセスが必要な開発ツールでは一般的な選択肢なので、helm では問題ありません。
snap を使いたくない場合は公式スクリプトでもインストールできます:
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
インストール確認:
helm version
Step 2:cert-manager のインストール
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
Pod の起動確認:
kubectl get pods -n cert-manager
# NAME READY STATUS
# cert-manager-xxxx 1/1 Running
# cert-manager-cainjector-xxxx 1/1 Running
# cert-manager-webhook-xxxx 1/1 Running
3つの Pod がすべて Running になれば OK です。
Step 3:ClusterIssuer の作成
まずステージング環境でテストしてから本番に切り替えるのが必須の流れです。
本番 Let's Encrypt はレートリミット(週5回の失敗で制限)があるため、いきなり本番を使うのはリスクがあります。
ステージング用(テスト)
# clusterissuer-staging.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: your-email@example.com # 自分のメールアドレスに変更
privateKeySecretRef:
name: letsencrypt-staging-key
solvers:
- http01:
ingress:
ingressClassName: nginx
本番用
# clusterissuer-prod.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: your-email@example.com # 自分のメールアドレスに変更
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
ingressClassName: nginx
kubectl apply -f clusterissuer-staging.yaml
kubectl apply -f clusterissuer-prod.yaml
kubectl get clusterissuer
# NAME READY
# letsencrypt-staging True
# letsencrypt-prod True
Step 4:Ingress に TLS 設定を追加
既存の Ingress マニフェストに2点追加します。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
namespace: default
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
cert-manager.io/cluster-issuer: "letsencrypt-staging" # ← 追加
spec:
ingressClassName: nginx
tls: # ← ブロックごと追加
- hosts:
- xxx.xxx.xxx.xxx.nip.io # FQDNを指定
secretName: my-app-tls
rules:
- host: xxx.xxx.xxx.xxx.nip.io
http:
paths:
- backend:
service:
name: my-app-service
port:
number: 80
path: /
pathType: Prefix
kubectl apply -f ingress.yaml
kubectl get certificate -n default -w
⚠️ ハマりポイント:Admission Webhook が HTTP-01 チャレンジをブロックする
ここで大きな壁にぶつかりました。
症状
kubectl get challenge -n default
# STATE: pending のまま変わらない
kubectl describe challenge -n default
# Events:
# Warning PresentError cert-manager-challenges
# Error: admission webhook "validate.nginx.ingress.kubernetes.io" denied the request:
# ingress contains invalid paths: path /.well-known/acme-challenge/xxxxx
# cannot be used with pathType Exact
原因
cert-manager は HTTP-01 チャレンジのために一時的な Ingress を自動作成しますが、この Ingress の pathType が Exact になっています。Nginx Ingress Controller v1.12.1 の Admission Webhook がこの pathType: Exact を /.well-known/acme-challenge/ パスに対して無効と判断してブロックしていました。
試したが効果がなかった対処
① ClusterIssuer に ingressTemplate を追加する
solvers:
- http01:
ingress:
ingressClassName: nginx
ingressTemplate:
metadata:
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
→ Webhook のバリデーションには効果なし
② failurePolicy: Fail → Ignore に変更する
kubectl patch validatingwebhookconfiguration ingress-nginx-admission \
--type='json' \
-p='[{"op": "replace", "path": "/webhooks/0/failurePolicy", "value": "Ignore"}]'
→ Ignore に変更されたことは確認できたが、それでもブロックされ続けた
(Nginx Ingress Controller 自体のバリデーションも別途動作しているため)
✅ 解決策:ValidatingWebhookConfiguration を一時的に削除する
kubectl delete validatingwebhookconfiguration ingress-nginx-admission
Webhook を完全に削除することで、cert-manager がチャレンジ用 Ingress を作成できるようになりました。
クリーンアップして再挑戦:
kubectl delete certificate my-app-tls -n default
kubectl delete secret my-app-tls -n default 2>/dev/null; true
kubectl delete challenge --all -n default
kubectl delete ingress my-app-ingress -n default
# 再適用
kubectl apply -f ingress.yaml
# 監視
kubectl get challenge -n default -w
# STATE が pending → valid に変わればOK
Step 5:ステージング確認 → 本番証明書に切り替え
Challenge が valid になり、証明書が取得できたことを確認:
kubectl get certificate -n default
# NAME READY SECRET AGE
# my-app-tls True my-app-tls 43s
READY: True を確認したら本番に切り替えます。
Ingress のアノテーションを変更:
cert-manager.io/cluster-issuer: "letsencrypt-prod" # staging → prod
# ステージング証明書を削除して再取得
kubectl delete certificate my-app-tls -n default
kubectl delete secret my-app-tls -n default
kubectl apply -f ingress.yaml
kubectl get certificate -n default -w
Step 6:Webhook を復元
本番証明書の取得が完了したら、削除していた Webhook を復元します。
kubectl apply -f deploy.yaml
動作確認
ブラウザで https://your-fqdn.nip.io にアクセスして、アドレスバーに 🔒 が表示されれば HTTPS 化完了 です!
まとめ:ハマりポイントと対策
| ハマりポイント | 原因 | 解決策 |
|---|---|---|
helm コマンドが見つからない |
未インストール | snap install helm --classic |
| Challenge が pending のまま | Nginx Ingress v1.12.1 の Webhook が pathType Exact を拒否 |
ValidatingWebhookConfiguration を一時削除 |
failurePolicy: Ignore が効かない |
Controller 自体にもバリデーションがある | Webhook リソースごと削除が必要 |
| 古い Challenge が残って競合 | リソースの削除漏れ | certificate / secret / challenge / ingress を全削除してからやり直す |
証明書の自動更新について
cert-manager は証明書の有効期限 30日前 に自動で更新してくれます。一度設定すれば、以後は何もしなくて OK です。