기존에 Docker Swarm에서 운영되던 Harbor를 Kubernetes로 이관하면서 발생한 TLS 인증서 관련 문제와 그 해결 과정을 정리한다.
이관에는 goharbor Helm chart를 사용했다.
배포된 Harbor에 이미지를 푸시하면 트래픽이 아래 과정을 거쳐 최종 서비스(Harbor의 nginx 서비스)에 도달했다.
Client → Cloudflare → Traefik → Istio Ingress Gateway → Istio Virtual Service → Harbor (nginx 서비스)
이 과정에서 Harbor nginx 서비스와의 최종 통신 시 TLS 인증서가 없어서 이미지 푸시가 실패하는 문제가 발생했다.
Traefik에서 HTTP 라우팅 방식을 사용하면 Cloudflare에서 TLS 종료 후 다시 암복호화를 수행하게 되어 성능상 낭비가 있었다.
이를 방지하기 위해 Traefik에서 TCP 규칙을 이용한 TLS Passthrough 방식을 사용했다.
TLS Passthrough를 선택하면 중간에서 TLS 세션이 종료되지 않고, 최종 목적지(Harbor)까지 end-to-end로 TLS 연결을 유지하여 불필요한 부하를 줄일 수 있다.
Traefik 설정 예시는 아래와 같다.

Traefik의 entryPoints는 대부분의 문서에서 websecure로 표기하지만, 기존에 사용 중이던 traefik.yml에서 443 포트가 https로 선언되어 있었으므로 그대로 사용했다.
주의사항:
만약websecure로 강제 선언하거나 443 포트를 중복 선언할 경우 Traefik 전체 프록시가 정상 동작하지 않으므로 주의해야 한다.
Istio는 아래와 같이 TLS Passthrough로 설정했다.
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: istio-ingressgateway
namespace: istio-ingress
spec:
selector:
istio: ingress
servers:
- hosts:
- '<YOUR_HARBOR_DOMAIN>'
port:
number: 443
name: https
protocol: HTTPS
tls:
mode: PASSTHROUGH
- hosts:
- '*'
port:
number: 80
name: http
protocol: HTTP
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: harbor-virtualservice
namespace: istio-system
spec:
hosts:
- <YOUR_HARBOR_DOMAIN>
gateways:
- istio-ingress/istio-ingressgateway
tls:
- match:
- port: 443
sniHosts:
- <YOUR_HARBOR_DOMAIN>
route:
- destination:
host: harbor.harbor.svc.cluster.local # Harbor 서비스의 FQDN
port:
number: 443 # Harbor NGINX의 HTTPS 포트
Harbor Helm chart는 TLS 인증서를 위한 certSource 옵션을 3가지 제공한다.
auto: 자체 서명된 인증서 사용 (브라우저에서 신뢰하지 않음)secret: 사전에 생성한 인증서 Secret 사용none: TLS 인증서 사용 안 함실제 운영 환경에서는 브라우저 및 Docker 클라이언트가 신뢰 가능한 공인 CA의 인증서를 사용해야 하므로 cert-manager를 이용해 Let's Encrypt에서 인증서를 발급받고, secret 방식을 사용했다.


apiVersion: v1
kind: Secret
metadata:
name: cloudflare-api-token-secret
namespace: cert-manager
type: Opaque
stringData:
api-token: "<발급받은 Cloudflare API 토큰>"
kubectl apply -f cloudflare-api-token-secret.yaml
# values.yaml
cert-manager:
crds:
enabled: true
helm repo add cert-manager https://charts.jetstack.io
helm repo update
kubectl create namespace cert-manager
helm install cert-manager cert-manager/cert-manager --version 1.15.3 -f values.yaml -n cert-manager
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: <YOUR_EMAIL>
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- dns01:
cloudflare:
email: <YOUR_EMAIL>
apiTokenSecretRef:
name: cloudflare-api-token-secret
key: api-token # cloudflare 에서 발급받은 api 토큰을 secret 으로 만들어 사용
# cluster-issuer.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: <YOUR_CERT_NAME>
namespace: harbor
spec:
secretName: <YOUR_TLS_NAME>
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
commonName: <YOUR_HARBOR_DOMAIN>
dnsNames:
- <YOUR_HARBOR_DOMAIN>
kubectl get certificates -A
NAMESPACE NAME READY SECRET AGE
harbor <YOUR_CERT_NAME> True <YOUR_TLS_NAME> 71m
주의사항
ClusterIssuer 와 cloudflare-api-token-secret 는 cert-manager 네임스페이스에 위치해야한다. 클러스터 전역적으로 사용되는 것으로 보이며, harbor 에서 사용할 ClusterIssuer 만 harbor 네임스페이스에 위치시킨다.
만약 issuer, secret 을 harbor 네임스페이스에 같이 배포하면 certificate 요청에 실패한다.
TeamCity(CI)에서 Harbor로 이미지 푸시 중 빈번히 실패하는 문제가 발생했다.
...
20:47:10 [5A [0J
20:47:10 [1AGET https://<YOUR_HARBOR_DOMAIN>/v2/ failed and will be retried
20:47:10 Executing tasks:
20:47:10 [====== ] 20.8% complete
20:47:10 > authenticating push to <YOUR_HARBOR_DOMAIN>
20:47:10 > launching base image layer pullers
…
20:47:31 [15A [0J
20:47:31 [1AHEAD https://<YOUR_HARBOR_DOMAIN>/v2/<PROJECT>/<REPOSITORY>/blobs/sha256:e5632816f8bda0f55689c81ba066a15544fb88b931b41f81da366fbb587b5c8c failed and will be retried
20:47:31 Executing tasks:
20:47:31 [========= ] 28.3% complete
20:47:31 > checking base image layer sha256:63d23e9cf3a3...
20:47:31 > checking base image layer sha256:25f4495e3c09...
20:47:31 > scheduling building manifests
20:47:31 > checking base image layer sha256:bec78f29d94d...
20:47:31 > pushing blob sha256:9fc3152e74dc331514ec59447...
20:47:31 > pushing blob sha256:6466ba806b58864a9565f7845...
20:47:31 > scheduling pushing container configurations
20:47:31 > pushing blob sha256:e5632816f8bda0f55689c81ba...
20:47:31 > scheduling pushing manifests
20:47:31 > pushing blob sha256:d3d100be985e20ada92abc0c4...
20:47:31 > pushing blob sha256:728ed75daf7f3c7aaddb09a1b...
20:47:31 > pushing blob sha256:95fefa52dfd13bfe5e5697667...
...
원인은 Istio Ingress Pod의 높은 부하였다. 이미지 푸시 트래픽이 몰리면서 Pod가 빈번히 Scale-out 되면서 성능 병목이 발생했다.
결국, Harbor와 같은 트래픽이 많고 성능이 중요한 서비스는 복잡한 Ingress 구조를 거치는 것보다는 직접 LoadBalancer로 노출하는 것이 관리나 성능상 유리하다고 판단하여 Istio Ingress Gateway를 제거하고 Harbor 서비스의 타입을 clusterIP에서 loadBalancer로 변경했다.
이후 문제는 해결되었다.