안녕하세요, 자바파커입니다.
"3개월마다 인증서 갱신하러 로그인해서 파일 받고 Secret 교체하다가 깜빡해서 서비스 TLS 에러 뜬 적 있습니다."
솔직히 저도 그랬습니다. Let's Encrypt 무료 인증서를 수동으로 돌리면 90일 만기 달력 알림이 울릴 때마다 배포 일 하나가 추가되고, 서비스 수가 늘어나면 "어떤 도메인이 언제 만료되는지" 파악 자체가 고역이 됩니다. 한 번 깜빡하면 NET::ERR_CERT_DATE_INVALID 배너가 사용자 화면에 뜨죠.
결론부터 말씀드리면 — cert-manager는 "Let's Encrypt 인증서 발급·갱신을 K8S 리소스로 자동화"하는 컨트롤러입니다. Ingress에 annotation 한 줄이면 HTTPS 인증서가 자동으로 발급되고, 만료 전에 알아서 갱신됩니다. 오늘은 Ingress 개념 복습부터 nginx-ingress 설치, cert-manager 셋업, Let's Encrypt 연동, 와일드카드 인증서까지 실전 순서대로 정리하겠습니다.
이전 편을 안 보셨다면 K8S에 대한 이해 · Helm 완벽 가이드 · ArgoCD GitOps 가이드 · Kustomize Overlay 가이드부터 읽으시면 흐름이 이어집니다.
Ingress 복습 — 왜 필요한가
Service는 Pod에 접근할 경로를 제공하지만, 도메인·경로 기반 라우팅·TLS 종단은 다루지 않습니다. 이 빈자리를 채우는 게 Ingress입니다.
Internet
│
▼
┌───────────────────┐
│ LoadBalancer │ ← 클라우드 LB or 노드 IP
└───────────────────┘
│
▼
┌───────────────────────────────────────────────────┐
│ Ingress Controller (nginx / traefik / haproxy) │
│ - 도메인 기반 분기 │
│ - 경로 기반 분기 │
│ - TLS 종단 처리 │
└───────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
Service A Service B Service C
(api.example) (admin.example) (blog.example)핵심 용어 구분:
| 용어 | 역할 |
|---|---|
| Ingress (리소스) | 라우팅 규칙을 선언한 K8S 오브젝트 |
| Ingress Controller | 그 규칙을 실제로 수행하는 Pod (nginx·traefik 등) |
| IngressClass | 여러 Controller 중 "이 Ingress를 누가 처리할지" 지정 |
Ingress 리소스만 만들어 놓고 Controller가 설치되지 않았으면 아무 일도 일어나지 않습니다. 처음 하는 실수 1위가 이것.
nginx-ingress 설치 — 10분 컷
가장 널리 쓰이는 ingress-nginx를 Helm으로 설치합니다.
# Helm 레포 추가
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
# 네임스페이스 + 설치 (LoadBalancer 타입 기본)
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set controller.service.type=LoadBalancer
# 외부 IP 할당 대기
kubectl get svc -n ingress-nginx --watchLoadBalancer가 Pending에서 실제 IP로 바뀌면 준비 완료. 이 IP가 앞으로 모든 Ingress의 진입점입니다.
IP 확보 후 DNS 레코드 설정
# 예시: Cloudflare·Route53·Namecheap 등에서
blog.javapark.kr A <LoadBalancer IP>
api.javapark.kr A <LoadBalancer IP>
*.dev.javapark.kr A <LoadBalancer IP> ← 와일드카드모든 서브도메인을 같은 IP로 보내고, 분기는 Ingress가 처리.
실전 팁: 관리형 K8S(EKS·GKE·AKS)는 LoadBalancer 타입이 자동으로 클라우드 LB를 프로비저닝합니다. bare-metal이나 로컬(minikube·kind)에서는
MetalLB나nodePort방식으로 바꿔야 합니다.
Ingress 기본 예제 — nginx 서비스 노출
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress
namespace: default
spec:
ingressClassName: nginx # ← Controller 지정
rules:
- host: blog.javapark.kr
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-svc
port:
number: 80이 상태에선 http://blog.javapark.kr로 nginx에 접근 가능하지만 HTTPS는 아직 없습니다. 여기서 cert-manager가 등장합니다.
cert-manager란? — 한 줄 정의
cert-manager는 K8S 클러스터 안에서 TLS 인증서 발급·갱신을 자동화하는 컨트롤러입니다. CNCF Graduated 프로젝트, 사실상 K8S HTTPS 표준.
- 인증서 발급: Let's Encrypt·ZeroSSL·HashiCorp Vault·사설 CA 모두 지원
- 자동 갱신: 만료 30일 전에 자동으로 재발급
- Secret 교체: 발급된 인증서를 K8S Secret으로 저장, Ingress가 자동 참조
핵심 CRD 3개:
| CRD | 역할 |
|---|---|
| Issuer | 특정 네임스페이스 전용 발급자 선언 |
| ClusterIssuer | 클러스터 전체 공용 발급자 — 보통 이걸 씀 |
| Certificate | "이 도메인에 대한 인증서가 필요하다"는 선언 (Ingress가 대신 만들어줌) |
cert-manager 설치
# Helm 레포 추가
helm repo add jetstack https://charts.jetstack.io
helm repo update
# 설치 (CRD 자동 설치 옵션)
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--set installCRDs=true
# 확인
kubectl get pods -n cert-manager세 개의 Pod(cert-manager, cert-manager-webhook, cert-manager-cainjector)가 모두 Running이면 준비 완료.
ClusterIssuer 만들기 — Let's Encrypt
1) Staging 먼저 (테스트용)
실전 발급 전 staging으로 먼저 테스트하세요. 실수로 Let's Encrypt rate limit(도메인당 주간 발급 제한)에 걸리면 일주일 동안 발급 불가합니다.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: you@example.com
privateKeySecretRef:
name: letsencrypt-staging-account-key
solvers:
- http01:
ingress:
class: nginx2) Production (실제 발급용)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: you@example.com
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
class: nginx차이는 server URL만. 배포:
kubectl apply -f clusterissuer-staging.yaml
kubectl apply -f clusterissuer-prod.yaml
kubectl get clusterissuer
# NAME READY AGE
# letsencrypt-staging True 10s
# letsencrypt-prod True 10s실전 — Ingress에 HTTPS 자동 발급
Ingress에 annotation + tls 섹션만 추가하면 끝.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress
namespace: default
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod # 이 한 줄
spec:
ingressClassName: nginx
tls:
- hosts:
- blog.javapark.kr
secretName: blog-tls # cert-manager가 여기에 저장
rules:
- host: blog.javapark.kr
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-svc
port:
number: 80
적용:
kubectl apply -f nginx-ingress.yaml진행 상황 추적:
# 1. Certificate 리소스 자동 생성 확인
kubectl get certificate
# NAME READY SECRET AGE
# blog-tls False blog-tls 5s ← 발급 중
# 2. Order + Challenge 확인
kubectl get order
kubectl get challenge
# 3. 성공 시
kubectl get certificate
# NAME READY SECRET AGE
# blog-tls True blog-tls 45s보통 30초~1분 내에 READY: True. 이제 https://blog.javapark.kr 접속하면 자동 발급된 인증서로 연결됩니다.
실전 팁: Certificate가 오랫동안
False면kubectl describe certificate blog-tls로 events 확인. 80%는 DNS가 아직 전파 안 됨 또는 80번 포트가 인바운드 차단된 경우.
HTTP-01 vs DNS-01 챌린지
Let's Encrypt가 "이 도메인이 정말 네 것이냐"를 검증하는 방식이 두 가지.
HTTP-01 (기본, 가장 쉬움)
cert-manager → 특정 경로에 토큰 파일 임시 배치
Let's Encrypt → http://<domain>/.well-known/acme-challenge/<token> 접근 → 확인조건:
- 도메인이 공인 IP로 연결되어 있어야 함
- 80번 포트가 공개되어야 함
불가능한 경우:
- 내부망(VPN 뒤) 도메인
- 와일드카드(
*.example.com) 인증서
DNS-01 (와일드카드·내부 도메인용)
cert-manager → DNS 공급자 API로 TXT 레코드 등록
Let's Encrypt → _acme-challenge.<domain> TXT 레코드 조회 → 확인조건:
- DNS 공급자가 API 지원 (Cloudflare·Route53·Google Cloud DNS 등)
장점: 와일드카드 가능, 내부망 가능.
DNS-01 예시 (Cloudflare)
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-api-token
namespace: cert-manager
type: Opaque
stringData:
api-token: <your-cloudflare-api-token>
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: you@example.com
privateKeySecretRef:
name: letsencrypt-dns-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token와일드카드 인증서 — 서브도메인 통합
여러 서브도메인(api.dev.javapark.kr, admin.dev.javapark.kr …)을 한 인증서로 커버.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wildcard-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt-dns # DNS-01 필수
spec:
ingressClassName: nginx
tls:
- hosts:
- "*.dev.javapark.kr"
secretName: wildcard-dev-tls
rules:
- host: api.dev.javapark.kr
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 80
- host: admin.dev.javapark.kr
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: admin-svc
port:
number: 80와일드카드 하나 발급해두면 새 서브도메인 추가 시 인증서 재발급 없이 rules만 늘리면 됩니다.
인증서 자동 갱신
cert-manager는 기본적으로 만료 30일 전에 자동 갱신합니다. 별도 설정 불필요.
직접 확인:
kubectl get certificate
# NAME READY SECRET AGE RENEWAL
# blog-tls True blog-tls 45d 2026-05-20
# 만료 시점 확인
kubectl describe certificate blog-tls | grep -A 3 "Not After"수동으로 즉시 갱신 트리거:
kubectl cert-manager renew blog-tls
# 또는
kubectl delete secret blog-tls # Secret 지우면 재발급자주 쓰는 부가 annotation
nginx-ingress 기준으로 실전에서 많이 쓰는 annotation들.
metadata:
annotations:
# cert-manager
cert-manager.io/cluster-issuer: letsencrypt-prod
# HTTPS 강제 (HTTP로 오면 301 → HTTPS)
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
# 큰 파일 업로드 (기본 1MB → 50MB)
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
# 타임아웃
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
nginx.ingress.kubernetes.io/proxy-send-timeout: "60"
# CORS
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://app.example.com"
# 라우팅 재작성
nginx.ingress.kubernetes.io/rewrite-target: /$2이 annotation들이 ingress-nginx 특화라는 점 주의. traefik·istio 쓰면 문법이 다릅니다.
자주 겪는 이슈 5가지
1) "Certificate가 READY: False에서 안 넘어간다"
kubectl describe certificate <name>→ Events 확인- 80%는 DNS 전파 안 됨 또는 80번 포트 차단
- staging으로 먼저 테스트했는지 확인 (rate limit 원인 배제)
2) "Too many failed authorizations"
Let's Encrypt rate limit 위반. 1주일 대기 또는 staging으로 전환해 검증 후 prod로 교체.
3) "Mixed content 에러"
HTTPS 페이지 안에서 HTTP 리소스 로드. force-ssl-redirect: "true" + 애플리케이션 레벨에서 절대 URL을 https로 통일.
4) "Ingress는 떴는데 404"
ingressClassName지정 누락 → Controller가 Ingress를 무시함- path·pathType 불일치 (
PrefixvsExact) - Service name·port 오타
5) "갱신이 일어나지 않음"
kubectl describe certificate→ 상태 확인- cert-manager Pod 로그:
kubectl logs -n cert-manager deploy/cert-manager
ArgoCD와 함께 쓰기
cert-manager·ingress-nginx 같은 플랫폼 컴포넌트는 ArgoCD의 별도 Application으로 선언해두면 인프라 재현이 자동화됩니다.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cert-manager
namespace: argocd
spec:
project: platform
source:
repoURL: https://charts.jetstack.io
chart: cert-manager
targetRevision: v1.16.0
helm:
values: |
installCRDs: true
destination:
server: https://kubernetes.default.svc
namespace: cert-manager
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=trueClusterIssuer는 별도 Git 레포에 YAML로 두고, App of Apps 패턴으로 같이 관리. ArgoCD 편의 패턴 그대로 재사용 가능.
FAQ
Q. ingress-nginx 말고 다른 Controller를 써야 할 때는?
- Traefik: 자동 서비스 디스커버리가 매력적. 가벼운 setup에 적합.
- Istio / Envoy Gateway: 서비스 메시가 이미 있으면 그 안에서 처리.
- AWS ALB Ingress Controller: AWS 환경에서 ALB로 직접 노출하고 싶을 때.
- Cloudflare Tunnel: 퍼블릭 IP 노출 없이 Ingress 기능 흉내.
입문은 ingress-nginx. 생태계·문서·annotation 레퍼런스가 가장 풍부.
Q. cert-manager와 Gateway API는 어떻게 다르나요?
Gateway API는 Ingress의 차세대 표준(K8S SIG). cert-manager는 현재 Gateway API와도 통합 지원 (gateway solver). 당분간 Ingress가 주력이지만, 2026~2027년경 전환기가 올 수 있습니다. 신규 프로젝트에서 Gateway API로 시작해보는 것도 좋은 선택.
Q. Let's Encrypt가 중단되면 어떻게 하나요?
cert-manager는 여러 Issuer를 병행 운영 가능. ClusterIssuer를 ZeroSSL·Google Trust Services·사설 CA 등으로 바꾸기만 하면 됨. 인증서 프로바이더 락인 없음.
Q. 사내 CA로도 가능한가요?
네. ClusterIssuer의 ca 또는 vault solver를 쓰면 사내 PKI·HashiCorp Vault와 연동되어 내부 도메인 인증서도 자동화됩니다.
Q. 한 Secret을 여러 네임스페이스에서 공유하려면?
cert-manager의 kubectl-cert-manager 플러그인 또는 reflector 컨트롤러로 Secret 자동 복제. 와일드카드 인증서 한 개를 전사에 뿌릴 때 자주 씀.
마무리 — 다음 단계
HTTPS가 수동 관리 업무에서 인프라 기본값으로 내려옵니다. 한 번 셋업하면 인증서를 다시 신경 쓸 일이 없습니다 — 이 경험 자체가 가치의 핵심.
오늘 정리 핵심:
- Ingress는 규칙, Ingress Controller는 수행자 — 둘 다 필요
- cert-manager = Issuer·ClusterIssuer·Certificate 3개 CRD
- Let's Encrypt는 staging으로 먼저 테스트, prod는 확인 후
- HTTP-01은 간단, DNS-01은 와일드카드·내부망 대응
- Ingress에 annotation + tls 섹션이면 자동 발급·갱신
- ArgoCD와 묶으면 인프라 재현까지 GitOps화
시리즈 다음 주제
- HPA·VPA로 오토스케일링 구성
- Prometheus + Grafana 모니터링 스택
- ArgoCD ApplicationSet 심화 — 멀티 클러스터 대규모 운영
- Gateway API 전환기 — 언제·어떻게 바꿀까
- K8S 네트워크 정책(NetworkPolicy)과 보안 레이어링
여러분의 클러스터에서는 인증서 갱신을 어떻게 관리하고 계신가요? 수동·스크립트·cert-manager — 어느 단계에 계신지 댓글로 공유해주세요.