쿠버네티스 클러스터를 구성하여 잘 운용하고 있다가 도메인을 변경해야 할 일이 있어 도메인을 라우터에 추가하고 TLS를 위한 인증서를 발급하는 과정에서 겪었던 오류와 삽질을 기록하기 위해 작성합니다.
Challenge types of Let's Encrypt 를 먼저 읽어보시면 이해에 도움이 됩니다.
작업은 아래 과정대로 진행했습니다.
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
의 존재를 배울 수 있었습니다.