การจัดการ TLS Certificate บน Kubernetes เป็นงานที่น่าเบื่อและเสี่ยงต่อข้อผิดพลาด: ต้อง Generate, ต้อง Renew ก่อนหมดอายุ, ต้อง Deploy ให้ถูก Secret cert-manager แก้ปัญหาทั้งหมดนี้ ทำให้การจัดการ TLS Certificate เป็น อัตโนมัติ 100%
ทำไมต้อง cert-manager?
| ปัญหา | ไม่มี cert-manager | มี cert-manager |
| ออก Certificate | certbot manual / ทำเอง | อัตโนมัติ (Issuer/ClusterIssuer) |
| Renew | ตั้ง Cron / ลืมแล้ว Expire | Auto-renew ก่อนหมดอายุ 30 วัน |
| Wildcard | DNS Challenge ด้วยมือ | Auto DNS Challenge |
| Multiple Domains | จัดการแยกแต่ละโดเมน | Certificate Resource ต่อโดเมน |
| Ingress Integration | ต้อง Mount Secret เอง | Annotation-based (อัตโนมัติ) |
| Monitoring | ไม่มี (ลืม Renew = Downtime) | Prometheus Metrics + Alert |
ติดตั้ง cert-manager ด้วย Helm
# ติดตั้ง cert-manager:
helm repo add jetstack https://charts.jetstack.io
helm repo update
# ติดตั้งพร้อม CRDs:
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.14.0 \
--set installCRDs=true
# ตรวจสอบ:
kubectl get pods -n cert-manager
# cert-manager-xxxx 1/1 Running
# cert-manager-webhook-xxxx 1/1 Running
# cert-manager-cainjector-xxx 1/1 Running
# ทดสอบด้วย cmctl:
cmctl check api
# → The cert-manager API is ready
Issuer vs ClusterIssuer
| คุณสมบัติ | Issuer | ClusterIssuer |
| Scope | Namespace เดียว | ทั้ง Cluster |
| ใช้เมื่อ | แยก Issuer ต่อ Namespace | ใช้ Issuer เดียวทุก Namespace |
| ใช้ใน Ingress | cert-manager.io/issuer | cert-manager.io/cluster-issuer |
| แนะนำ | Multi-tenant Cluster | ส่วนใหญ่ใช้ตัวนี้ |
# ClusterIssuer สำหรับ Let's Encrypt (Staging):
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-staging-key
solvers:
- http01:
ingress:
class: nginx
# ClusterIssuer สำหรับ Let's Encrypt (Production):
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: nginx
# Apply:
kubectl apply -f clusterissuer.yaml
# ตรวจสอบ:
kubectl get clusterissuer
# NAME READY AGE
# letsencrypt-staging True 1m
# letsencrypt-prod True 1m
ACME Challenges: HTTP-01 vs DNS-01
| คุณสมบัติ | HTTP-01 | DNS-01 |
| วิธีการ | สร้างไฟล์ที่ /.well-known/acme-challenge/ | สร้าง TXT Record ใน DNS |
| ต้องการ | Ingress + Port 80 เปิด | DNS Provider API Access |
| Wildcard | ไม่ได้ | ได้ |
| ง่ายในการ Setup | ง่ายกว่า | ต้อง Config DNS Provider |
| Internal Cluster | ไม่ได้ (ต้อง Public) | ได้ (ต้อง DNS เข้าถึง) |
| แนะนำ | Single Domain, ง่าย | Wildcard, Multiple Subdomains |
Let's Encrypt Staging vs Production
# ทำไมต้องใช้ Staging ก่อน?
#
# Let's Encrypt Production มี Rate Limits:
# - 50 Certificates per Registered Domain per week
# - 5 Duplicate Certificates per week
# - 300 New Orders per account per 3 hours
#
# Staging ไม่มี Rate Limits (เหมาะสำหรับ Testing)
# แต่ Certificate ไม่ Trusted (Browser จะเตือน)
#
# ขั้นตอนที่ถูกต้อง:
# 1. ตั้งค่ากับ letsencrypt-staging ก่อน
# 2. ตรวจสอบว่า Certificate ออกได้สำเร็จ
# 3. เปลี่ยนเป็น letsencrypt-prod
# 4. ลบ Certificate/Secret เก่าของ Staging
# 5. cert-manager จะออก Certificate ใหม่จาก Production
Certificate Resource
# สร้าง Certificate Resource โดยตรง:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-com-tls
namespace: production
spec:
secretName: example-com-tls-secret
duration: 2160h # 90 วัน
renewBefore: 720h # Renew ก่อนหมดอายุ 30 วัน
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
commonName: example.com
dnsNames:
- example.com
- www.example.com
- api.example.com
# ตรวจสอบ:
kubectl get certificate -n production
# NAME READY SECRET AGE
# example-com-tls True example-com-tls-secret 5m
kubectl describe certificate example-com-tls -n production
# → ดู Status, Events, Conditions
# ดู Secret:
kubectl get secret example-com-tls-secret -n production
# → มี tls.crt และ tls.key
cert-manager + Ingress (Annotation-based)
# วิธีง่ายที่สุด: ใช้ Annotation ใน Ingress
# cert-manager จะสร้าง Certificate อัตโนมัติ!
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
namespace: production
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod" # ← แค่เพิ่มบรรทัดนี้!
spec:
ingressClassName: nginx
tls:
- hosts:
- example.com
- www.example.com
secretName: example-com-tls # cert-manager สร้างให้อัตโนมัติ
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app
port:
number: 80
# แค่เพิ่ม annotation + tls section
# cert-manager ทำทุกอย่างให้:
# 1. สร้าง Certificate Resource
# 2. สร้าง CertificateRequest
# 3. สร้าง Order
# 4. สร้าง Challenge (HTTP-01)
# 5. ออก Certificate
# 6. เก็บใน Secret
# 7. Auto-Renew ก่อนหมดอายุ
Wildcard Certificates with DNS-01
# Wildcard Certificate ต้องใช้ DNS-01 Challenge
# ตัวอย่างกับ Cloudflare:
# 1. สร้าง Cloudflare API Token Secret:
kubectl create secret generic cloudflare-api-token \
--from-literal=api-token=YOUR_CLOUDFLARE_API_TOKEN \
-n cert-manager
# 2. ClusterIssuer กับ DNS-01 (Cloudflare):
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod-dns
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-prod-dns-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
# 3. Certificate (Wildcard):
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-example-com
namespace: production
spec:
secretName: wildcard-example-com-tls
issuerRef:
name: letsencrypt-prod-dns
kind: ClusterIssuer
dnsNames:
- example.com
- "*.example.com" # ← Wildcard!
# DNS-01 Providers ที่ cert-manager Support:
# Cloudflare, Route53 (AWS), Cloud DNS (GCP),
# Azure DNS, DigitalOcean, Akamai,
# RFC2136 (BIND), Webhook (Custom)
Monitoring Certificate Expiry
# cert-manager มี Prometheus Metrics:
# cert-manager เปิด Metrics ที่ port 9402
# Metrics สำคัญ:
# certmanager_certificate_ready_status
# certmanager_certificate_expiration_timestamp_seconds
# certmanager_certificate_renewal_timestamp_seconds
# Prometheus Alert Rule:
# alert: CertificateExpiringSoon
# expr: certmanager_certificate_expiration_timestamp_seconds - time() < 7*24*60*60
# for: 1h
# labels:
# severity: warning
# annotations:
# summary: "Certificate expiring in less than 7 days"
# ตรวจสอบด้วย cmctl:
cmctl status certificate example-com-tls -n production
# → แสดง Expiry Date, Renewal Date, Status
# ตรวจสอบด้วย kubectl:
kubectl get certificate --all-namespaces
# → ดู READY status ทุก Certificate
cert-manager + Istio
# ใช้ cert-manager เป็น CA สำหรับ Istio mTLS:
#
# ปกติ Istio ใช้ istiod เป็น CA (Self-Signed)
# ใช้ cert-manager แทน → จัดการ CA ได้ดีกว่า
# 1. ติดตั้ง istio-csr:
helm install istio-csr jetstack/cert-manager-istio-csr \
--namespace cert-manager \
--set "app.tls.rootCAFile=/var/run/secrets/istio-csr/ca.pem"
# 2. Istio ใช้ cert-manager CA:
# Istio workload certificates จะถูก Sign โดย cert-manager
# → จัดการ CA Rotation ได้
# → ใช้ External CA (Vault, AWS PCA) ได้
cert-manager for Internal CA
# ใช้ cert-manager สำหรับ Internal Certificate (Self-Signed CA):
# 1. สร้าง Self-Signed Issuer:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned-issuer
spec:
selfSigned: {}
# 2. สร้าง CA Certificate:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: internal-ca
namespace: cert-manager
spec:
isCA: true
commonName: Internal CA
secretName: internal-ca-secret
duration: 87600h # 10 ปี
issuerRef:
name: selfsigned-issuer
kind: ClusterIssuer
# 3. สร้าง CA Issuer:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: internal-ca-issuer
spec:
ca:
secretName: internal-ca-secret
# ใช้ internal-ca-issuer ออก Certificate สำหรับ Service ภายใน
# เหมาะกับ: Microservices mTLS, Internal APIs, Dev/Staging
Troubleshooting
# Certificate Not Ready? ทำอย่างไร:
#
# 1. ตรวจ Certificate Status:
kubectl describe certificate MY_CERT -n MY_NAMESPACE
# → ดู Conditions, Events
# 2. ตรวจ CertificateRequest:
kubectl get certificaterequest -n MY_NAMESPACE
kubectl describe certificaterequest MY_CERT-xxxxx -n MY_NAMESPACE
# 3. ตรวจ Order:
kubectl get order -n MY_NAMESPACE
kubectl describe order MY_CERT-xxxxx -n MY_NAMESPACE
# 4. ตรวจ Challenge:
kubectl get challenge -n MY_NAMESPACE
kubectl describe challenge MY_CERT-xxxxx -n MY_NAMESPACE
# → ดูว่า Challenge Pass หรือไม่
# Common Issues:
# =============================================
# 1. Challenge Failed (HTTP-01):
# - Port 80 ถูก Block?
# - Ingress Controller ทำงานปกติ?
# - DNS ชี้มาที่ Cluster?
# → ทดสอบ: curl http://example.com/.well-known/acme-challenge/test
#
# 2. Challenge Failed (DNS-01):
# - API Token ถูกต้อง?
# - API Token มีสิทธิ์แก้ไข DNS?
# - DNS Propagation ช้า?
# → ทดสอบ: dig TXT _acme-challenge.example.com
#
# 3. Rate Limited:
# - ใช้ Staging ก่อนทดสอบ!
# - ดู: https://letsencrypt.org/docs/rate-limits/
#
# 4. Secret ไม่ถูกสร้าง:
# - cert-manager Pod มี Permission?
# - RBAC ถูกต้อง?
# → kubectl logs -n cert-manager deploy/cert-manager
cert-manager + external-dns
# ใช้ cert-manager ร่วมกับ external-dns:
# external-dns สร้าง DNS Record อัตโนมัติ
# cert-manager ออก Certificate อัตโนมัติ
#
# Flow:
# 1. สร้าง Ingress → external-dns สร้าง DNS Record
# 2. DNS Record ชี้มาที่ Cluster
# 3. cert-manager ออก Certificate (HTTP-01 Challenge ผ่าน)
# → ทุกอย่างอัตโนมัติ!
# Ingress ที่ใช้ทั้ง external-dns + cert-manager:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
external-dns.alpha.kubernetes.io/hostname: "app.example.com"
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-example-com-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app
port:
number: 80
# แค่ Apply Ingress → DNS + TLS Certificate ครบอัตโนมัติ!
Best Practices
| Practice | คำอธิบาย |
| ใช้ Staging ก่อน | ทดสอบกับ Let's Encrypt Staging ก่อนเสมอ |
| ใช้ ClusterIssuer | ส่วนใหญ่ใช้ ClusterIssuer ดีกว่า Issuer |
| Monitor Expiry | ตั้ง Prometheus Alert สำหรับ Certificate ใกล้หมดอายุ |
| ใช้ Annotation-based | ใส่ Annotation ใน Ingress แทนสร้าง Certificate Resource แยก |
| Wildcard สำหรับ Subdomains มาก | ถ้ามี Subdomain > 5 ตัว ใช้ Wildcard ดีกว่า |
| Backup CA Key | ถ้าใช้ Internal CA → Backup Secret ของ CA Key |
| RBAC | จำกัดสิทธิ์ cert-manager ให้เฉพาะ Namespace ที่ต้องการ |
| Version Pinning | ใช้ Version ที่เฉพาะเจาะจง (ไม่ใช่ latest) |
สำคัญ: อย่าลืมตั้ง email ที่ใช้จริงใน ClusterIssuer เพราะ Let's Encrypt จะส่ง Email เตือนก่อน Certificate หมดอายุ (กรณี cert-manager ทำงานผิดพลาด)
สรุป
cert-manager เป็นเครื่องมือที่ขาดไม่ได้สำหรับ Kubernetes Production ช่วยจัดการ TLS Certificate ตั้งแต่การออก Certificate, Renew อัตโนมัติ, จนถึง Monitoring ทั้งหมดนี้เป็นอัตโนมัติ 100% ไม่ต้องกังวลเรื่อง Certificate Expire อีกต่อไป เพียงตั้งค่า ClusterIssuer + Annotation ใน Ingress แล้ว cert-manager จัดการให้ทุกอย่าง