멀티 노드 쿠버네티스 TLS 삽질

Dierslair·2022년 7월 30일
0

kubernetes

목록 보기
5/5

쿠버네티스 클러스터를 구성하여 잘 운용하고 있다가 도메인을 변경해야 할 일이 있어 도메인을 라우터에 추가하고 TLS를 위한 인증서를 발급하는 과정에서 겪었던 오류와 삽질을 기록하기 위해 작성합니다.

Challenge types of Let's Encrypt 를 먼저 읽어보시면 이해에 도움이 됩니다.

작업은 아래 과정대로 진행했습니다.

Router에 앱 추가하기

ingress를 사용하고 있으므로 사용하고자 하는 앱의 서비스를 아래 형식에 맞게 작성해 주기만 하면 ingress 외부 요청을 애플리케이션의 서비스와 연결해 줍니다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-router
  annotations:
    kubernetes.io/ingress.class: "nginx"
    ...
spec:
  rules:
    - host: example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-application
                port:
                  number: 8080
# 여기는 인증서 발급 완료 후, 적용합니다.
#  tls:
#    - hosts: ["example.com", "www.example.com"]
#      secretName: my-application-tls

http://example.com 으로 접속하면 my-application 으로 연결됩니다.

인증서 발급 주체 등록

let's encrypt를 사용하여 인증서 발급 주체를 등록합니다. 이름은 my-application-issuer 이고, 키는 my-application-key 로 생성됩니다.

# issuer.yml
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: my-application-issuer
  namespace: default
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: my-admin@gmail.com
    privateKeySecretRef:
      name: my-application-key
    solvers:
      - selector: {}
        http01: # ACME Challenge는 HTTP-01 방식으로 수행합니다
          ingress:
            class: nginx

등록을 위해 아래 명령어를 사용합니다.

$ kubectl apply -f issuer.yaml

인증 주체와 키가 잘 생성되었는지 확인합니다.

$ kubectl get issuer
---
NAME                      READY   AGE
my-application-issuer     True    12s

$ kubectl get secret -o wide
---
NAME                    TYPE        DATA   AGE
my-application-key      Opaque      1      57s

인증서 생성 및 발급

인증서도 마찬가지로 아래 형식에 맞게 생성하면 ACME Challenge 확인을 자동으로 진행하고 인증서가 쿠버네티스 리소스의 형태로 생성됩니다.

# certificate.yml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: my-application-cert
  namespace: default
spec:
  secretName: my-application-tls # TLS 키 이름을 지정합니다.
  issuerRef:
    name: my-application-issuer # issuer.yaml 의 이름과 일치해야 합니다.
  commonName: example.com
  dnsNames:
    - example.com
    - www.example.com

마찬가지로 kubectl apply -f certificate.yml 명령어를 사용하여 발급을 진행합니다. 네트워크 상황에 따라 다르겠지만 늦어도 1분 내에는 작업이 완료됩니다.

$ kubectl get certificate -o wide
---
NAME                    READY    SECRET                 AGE
my-application-cert     False    my-application-tls     92s

뭐지.. 왜 안되지...

문제 상황

문제는 ACME Challenge 과정에서 발생했는데요, Let's Encrypt 에서 /.well-known/acme-challenge/<TOKEN> 경로를 사용하여 token 을 식별하는 과정에서 issuer.yml 에서 등록한 solver 서비스를 찾지 못해서 오류가 계속 발생하고 있었습니다.

결론부터 말씀드리면, certificate.yml 를 생성하면 위 ACME Challenge 를 처리하는 solver 서비스가 등록되는데 멀티 노드로 구성된 쿠버네티스에서 스케줄러가 ingress 가 도달할 수 없는 노드에 해당 solver 서비스를 등록하기 때문이었습니다.

ingress 서비스는 load-balancer 노드에서 동작하고 있으므로 당연히 solver 서비스도 같은 노드에 생성될 것이라고 생각했지만 쿠버네티스 스케줄러는 매우 정직하게 '여기 노는 노드가 있는데 여기 할당하는 게 맞지~' 하며 놀고 있는(부하가 적은) storage 노드에 할당하면서 ingress 서비스가 접근하지 못하는 상황에 연출되고 있었습니다.

해결 방법

해당 solver 서비스를 load-balancer 노드에 생성되도록 하기 위해 nodeAffinity 설정이 반드시 수반되어야 합니다. issuer.yml 를 수정하여 해결합니다.

# 수정된 issuer.yml
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: my-application-issuer
  namespace: default
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: my-admin@gmail.com
    privateKeySecretRef:
      name: my-application-key
    solvers:
      - selector: {}
        http01:
          ingress:
            class: nginx
            # 아래를 추가합니다
            podTemplate:
              spec:
                affinity:
                  nodeAffinity:
                    requiredDuringSchedulingIgnoredDuringExecution:
                      nodeSelectorTerms:
                        - matchExpressions:
                          - key: type
                            operator: In
                            values:
                              - load-balancer

nodeAffinity 는 보통 노드에 레이블을 할당하고 그것을 사용합니다. 저는 type=load-balancer 레이블을 로드밸런서 노드에 할당하여 사용했습니다.

$ kubectl get nodes --show-labels
---
NAME                STATUS   ROLES           AGE   VERSION   LABELS
k8s-master          Ready    control-plane   55d   v1.24.1   ...node.kubernetes.io/exclude-from-external-load-balancers=
k8s-load-balancer   Ready    <none>          55d   v1.24.1   ...kubernetes.io/os=linux,type=load-balancer
k8s-worker-1        Ready    <none>          55d   v1.24.1   ...kubernetes.io/os=linux,type=worker
k8s-worker-2        Ready    <none>          55d   v1.24.1   ...kubernetes.io/os=linux,type=worker
k8s-storage         Ready    <none>          55d   v1.24.1   ...kubernetes.io/os=linux,type=storage

쿠버네티스의 추상화는 매우 훌륭합니다만 이런 상황이 발생할 때 마다 X줄이 타는 개발자가 많으리라 생각합니다. 내부 속사정을 잘 모르기 때문에...

저는 이 오류로 인증서 발급 과정과 solver 의 존재를 배울 수 있었습니다.

profile
Java/Kotlin Backend Developer

0개의 댓글