[Kubernetes] Traefik v2와 cert-manager 를 이용한 secure service 구현
GCP, AWS와 같은 Public cloud나 On-premise 에서도, 외부에 secure web 서비스를 제공하기 위해 http entrypoint에 대해
유효한 SSL 인증키를 적용하기 위해서는 많은 번거로운 작업 과정을 거쳐야 한다. 그 방법도 여러 가지가 존재하겠지만 여기서는
Kubernetes의 장점을 살려, 간단한 설정과 수작업이 최소화 된 간편 버전의 솔루션을 직접 구현해 보고 정리한 문서를, 따라하기 버전으로 소개하려 한다.
Traefik ingress 의 entrypoint에 대해 Let's Encrypt 로부터 유효(정식) 인증키를 적용해 주는 cert-manager(Jetstack 버전)
를 활용한 방법을 아래에 정리해 둔다.
Cert-manager 활용 - jetstack/cert-manager with Traefik v2
- 사전 요건(Pre-requisites)
- 사용 가능한 FQDN(여기서는 route53.kube.click을 사용)과 외부 접속 가능한 공인(Public) IP
- CRD(Custom Resource Definition) 적용 가능한 Kubernetes 환경(여기서는 Local, on-premise kubernetes cluster)
- Kubernetes 1.15 버전 이상 전제(그 이전 버전은 아래 Reference 4 번 째 내용 참조)
1. Install Traefik v2 & set-up DNS hosted zone
Traefik ingress 를 제공하는 Helm chart는 stable/traefik(v1)과 traefik/traefik(v2) 가 대표적이다. 여기서는 그 중 traefik/traefik 차트를 사용할 것이다. 상대적으로 Nginx ingress controller나 stable/traefik 보다는 통합적이고 단순하면서도 미래 지향적인 아키텍처와 향상된 기능, 다양한 provider들(Docker, Kubernetes, Rancher, Marathon, AWS ECS 등)을 지원하는 장점을 보인다는 정도로 이해하자.
최근 출시된 것이라 당연한 말이겠지만, 앞 선 다른 Ingress controller 들에 비해 가장 최근에 선보인 것으로 2019년 9월에 GA(Generally Available)되었고, Containous Traefiklabs에서 traefik proxy라는 이름으로 정식 출시되었다.
Traefik v2를 위한 다양한 옵션들이 존재하는데, 자세한 사항은 github.com/traefik/traefik/ 을 참고하도록 하고 다음과 같이 traefik/values.yaml 파일을 하나 작성 해 둔다.
additionalArguments:
- "--accesslog=true"
- "--accesslog.format=json"
- "--log.level=INFO"
- "--entrypoints.websecure.http.tls"
- "--providers.kubernetesIngress.ingressClass=traefik-cert-manager"
- "--ping"
- "--metrics.prometheus"
- "--api.dashboard=true"
- "--providers.kubernetescrd"
- "--providers.kubernetesingress"
deployment:
replicas: 2
Helm v3를 사용해서 traefik 네임스페이스에 Containous helm chart를 통해 traefik을 설치한다. 단, 이 방식으로 설치하면 Traefik 서비스의 Type이 LoadBalancer 방식으로 결정되며, 이 서비스에 외부에서 접속 가능한 공인 IP가 자동 할당된다.
$ helm repo add traefik https://containous.github.io/traefik-helm-chart
$ helm repo update
$ kubectl create namespace traefik
$ helm install --namespace traefik traefik traefik/traefik --values traefik/values.yaml
다음 과정에서 A record의 IP 주소로 등록하기 위해 Traefik 서비스의 External IP를 확인한다(여기서는 보안상 실제 IP를 redact 하여 20X.XXX.XXX.XX5 로 표현한다).
$ kubectl get service -n traefik
----------
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
traefik LoadBalancer 172.20.56.175 20X.XXX.XXX.XX5 80:32522/TCP,443:32428/TCP 3d7h
본 과정에서 사용할 DNS(Route53) 설정으로 가서 route53.kube.click 서브도메인 설정을 다음 캡처 화면과 같이 Wildcard DNS 방식으로 설정하여 준비해 둔다. AWS, GCP 등에서 서브도메인 설정 방법이 궁금하다면 지난 포스팅 bryan.wiki/305 를 참고해보자.
DNS의 설정이 끝났으면 host 명령을 통해 외부 접속이 가능한지 확인한다.
$ host route53.kube.click
----------
route53.kube.click has address 2XX.XXX.XXX.XX5
$ host svc.route53.kube.click
----------
svc.route53.kube.click has address 2XX.XXX.XXX.XX5
2. Access to the traefik dashboard
후반으로 가면 Dashboard 접근을 위해 Ingressroute를 생성하고 basic_auth로 로그인 가능한 작업이 진행될 테지만 여기서는 port-forward로 traefik pod의 dashboard UI로 직접 접근이 가능한지 우선 확인해 보자.
Tcp port 9000은 traefik pod의 dashboard UI에 연결되도록 설정된 것이다
# Choose only 1 pod, 'cause we deployed it with replicas=2
$ kubectl get pods -n traefik --selector "app.kubernetes.io/name=traefik" --output=name
----------
pod/traefik-68f54c4b94-cjrrl
pod/traefik-68f54c4b94-w8tnr
$ kubectl port-forward -n traefik pod/traefik-68f54c4b94-cjrrl 9001:9000
이제 웹브라우저를 통해 http://127.0.0.1:9001/dashboard/ 로 접속이 가능할 것이다.
참고로 위의 port 9000은, traefik pod의 dashboard UI에 연결되도록 설정된 것으로, 해당 포트를 알아 내려면 'kubectl get pod -n traefik traefik-68f54c4b94-cjrrl -o json' 와 같이 Pod 구성 전체를 보거나, 또는 아래와 같이 해당 port 를 jsonpath 조회를 통해 직접 찾아낼 수 있다.
kubectl get pod -n traefik traefik-68f54c4b94-cjrrl -o \
jsonpath='{.spec.containers[0].ports[?(@.name=="traefik")].containerPort}'
3. Install demo whoami application
whoami 네임스페이스를 만들고 그 내부에 whoami app 리소스를 create(apply)한다.
SSL verify 가 아직 되지 않았기 때문에, curl을 통해 접속해 볼 때는 '--insecure' 또는 '-k' 옵션을 사용해야 한다.
$ kubectl create namespace whoami
$ cat << EOF > whoami-svc-ingress.yaml
kind: Deployment
apiVersion: apps/v1
metadata:
name: whoami
namespace: whoami
spec:
replicas: 1
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: containous/whoami
imagePullPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
name: whoami
namespace: whoami
labels:
app: whoami
spec:
type: ClusterIP
ports:
- port: 80
name: whoami
selector:
app: whoami
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: whoami
namespace: whoami
spec:
entryPoints:
- websecure
routes:
- match: Host(`whoami.route53.kube.click`)
kind: Rule
services:
- name: whoami
port: 80
tls:
secretName: whoami-cert-tls
EOF
$ kubectl apply -f whoami-svc-ingress.yaml
$ curl http://whoami.route53.kube.click/
----------
404 page not found
$ curl https://whoami.route53.kube.click/
----------
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
$ curl --insecure https://whoami.route53.kube.click/
----------
Hostname: whoami-5c8d94f78-xqzvp
IP: 127.0.0.1
IP: 172.21.195.33
RemoteAddr: 172.21.195.9:57340
GET / HTTP/1.1
Host: whoami.route53.kube.click
User-Agent: curl/7.64.1
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 2XX.XXX.XXX.XX3
X-Forwarded-Host: whoami.route53.kube.click
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Server: traefik-64b966b4b7-8xh28
X-Real-Ip: 2XX.XXX.XXX.XX3
4. Install cert-manager
# Install the CustomResourceDefinition resources separately
$ kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.0.2/cert-manager.crds.yaml
# Create the namespace for cert-manager
$ kubectl create namespace cert-manager
# Add the Jetstack Helm repository
$ helm repo add jetstack https://charts.jetstack.io
# Update your local Helm chart repository cache
$ helm repo update
# Install the cert-manager Helm chart
$ helm install \
cert-manager jetstack/cert-manager \
--namespace cert-manager \
--version v1.0.2
# Verify installation
$ kubectl get pods --namespace cert-manager
----------
NAME READY STATUS RESTARTS AGE
cert-manager-7cdc47446d-z99s6 1/1 Running 0 3d10h
cert-manager-cainjector-6754f97f69-58qsr 1/1 Running 0 3d10h
cert-manager-webhook-7b56df6ddb-zkpx2 1/1 Running 0 3d10h
5. Create cluster issuer + certificate for whoami.route53.kube.click
약간의 시간이 지난 후, ACME solver Pod(cm-acme-http-solver-xxxxx)가 생성되어 cert-manager를 통해
Let's Encrypt 로부터 certificate를 발급받는 과정이 자동으로 진행된다(Log 마지막의 Error 는 Pod가 종료되면서
나타나는 정상적인 상황이므로 무시해도 된다
$ cat << EOF > whoami-cluster-issuer.yaml
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
name: letsencrypt-staging-issuer-whoami
spec:
acme:
email: my@email.id
server: https://acme-staging-v02.api.letsencrypt.org/directory
privateKeySecretRef:
# Secret resource used to store the account's private key.
name: letsencrypt-staging-whoami
solvers:
- selector:
dnsNames:
- whoami.route53.kube.click
http01:
ingress:
class: traefik-cert-manager
---
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
name: letsencrypt-prod-issuer-whoami
spec:
acme:
email: my@email.id
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
# Secret resource used to store the account's private key.
name: letsencrypt-prod-whoami
solvers:
- selector:
dnsNames:
- whoami.route53.kube.click
http01:
ingress:
class: traefik-cert-manager
EOF
cat << EOF > whoami-certificate.yaml
---
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
name: whoami-cert
namespace: whoami
annotations:
cert-manager.io/issue-temporary-certificate: "true"
spec:
commonName: whoami.route53.kube.click
secretName: whoami-cert-tls
dnsNames:
- whoami.route53.kube.click
issuerRef:
name: letsencrypt-prod-issuer-whoami
kind: ClusterIssuer
EOF
$ kubectl apply -f whoami-cluster-issuer.yaml
$ kubectl apply -f whoami-certificate.yaml
$ kubectl logs -n whoami cm-acme-http-solver-xxxxx
----------
...
I0928 16:20:08.132986 1 solver.go:72] cert-manager/acmesolver "msg"="comparing host" "base_path"="/.well-known/acme-challenge" "host"="whoami.route53.kube.click" "path"="/.well-known/acme-challenge/ZzsXXXdw" "token"="ZzsXXXdw" "expected_host"="whoami.route53.kube.click"
I0928 16:20:08.133026 1 solver.go:79] cert-manager/acmesolver "msg"="comparing token" "base_path"="/.
well-known/acme-challenge" "host"="whoami.route53.kube.click" "path"="/.well-known/acme-challenge/ZzsXXXdw" "token"="ZzsXXXdw" "expected_token"="ZzsXXXdw"
I0928 16:20:08.133049 1 solver.go:87] cert-manager/acmesolver "msg"="got successful challenge request, writing key" "base_path"="/.well-known/acme-challenge" "host"="whoami.route53.kube.click" "path"="/.well-known/acme-challenge/ZzsXXXdw" "token"="ZzsXXXdw"
Error: http: Server closed
Usage:
acmesolver [flags]
Flags:
--domain string the domain name to verify
-h, --help help for acmesolver
--key string the challenge key to respond with
--listen-port int the port number to listen on for connections (default 8089)
--token string the challenge token to verify against
http: Server closed
whoami 네임스페이스 내에 cert-manager 를 통해 생성되는 주요 resource 들이 정상 상태로 관찰되는지 확인한다.
$ kubectl describe certificate -n whoami whoami-cert
$ kubectl describe certificaterequest -n whoami whoami-cert-abcde
$ kubectl describe order -n whoami whoami-cert-abcde-1234567890
SSL 인증키가 Let's Encrypt 로부터 정상 발급되어 조회되는지 확인한다. 정상 발급이 되지 않았거나 위의 발급 요청 과정을 수행하기 전에는 'Issuer: CN=TRAEFIK DEFAULT CERT' 와 같은 결과가 나올 것이다.
$ echo | openssl s_client -showcerts -servername whoami.route53.kube.click -connect whoami.route53.kube.click:443 2> /dev/null | openssl x509 -inform pem -text | grep 'Issuer'
----------
Issuer: C=US, O=Let's Encrypt, CN=Let's Encrypt Authority X3
CA Issuers - URI:http://cert.int-x3.letsencrypt.org/
'--insecure' 옵션 없이 https 접속이 가능한지 확인한다. 웹 브라우저로도 확인해 보자. 웹주소 왼 쪽에 자물쇠 모양이 나온다면 정상.
$ curl https://whoami.route53.kube.click/
----------
Hostname: whoami-5c8d94f78-44v6m
IP: 127.0.0.1
IP: 172.21.195.128
RemoteAddr: 172.21.195.137:43042
GET / HTTP/1.1
Host: whoami.route53.kube.click
User-Agent: curl/7.64.1
Accept: _/_
Accept-Encoding: gzip
X-Forwarded-For: 2XX.XXX.XXX.XX3
X-Forwarded-Host: whoami.route53.kube.click
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Server: traefik-55c75f88-p97qd
X-Real-Ip: 2XX.XXX.XXX.XX3
6. Cleanup
$ kubectl delete -f whoami-cluster-issuer.yaml
$ kubectl delete -f whoami-svc-ingress.yaml
$ helm uninstall cert-manager --namespace cert-manager
$ kubectl delete -f https://github.com/jetstack/cert-manager/releases/download/v1.0.2/cert-manager.crds.yaml
$ helm uninstall --namespace traefik traefik
$ kubectl delete namespace traefik
$ kubectl delete namespace cert-manager
References:
community.traefik.io/t/traefik-v2-helm-a-tour-of-the-traefik-2-helm-chart/6126
medium.com/dev-genius/quickstart-with-traefik-v2-on-kubernetes-e6dff0d65216
cert-manager.io/docs/installation/kubernetes/
- Barracuda -