환경: k3s v1.31+ (Traefik 내장), cert-manager v1.19+, 도메인 등록 대행: 호스팅케이알
들어가며
1편에서 Ubuntu 설치와 k3s 클러스터 구성을 마쳤습니다. 두 대의 노드가 내부 네트워크에서 정상적으로 통신하고 있지만, 아직 외부 인터넷에서는 접근할 수 없는 상태입니다. 클러스터가 집 공유기 뒤에 있기 때문입니다.
이 편에서는 공유기 포트포워딩으로 외부 트래픽을 클러스터로 전달하고, 도메인을 연결한 뒤, Let's Encrypt로 TLS 인증서를 자동 발급받아 HTTPS를 적용하는 과정을 다룹니다.
| 단계 | 목표 |
|---|---|
| 포트포워딩 | 외부 트래픽 → 공유기 → k3s 노드 |
| 도메인 연결 | mirunamu.info → 집 공인 IP |
| TLS 인증서 | cert-manager + Let's Encrypt로 HTTPS 자동 적용 |
공유기 포트포워딩
외부에서 들어오는 HTTP/HTTPS 트래픽이 집 공유기를 통과하여 k3s 클러스터에 도달하려면, 공유기에서 해당 포트를 마스터 노드의 내부 IP로 전달하도록 설정해야 합니다.
공유기 관리 페이지(보통 192.168.0.1 또는 192.168.1.1)에 접속한 뒤, 포트포워딩(또는 가상서버) 메뉴에서 다음 세 개의 규칙을 추가합니다.
| 외부 포트 | 내부 IP | 내부 포트 | 프로토콜 | 용도 |
|---|---|---|---|---|
| 80 | 192.168.0.20 | 80 | TCP | HTTP (TLS 인증서 발급 챌린지) |
| 443 | 192.168.0.20 | 443 | TCP | HTTPS (실제 서비스 트래픽) |
| 6443 | 192.168.0.20 | 6443 | TCP | Kubernetes API |
6443 포트 보안
6443은 Kubernetes API 서버 포트입니다. 외부에 열어두면 클러스터 관리 인터페이스가 인터넷에 노출되므로, 외부에서 kubectl 접근이 필요하지 않다면 이 포트는 포워딩하지 않는 것이 안전합니다. 내부 네트워크에서만 관리하는 경우에는 80, 443만 열면 충분합니다.
k3s는 Traefik을 기본 Ingress Controller로 내장하고 있어서, 80번과 443번 포트로 들어오는 트래픽은 Traefik이 받아 적절한 서비스로 라우팅합니다. 별도의 Ingress Controller 설치가 필요 없다는 것이 k3s의 장점 중 하나입니다.
도메인 연결
도메인 등록
도메인은 호스팅케이알에서 등록했습니다. .info 도메인은 .com이나 .kr에 비해 가격이 저렴하면서도 개인 프로젝트 용도로 무난합니다. 도메인 등록 대행 업체는 어디를 사용해도 상관없으며, 중요한 것은 DNS 레코드를 관리할 수 있는지 여부입니다.
A 레코드 설정
도메인을 집 공유기의 공인 IP에 연결하려면 DNS A 레코드를 설정해야 합니다. 먼저 집의 공인 IP를 확인합니다.
# 마스터 노드에서 실행
curl -s https://api.ipify.org출력된 IP를 도메인 관리 페이지에서 A 레코드로 등록합니다.
| 타입 | 호스트 | 값 | TTL |
|---|---|---|---|
| A | @ | 203.0.113.50 (공인 IP) | 300 |
| A | * | 203.0.113.50 (공인 IP) | 300 |
@는 mirunamu.info 자체를 의미하고, *는 모든 서브도메인(app.mirunamu.info, api.mirunamu.info 등)을 같은 IP로 보내는 와일드카드 레코드입니다. 서브도메인별로 다른 서비스를 배포할 계획이라면 와일드카드를 설정해두는 것이 편리합니다.
DDNS는 설정하지 않았습니다
가정용 인터넷은 ISP가 유동 공인 IP를 할당하므로, 이론적으로는 IP가 변경될 때마다 A 레코드를 갱신해주는 DDNS(Dynamic DNS)를 구성하는 것이 정석입니다. Cloudflare로 DNS를 옮기면 API를 통한 자동 갱신 스크립트를 돌릴 수 있습니다.
다만 실제로는 가정용 회선이라도 공인 IP가 수개월~수년간 유지되는 경우가 대부분이어서, 이 클러스터에서는 별도의 DDNS를 구성하지 않고 A 레코드에 공인 IP를 직접 입력해두었습니다. IP가 변경되면 수동으로 A 레코드를 갱신하는 방식으로 운영하고 있습니다.
DDNS가 필요한 경우
IP가 자주 바뀌는 환경이라면 Cloudflare로 네임서버를 옮기고 API 토큰 기반 DDNS 스크립트를 cron으로 돌리는 것을 권장합니다. Cloudflare는 무료 플랜에서도 DNS API를 제공하며, cert-manager의 DNS-01 챌린지도 함께 활용할 수 있어 일석이조입니다.
DNS 설정 후 전파까지 최대 24~48시간이 걸릴 수 있지만, TTL을 짧게 설정했다면 보통 몇 분 내에 반영됩니다. 전파 상태는 아래 명령으로 확인합니다.
# DNS 전파 확인
nslookup mirunamu.info설정한 공인 IP가 응답에 나타나면 도메인 연결이 완료된 것입니다.
cert-manager 설치
TLS 인증서를 수동으로 발급받고 갱신하는 것은 번거로운 작업입니다. cert-manager는 Kubernetes 클러스터 내에서 Let's Encrypt 인증서를 자동으로 발급하고 갱신해주는 컨트롤러입니다.
# cert-manager 설치
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.19.2/cert-manager.yaml이 명령 하나로 CRD(Custom Resource Definition), 컨트롤러, webhook까지 cert-manager 네임스페이스에 전부 배포됩니다. 설치 후 Pod가 정상적으로 올라왔는지 확인합니다.
kubectl get pods -n cert-managerNAME READY STATUS RESTARTS AGE
cert-manager-xxxxxxxxx-xxxxx 1/1 Running 0 30s
cert-manager-cainjector-xxxxxxxxx-xxxxx 1/1 Running 0 30s
cert-manager-webhook-xxxxxxxxx-xxxxx 1/1 Running 0 30s
세 개의 Pod(cert-manager, cainjector, webhook)가 모두 Running 상태가 되면 정상입니다.
ClusterIssuer 생성
cert-manager가 설치되었으면, 인증서를 어디서 발급받을지 정의하는 ClusterIssuer를 생성합니다. Let's Encrypt의 HTTP-01 챌린지를 사용하며, 이 방식은 80번 포트를 통해 도메인 소유권을 검증합니다. 앞서 포트포워딩에서 80번을 열어둔 이유가 바로 이것입니다.
Staging → Production 순서
Let's Encrypt에는 등록 도메인당 주 50회라는 인증서 발급 횟수 제한이 있습니다. 설정이 올바른지 먼저 Staging 서버로 테스트한 뒤, 정상 동작이 확인되면 Production으로 전환하는 것이 안전합니다.
# cluster-issuer-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-account-key
solvers:
- http01:
ingress:
ingressClassName: traefikingressClassName에는 클러스터에서 실제로 동작 중인 Ingress Controller의 이름을 넣어야 합니다. k3s는 Traefik을 기본으로 내장하고 있으므로 traefik을 지정합니다. 만약 --disable=traefik으로 기본 Traefik을 끄고 nginx ingress controller를 별도 설치했다면 nginx를 넣으면 됩니다.
# Staging부터 적용
kubectl apply -f cluster-issuer-staging.yaml
# 상태 확인
kubectl get clusterissuerNAME READY AGE
letsencrypt-staging True 10s
READY가 True이면 Let's Encrypt ACME 서버와의 연동이 정상적으로 완료된 것입니다.
Ingress에 TLS 적용
ClusterIssuer가 준비되었으면, 실제 서비스에 Ingress를 생성하여 HTTPS를 적용합니다. 아래는 테스트용 nginx 서비스에 TLS를 적용하는 예시입니다.
# 테스트용 nginx 배포
kubectl create deployment nginx-test --image=nginx --port=80
kubectl expose deployment nginx-test --port=80 --target-port=80이 서비스에 대한 Ingress를 생성합니다. cert-manager.io/cluster-issuer 어노테이션이 cert-manager에게 자동으로 인증서를 발급하도록 지시하는 트리거입니다.
# nginx-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-test-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
spec:
ingressClassName: traefik
rules:
- host: mirunamu.info
http:
paths:
- pathType: Prefix
path: /
backend:
service:
name: nginx-test
port:
number: 80
tls:
- hosts:
- mirunamu.info
secretName: nginx-test-tlskubectl apply -f nginx-ingress.yaml이 Ingress가 생성되면 cert-manager가 자동으로 동작합니다. 내부적으로는 다음과 같은 순서로 진행됩니다.
Certificate 리소스 자동 생성
cert-manager가 Ingress의 tls 섹션과 cluster-issuer 어노테이션을 감지하여 Certificate 리소스를 생성합니다.
HTTP-01 챌린지 수행
Let's Encrypt가 http://mirunamu.info/.well-known/acme-challenge/<token> 으로 HTTP 요청을 보냅니다. cert-manager는 이 요청을 처리할 임시 Ingress와 Pod를 자동으로 생성합니다. 앞서 포트포워딩한 80번 포트를 통해 이 요청이 클러스터에 도달합니다.
인증서 발급 및 저장
챌린지가 성공하면 Let's Encrypt가 인증서를 발급하고, cert-manager가 이를 nginx-test-tls Secret에 저장합니다. Traefik이 이 Secret을 읽어 HTTPS 트래픽을 처리합니다.
인증서 발급 상태를 확인합니다.
# 인증서 상태 확인
kubectl get certificateNAME READY SECRET AGE
nginx-test-tls True nginx-test-tls 60s
READY가 True가 되면 인증서 발급이 완료된 것입니다. 발급까지 보통 1~2분 정도 소요됩니다.
READY가 False인 경우
챌린지가 실패하면 READY가 False로 남습니다. 대부분의 원인은 다음 세 가지입니다.
- 포트포워딩 미설정: 80번 포트가 외부에서 클러스터까지 도달하지 못함
- DNS 미전파: 도메인이 아직 공인 IP로 resolve되지 않음
- ingressClassName 오류:
traefik대신 다른 값이 들어가 있음
kubectl describe certificate nginx-test-tls와 kubectl get challenges로 상세 원인을 확인할 수 있습니다.
Staging에서 Production으로 전환
Staging 인증서는 브라우저에서 "신뢰할 수 없는 인증서" 경고가 뜹니다. 이는 정상이며, Staging은 설정 검증 용도일 뿐입니다. Staging에서 READY: True를 확인했으면 Production으로 전환합니다.
# Production ClusterIssuer 적용
kubectl apply -f cluster-issuer-prod.yamlIngress의 어노테이션을 letsencrypt-prod로 변경하고, 기존 인증서 Secret을 삭제하여 재발급을 트리거합니다.
# Ingress 어노테이션을 먼저 Production으로 변경
kubectl annotate ingress nginx-test-ingress \
cert-manager.io/cluster-issuer=letsencrypt-prod --overwrite
# 기존 Staging 인증서 삭제 (Production으로 재발급 트리거)
kubectl delete secret nginx-test-tls1~2분 후 다시 kubectl get certificate로 확인하면 Production 인증서가 발급되어 있을 것입니다.
외부 접속 확인
브라우저에서 https://mirunamu.info에 접속합니다. nginx 기본 페이지가 나타나고, 주소 표시줄에 자물쇠 아이콘이 표시되면 TLS 적용이 완료된 것입니다.
# CLI로 확인
curl -v https://mirunamu.info 2>&1 | grep "subject:"subject: CN=mirunamu.info 같은 출력이 나오면 Let's Encrypt에서 발급한 정상 인증서가 적용된 것입니다.
테스트가 끝났으면 리소스를 정리합니다.
kubectl delete ingress nginx-test-ingress
kubectl delete service nginx-test
kubectl delete deployment nginx-test
kubectl delete secret nginx-test-tlscert-manager와 ClusterIssuer는 실제 서비스에서 계속 사용할 것이므로 삭제하지 않습니다.
트러블슈팅
정리
2편 요약
- 포트포워딩: 공유기에서 80(HTTP), 443(HTTPS) 포트를 마스터 노드로 전달합니다
- 도메인 + DNS: A 레코드에 집 공인 IP를 등록하여 도메인을 클러스터에 연결합니다
- cert-manager: 클러스터에 설치하여 TLS 인증서 발급과 갱신을 자동화합니다
- TLS 적용: Ingress에 ClusterIssuer 어노테이션을 추가하면 cert-manager가 Let's Encrypt 인증서를 자동으로 발급하고 Traefik이 HTTPS를 처리합니다
이것으로 홈 클러스터에 외부에서 HTTPS로 접속할 수 있는 환경이 갖춰졌습니다. 다음 편에서는 ArgoCD를 설치하여 GitHub 저장소와 연동하고, GitOps 방식으로 실제 애플리케이션을 자동 배포하는 파이프라인을 구축합니다.