이미지: 브라우저의 HTTPS 요청이 게이트웨이를 지나 클러스터의 애플리케이션에 도착하는 흐름을 표현한 AI 생성 개념 이미지.
Pod와 Service를 만들었는데 다른 사람이 접속할 주소는 아직 없다. 개발 중에는 kubectl port-forward로 화면을 확인할 수 있지만, 서비스마다 터널을 열어 두는 방식만으로 여러 도메인을 운영하기는 어렵다. 이번에는 HTTP 요청을 받아 알맞은 서비스로 보내는 Ingress와, 그 입구에서 HTTPS를 처리하는 TLS 인증서를 연결한다.
시리즈 5편에서는 기존 kind 클러스터에 Traefik을 설치하고 web.lab.test라는 실습용 이름으로 웹 서버에 접속한다. HTTP 라우팅을 먼저 확인한 뒤 자체 서명 인증서를 붙이고, 인증서를 검증하는 HTTPS 요청까지 만들어 본다. 공인 도메인이나 유료 인증서는 필요 없다.
30초 요약
- Service는 Pod로 연결되는 안정적인 접점이고, Ingress는 HTTP의 도메인·경로별 연결 규칙이다.
- Ingress YAML만으로는 트래픽을 처리하지 못한다. 규칙을 실행할 Ingress Controller가 필요하다.
- 이번 실습은 Traefik과 표준
networking.k8s.io/v1Ingress를 사용한다. - HTTPS에는 서버 인증서·개인키·호스트 이름의 일치, 그리고 클라이언트의 인증서 신뢰가 필요하다.
- 로컬에서는 컨트롤러까지 port-forward로 연결한다. 인터넷 공개는 외부 IP·DNS·방화벽 설정이 추가로 필요하다.
Service와 Ingress는 무엇이 다른가
2편에서 만든 Service는 Pod가 교체되어도 같은 이름으로 접근하게 해 주었다. 그 Service를 누가, 어디에서 접근할 수 있는지는 유형과 네트워크 환경에 따라 달라진다.
| 구성 | 역할 | 로컬 실습에서 알아둘 점 |
|---|---|---|
| ClusterIP Service | 클러스터 내부 주소로 Pod에 연결 | 외부 공개 주소를 자동으로 만들지 않음 |
| NodePort Service | 각 Node의 지정 포트로 서비스 접근 제공 | kind Node는 컨테이너여서 호스트까지의 포트 연결도 필요 |
| LoadBalancer Service | 지원하는 인프라에 외부 로드밸런서 요청 | 기본 kind에는 이를 제공할 구현이 없어 외부 IP가 Pending일 수 있음 |
| Ingress + Controller | HTTP 호스트·경로를 보고 백엔드 선택 | Controller 자체에 도달하는 네트워크 경로가 별도로 필요 |
예를 들어 shop.example.com과 api.example.com이 같은 입구로 들어와도, Ingress의 host 규칙에 따라 서로 다른 Service로 보낼 수 있다. /api 같은 경로별 분기도 가능하다. Ingress는 Service의 새 유형이 아니라 그 앞에 놓는 HTTP 라우팅 규칙이다. 일반 TCP 데이터베이스를 표준 Ingress로 공개하는 방식은 이 글의 범위가 아니다. Kubernetes Service 문서
브라우저 / curl
│ HTTP Host 또는 TLS SNI: web.lab.test
▼
127.0.0.1:8080 / :8443 ← 이번 글의 로컬 터널 입구
│ kubectl port-forward
▼
Traefik Controller ← Ingress 규칙 확인, TLS 종료
│ 백엔드 Service가 선택한 엔드포인트로 전달
▼
web Service → web Pod
여기서 TLS 종료는 암호화된 연결을 Traefik에서 풀어 백엔드로 전달한다는 뜻이다. 이 예제의 Traefik→웹 서버 구간은 HTTP다. 클러스터 내부 구간까지 암호화해야 한다면 백엔드 TLS나 mTLS를 별도로 구성해야 한다.
Ingress API와 ingress-nginx는 다르다 Kubernetes는 Ingress API의 기능 확장을 동결하고 신규 설계에 Gateway API를 권장한다. Ingress 자체가 제거된 것은 아니다. 한편 Kubernetes 커뮤니티의 ingress-nginx 컨트롤러는 2026년 3월 유지보수 종료 대상으로 발표됐다. 이 글은 그 구현 대신 Traefik으로 기존 Ingress의 동작을 익힌다. 새 운영 시스템은 Gateway API 지원도 함께 검토한다.
관련 근거: Kubernetes Ingress 문서, ingress-nginx 유지보수 종료 안내.
준비 환경: 기존 클러스터를 그대로 사용하기
1편의 k8s-lab kind 클러스터와 kubectl이 필요하다. 추가 도구는 Helm, OpenSSL, curl이며 명령은 Linux·macOS·WSL의 셸 기준이다. Helm 설치는 공식 설치 안내를 따른다. OpenSSL은 -addext를 지원하는 버전을 사용한다.
kubectl config current-context
kubectl --context kind-k8s-lab get nodes
helm version --short
openssl version
curl --version
Context가 kind-k8s-lab인지 확인하고, 다른 Context라면 kubectl config use-context kind-k8s-lab으로 바꾼다. 아래 명령은 모두 이 실습 클러스터에서 실행한다. 기존 PVC를 보존하기 위해 클러스터를 다시 만들지 않는다. 2·3편의 웹 설정과 충돌하지 않도록 이번 웹 서버는 별도 Namespace에 배포한다.
mkdir -p k8s-lab/ingress
cd k8s-lab/ingress
kubectl create namespace ingress-lab
Namespace가 이미 있다면 새로 만들 필요 없다. 다음 절의 릴리스 이름 traefik-lab은 이번 글 전용이다. 같은 이름의 릴리스가 기존에 있다면 내용을 먼저 확인한다.
1. Traefik Controller 설치하기
Helm은 Kubernetes 애플리케이션을 Chart라는 묶음으로 설치하는 도구다. 아래 예제는 작성일에 공식 저장소에서 확인한 Chart 41.6.0 / Traefik v3.7.13을 기준으로 버전을 고정한다. 이후 운영 도입 때는 지원 버전·보안 업데이트·Kubernetes 호환성을 다시 확인한다.
traefik-values.yaml을 만든다.
fullnameOverride: traefik-lab
service:
type: ClusterIP
ingressClass:
enabled: true
name: traefik-lab
isDefaultClass: false
providers:
kubernetesIngress:
enabled: true
ingressClass: traefik-lab
publishedService:
enabled: false
kubernetesCRD:
enabled: false
kubernetesGateway:
enabled: false
ports:
web:
port: 8000
exposedPort: 80
websecure:
port: 8443
exposedPort: 443
표준 Ingress만 사용하므로 Traefik 전용 IngressRoute와 Gateway Provider는 끈다. 다른 Ingress에 영향을 주지 않도록 기본 IngressClass로 지정하지 않는다. publishedService를 끈 것은 이 실습에서 외부 LoadBalancer 주소를 Ingress 상태에 게시하지 않기 때문이다.
helm repo add traefik https://traefik.github.io/charts
helm repo update
helm upgrade --install traefik-lab traefik/traefik \
--version 41.6.0 \
--namespace traefik-lab --create-namespace \
--values traefik-values.yaml --wait --timeout 5m
kubectl -n traefik-lab get pods,svc
kubectl get ingressclass traefik-lab
traefik-lab Service의 80·443 포트와 traefik-lab IngressClass가 보이는지 확인한다. 설치 옵션의 근거는 Traefik Helm Chart와 Kubernetes Ingress Provider 문서다.
2. 웹 서버와 HTTP Ingress 만들기
다음 내용을 web-http.yaml로 저장한다. nginx는 여기서 응답을 돌려주는 일반 웹 서버이며, 앞에서 언급한 ingress-nginx 컨트롤러와는 다른 역할이다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: ingress-lab
spec:
replicas: 1
selector:
matchLabels:
app: ingress-web
template:
metadata:
labels:
app: ingress-web
spec:
containers:
- name: web
image: nginx:1.28-alpine
ports:
- name: http
containerPort: 80
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 2
periodSeconds: 3
---
apiVersion: v1
kind: Service
metadata:
name: web
namespace: ingress-lab
spec:
selector:
app: ingress-web
ports:
- name: http
port: 80
targetPort: http
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-http
namespace: ingress-lab
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
ingressClassName: traefik-lab
rules:
- host: web.lab.test
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
ingressClassName은 처리할 Controller를, host는 요청의 호스트 이름을 지정한다. Prefix 경로 /는 하위 경로도 받지만 경로를 자동으로 지우거나 바꾸지는 않는다. 백엔드의 port.number는 Pod 포트가 아니라 Service의 port다.
kubectl apply --dry-run=server -f web-http.yaml
kubectl apply -f web-http.yaml
kubectl -n ingress-lab rollout status deployment/web --timeout=3m
이제 별도 터미널 A에서 Controller까지 터널을 연다. 이 명령은 실행 상태로 둔다.
kubectl -n traefik-lab port-forward --address 127.0.0.1 \
service/traefik-lab 8080:80 8443:443
터미널 B에서 HTTP를 확인한다.
curl --noproxy '*' --resolve web.lab.test:8080:127.0.0.1 \
-I http://web.lab.test:8080/
예상 결과는 HTTP/1.1 200 OK다. --resolve는 해당 curl 요청에만 이름→IP 매핑을 적용한다. hosts 파일이나 실제 DNS를 바꾸지 않는다. 단순히 http://127.0.0.1:8080으로 접근하면 Host가 달라 Ingress 규칙과 맞지 않을 수 있다.
이 방식은 클러스터 밖의 로컬 PC에서 Controller를 거쳐 접속하는 실습이다. 127.0.0.1에만 열었으므로 다른 PC나 인터넷에는 공개되지 않는다.
3. 호스트 이름이 들어 있는 인증서 만들기
HTTPS는 암호화뿐 아니라 ‘접속한 이름의 서버가 맞는가’도 확인한다. 인증서의 SAN(Subject Alternative Name) 에 web.lab.test를 넣는다. CN만 적는 것으로 브라우저의 호스트 이름 검증을 만족한다고 가정하면 안 된다.
umask 077
openssl req -x509 -nodes -days 30 -newkey rsa:2048 \
-keyout tls.key -out tls.crt \
-subj '/CN=web.lab.test' \
-addext 'subjectAltName=DNS:web.lab.test'
openssl x509 -in tls.crt -noout -subject -dates -ext subjectAltName
openssl verify -CAfile tls.crt -verify_hostname web.lab.test tls.crt
실행 화면: 본문 명령으로 실제 생성한 로컬 인증서의 SAN·유효기간·호스트 이름 검증 결과. Kubernetes 배포 결과를 보여주는 화면은 아니다.
tls.key는 개인키다. Git이나 블로그에 올리지 않는다. -nodes는 이 실습용 키 파일에 암호를 걸지 않는 옵션이므로 파일 권한도 제한한다. 자체 서명 인증서는 공개 인증기관이 보증하지 않아 기본 브라우저 신뢰 저장소에서는 신뢰되지 않는다.
인증서와 키를 Ingress와 같은 Namespace의 TLS Secret에 넣는다.
kubectl -n ingress-lab create secret tls web-lab-tls \
--cert=tls.crt --key=tls.key \
--dry-run=client -o yaml | kubectl apply -f -
kubectl -n ingress-lab get secret web-lab-tls
Secret 유형은 kubernetes.io/tls이며 인증서와 키가 각각 tls.crt, tls.key 항목으로 저장된다. Secret도 접근 권한과 저장 시 암호화 설정이 필요하다. 3편의 Secret 취급 원칙을 그대로 적용한다. kubectl create secret tls 공식 문서
4. HTTPS Ingress 연결하고 인증서까지 검증하기
HTTP와 HTTPS를 분리해 비교하기 위해 web-https.yaml을 추가한다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-https
namespace: ingress-lab
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
ingressClassName: traefik-lab
tls:
- hosts:
- web.lab.test
secretName: web-lab-tls
rules:
- host: web.lab.test
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
인증서 SAN, tls.hosts, rules.host의 이름이 모두 같아야 한다. spec.tls에 적은 Secret을 Controller가 읽고 인증서를 제공한다. TLS 안의 http 필드는 백엔드 HTTP 라우팅 규칙을 뜻하므로 HTTPS Ingress에서도 그대로 사용한다. Traefik 전용 annotation의 의미는 공식 Ingress 라우팅 문서에서 확인할 수 있다.
kubectl apply --dry-run=server -f web-https.yaml
kubectl apply -f web-https.yaml
kubectl -n ingress-lab get ingress
curl --noproxy '*' --resolve web.lab.test:8443:127.0.0.1 \
--cacert tls.crt -I https://web.lab.test:8443/
--cacert tls.crt는 이번 요청에서 이 인증서를 신뢰 대상으로 지정한다. 기대 결과는 인증서 오류 없는 HTTP 200 응답이다. -k로 검증을 끄면 암호화 연결의 성공만 볼 수 있으므로 이 글의 완료 기준으로 사용하지 않는다.
TLS에서는 HTTP 요청보다 먼저 SNI(Server Name Indication)로 서버 이름을 전달한다. 따라서 IP 주소 URL에 Host 헤더만 덧붙이기보다, 위처럼 호스트 이름이 들어 있는 URL과 --resolve를 함께 쓰면 SNI와 HTTP Host를 일치시킬 수 있다.
인증서 오류를 Kubernetes와 분리해서 확인하는 방법
인증서가 잘못됐는지 Ingress 설정이 잘못됐는지 헷갈린다면, 같은 파일로 잠깐 로컬 TLS 서버를 열어 검증할 수 있다. 이 단계는 Kubernetes와 무관한 보조 실험이며, 앞의 8443 터널과 겹치지 않도록 9443을 쓴다.
# 별도 터미널에서 실행하고 유지
openssl s_server -accept 127.0.0.1:9443 \
-cert tls.crt -key tls.key -www
다른 터미널에서 실행한다. OpenSSL 예제 서버에는 HEAD 대신 일반 GET을 사용하고 응답 코드를 출력한다.
curl --noproxy '*' --resolve web.lab.test:9443:127.0.0.1 \
--cacert tls.crt -sS -o /dev/null \
-w 'HTTPS status: %{http_code}\n' https://web.lab.test:9443/
openssl verify -CAfile tls.crt -verify_hostname wrong.lab.test tls.crt
실행 화면: 로컬 OpenSSL TLS 서버에 대한 실제 curl HTTP 200 응답과 잘못된 호스트 이름의 검증 실패. Ingress를 통과한 요청은 아니며, 인증서와 클라이언트 검증만 독립적으로 확인한 화면이다.
첫 요청은 성공하고 두 번째 검증은 호스트 이름 불일치로 실패해야 한다. 여기서 성공해도 Kubernetes 설정까지 검증된 것은 아니다. 최종 확인은 반드시 앞의 8443 Traefik 경유 요청으로 한다. 보조 서버는 확인 후 Ctrl+C로 종료한다.
실제 도메인과 공인 HTTPS로 바꾸려면
실습에서는 도메인 해석을 curl이, 외부 진입 경로를 port-forward가 대신했다. 운영에서는 Controller에 도달하는 LoadBalancer 등 외부 진입점을 만들고, 실제 도메인의 DNS를 그 진입점에 연결한다. 방화벽·보안 그룹도 필요한 포트만 허용한다.
공인 인증서를 자동 발급·갱신하려면 cert-manager 같은 도구를 사용할 수 있다. 큰 흐름은 Issuer 또는 ClusterIssuer 구성 → Certificate 요청 → 도메인 소유 확인 → TLS Secret 생성·갱신 → Ingress에서 참조다. ACME HTTP-01 검증에는 인터넷에서 도달 가능한 HTTP 경로가, DNS-01에는 DNS TXT 레코드를 설정할 권한이 필요하다. web.lab.test 같은 로컬 실습 이름으로 공인 인증서를 받으려 하지 않는다. cert-manager ACME 문서
또한 TLS Secret을 붙이는 것만으로 HTTP가 HTTPS로 자동 리다이렉트되지는 않는다. 이 글은 비교를 위해 두 입구를 모두 남겼다. 운영에서는 Controller의 리다이렉트 설정과 실제 외부 HTTPS 포트까지 확인한다. 인증서 발급 성공 이후에도 갱신·만료 알림·복구 절차를 점검해야 한다.
연결이 안 될 때 확인할 순서
입구부터 백엔드 방향으로 따라가면 문제 범위를 빠르게 줄일 수 있다.
| 증상 | 우선 확인할 것 | 확인 명령·조치 |
|---|---|---|
| Connection refused | 터널 종료, 잘못된 로컬 포트 | 터미널 A의 port-forward 로그와 8080·8443 확인 |
| HTTP 404 | Host·path·IngressClass·entrypoint 불일치 | 올바른 --resolve와 호스트 URL로 다시 요청 |
| 502 또는 503 | Pod Ready 상태, Service selector·port, 백엔드 통신 | EndpointSlice와 Pod 상태 확인 |
| 인증서 신뢰 오류 | 자체 서명 인증서를 신뢰하지 않음 | 실습 인증서를 --cacert tls.crt로 지정 |
| 호스트 이름 불일치 | URL 이름과 인증서 SAN 불일치 | SAN과 rules.host, tls.hosts 비교 |
| Traefik 기본 인증서가 보임 | TLS Secret 누락·이름·Namespace 오류, SNI 불일치 | Secret 존재와 Controller 로그 확인 |
| Ingress ADDRESS가 비어 있음 | 로컬 설치에서 외부 주소를 게시하지 않음 | ADDRESS만 보지 말고 실제 curl 결과로 판단 |
kubectl -n ingress-lab describe ingress web-https
kubectl -n ingress-lab get pods,svc
kubectl -n ingress-lab get endpointslices \
-l kubernetes.io/service-name=web
kubectl -n ingress-lab get secret web-lab-tls
kubectl -n traefik-lab logs deployment/traefik-lab --tail=100
EndpointSlice에 준비된 웹 Pod 주소가 없다면 Ingress보다 Deployment·Service 연결을 먼저 해결한다. Secret을 확인할 때 -o yaml로 개인키까지 화면에 출력할 필요는 없다. 인증서가 교체된 뒤에는 새 연결로 다시 요청해 실제 제공되는 인증서를 확인한다.
실습 리소스 정리하기
두 터미널의 port-forward와 보조 TLS 서버를 Ctrl+C로 종료한다. 아래 삭제는 이번 실습 전용 리소스를 더 이상 사용하지 않을 때만 실행한다. 4편의 PostgreSQL PVC에는 손대지 않는다.
kubectl delete -f web-https.yaml
kubectl delete -f web-http.yaml
kubectl -n ingress-lab delete secret web-lab-tls
helm uninstall traefik-lab -n traefik-lab
Namespace와 로컬 인증서 파일은 자동으로 지우지 않는다. 다른 리소스가 없는지 확인한 뒤 따로 정리한다. Helm uninstall 뒤에도 Chart가 설치한 CRD가 남을 수 있으며 다른 릴리스가 공유할 수 있으므로 일괄 삭제하지 않는다.
한 줄 결론
다음 6편에서는 배포 이후의 운영을 다룬다. Probe·리소스 설정부터 스케일링, 롤링 업데이트·롤백, 로그와 Event로 장애 원인을 찾는 순서를 이어서 살펴본다.
시리즈: Kubernetes 실전 사용법 시리즈 집필 계획 · 이전 글: Pod가 사라져도 데이터는 남게 - Volume과 PVC 사용하기
참고 자료
'Server > k8s' 카테고리의 다른 글
| Pod가 사라져도 데이터는 남게: Volume과 PVC 사용하기 (0) | 2026.09.21 |
|---|---|
| 설정을 이미지에서 분리하자: ConfigMap과 Secret 사용하기 (0) | 2026.09.19 |
| 컨테이너를 서비스로: Pod·Deployment·Service로 애플리케이션 배포하기 (0) | 2026.09.08 |
| Docker 다음 단계: 로컬에서 Kubernetes 클러스터 시작하기 (0) | 2026.09.05 |
| 숫자를 한눈에: Grafana 설치부터 첫 대시보드까지 (0) | 2026.08.28 |


