Harbor 쿠버네티스로 이관하기

hbjs97·2024년 9월 2일
post-thumbnail

기존에 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 Gateway 및 Virtual Service 설정 예시

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 포트

cert-manager를 이용한 TLS 인증서 발급 및 적용

Harbor Helm chart는 TLS 인증서를 위한 certSource 옵션을 3가지 제공한다.

  • auto: 자체 서명된 인증서 사용 (브라우저에서 신뢰하지 않음)
  • secret: 사전에 생성한 인증서 Secret 사용
  • none: TLS 인증서 사용 안 함

실제 운영 환경에서는 브라우저 및 Docker 클라이언트가 신뢰 가능한 공인 CA의 인증서를 사용해야 하므로 cert-manager를 이용해 Let's Encrypt에서 인증서를 발급받고, secret 방식을 사용했다.

cert-manager 설치 및 설정 과정

1.Cloudflare API 토큰 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

2. cert-manager 설치

# 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

3. ClusterIssuer 생성

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 으로 만들어 사용

4. certificate 생성

# 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>

5. certificate 가 준비된지 확인

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로 변경했다.

이후 문제는 해결되었다.

결론

  • 복잡한 Ingress 구조는 유연한 관리 및 보안을 제공할 수 있으나, 부하가 큰 서비스에선 오히려 성능 문제가 발생할 수 있다.
  • 인증서는 cert-manager와 공인 CA를 통해 신뢰성을 확보하는 것이 중요하다.
  • 서비스 특성에 맞게 구조를 단순화하여 관리와 성능을 최적화하는 것이 효과적이다.

0개의 댓글