AKS에 배포한 웹앱의 HTTPS 통신을 위해 인증서를 적용하려고 알아보던 중, 이전에 회사 동료분이 알려주셨던 Let’s Encrypt라는 무료 인증서 발급 사이트가 떠올라 사용하기로 했다.
자료를 찾다보니 cert-manager를 함께 사용해서 인증서를 자동 갱신하는 사례가 효율적이고 많이 사용되는 것으로 보여 본 게시물에서도 Let’s Encrypt + cert-manager 조합으로 AKS 웹앱의 인증서 자동 갱신 환경을 구성하려고 한다.
0. Overview
0.1 Let’s Encrypt? cert-manager?
- Let’s Encrypt : TLS 인증서를 무료로 제공하는 비영리 인증 기관(CA)
- cert-manager : k8s 환경에서 TLS 인증서 발급 요청 및 자동 갱신과 같은 관리 역할을 제공
- Certificate
- (Cluster) Issuer
- CertificateRequest
- Order
- Challenge
0.2 간단한 아키텍처와 흐름
출처 : https://www.nginx.com/blog/automating-certificate-management-in-a-kubernetes-environment/
- cert-manager는 Let’s Encrypt(CA)로부터 인증서를 발급받기 위해 소유한 도메인의 DNS 영역에 특수 레코드를 생성
- 레코드 등록을 마치고 Let’s Encrypt에게 자신의 도메인을 소유하고 있음을 인증하기 위한 Challenge를 요청
- Let’s Encrypt는 Challenge 요청을 수신 후 특수 도메인을 확인
- 해당 도메인에 대한 확인을 마치고 Let’s Encrypt는 cert-manager로 인증서를 공급
- cert-manager는 공급받은 인증서를 AKS Secret 내 저장
1. Labs
1.0 준비 사항
- Azure CLI 설치 ≥ v2.40.0
- kubectl 설치
- 외부 접근이 가능한 Azure DNS 영역 생성
- 외부 도메인 구매 시, 도메인 네임서버를 Azure DNS 영역의 네임 서버로 교체 필요
- Azure Kubernetes Service(AKS) 클러스터 생성
1.1 환경 변수 초기화
- 본 게시글에선 k8s 매니페스트 작성 시 환경 변수를 활용할 예정이므로 개인의 테스트 환경에 맞춰 아래 변수 초기화 단계를 진행
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # Azure 테스트 환경
export AZURE_SUBSCRIPTION=<구독 명>
export AZURE_SUBSCRIPTION_ID=$(az account show --name $AZURE_SUBSCRIPTION --query 'id' -o tsv)
export AZURE_DEFAULTS_GROUP=<리소스 그룹 명>
export AZURE_DEFAULTS_LOCATION=<리전>
export DOMAIN_NAME=<Public DNS 도메인>
# AKS 관련
export CLUSTER=<AKS 클러스터 명>
export AZURE_LOADBALANCER_DNS_LABEL_NAME=<AKS Public IP의 DNS Label 임의 지정> # EX) demo-lb
export AKS_OIDC_ISSUER="$(az aks show -n demo-cluster -gAZURE_DEFAULTS_GROUP $ --query "oidcIssuerProfile.issuerUrl" -o tsv)" # 생략 가능할듯한데
# ETC
export EMAIL_ADDRESS=<이메일 주소>
|
1.2 cert-manager 설치
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # cert-manager 설치
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm upgrade cert-manager jetstack/cert-manager \
--install \
--create-namespace \
--wait \
--namespace cert-manager \
--set installCRDs=true
# 정상 설치 여부 및 컴포넌트 확인
kubectl -n cert-manager get all # STATUS Running 여부
kubectl explain Certificate
kubectl explain CertificateRequest
kubectl explain Issuer
|
1.3 cert-manager의 Azure 인증/권한 구성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| # AKS 클러스터의 MI 인증 활성화
az extension add --name aks-preview
az aks update -g $AZURE_DEFAULTS_GROUP -n $CLUSTER \
--enable-oidc-issuer \
--enable-workload-identity \
--enable-managed-identity
# MI 생성
## cert-manager의 Azure DNS API 사용을 위한 인증 주체 생성
export USER_ASSIGNED_IDENTITY_NAME="demo-mi"
az identity create --name "${USER_ASSIGNED_IDENTITY_NAME}"
## MI가 Azure DNS 레코드를 생성할 수 있는 RBAC 할당
export USER_ASSIGNED_IDENTITY_CLIENT_ID=$(az identity show --name "${USER_ASSIGNED_IDENTITY_NAME}" --query 'clientId' -o tsv --resource-group $AZURE_DEFAULTS_GROUP)
az role assignment create \
--role "DNS Zone Contributor" \
--assignee $USER_ASSIGNED_IDENTITY_CLIENT_ID \
--scope $(az network dns zone show --name $DOMAIN_NAME -o tsv --query id)
export SERVICE_ACCOUNT_NAME=cert-manager
export SERVICE_ACCOUNT_NAMESPACE=cert-manager
export SERVICE_ACCOUNT_ISSUER=$(az aks show --resource-group $AZURE_DEFAULTS_GROUP --name $CLUSTER --query "oidcIssuerProfile.issuerUrl" -o tsv)
# Federation 생성
az identity federated-credential create \
--name 'cert-manager' \
-g $AZURE_DEFAULTS_GROUP \
--identity-name $USER_ASSIGNED_IDENTITY_NAME \
--issuer $SERVICE_ACCOUNT_ISSUER \
--subject "system:serviceaccount:${SERVICE_ACCOUNT_NAMESPACE}:${SERVICE_ACCOUNT_NAME}"
# cert-manager 라벨 구성
## values.yaml
podLabels:
azure.workload.identity/use: "true"
serviceAccount:
labels:
azure.workload.identity/use: "true"
helm upgrade cert-manager jetstack/cert-manager \
--namespace $SERVICE_ACCOUNT_NAMESPACE \
--reuse-values \
--values values.yaml
|
1.4 ClusterIssuer 및 Certificate 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| # clusterissuer-lets-encrypt-production.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-production
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: $EMAIL_ADDRESS
privateKeySecretRef:
name: letsencrypt-production
solvers:
- dns01:
azureDNS:
resourceGroupName: $AZURE_DEFAULTS_GROUP
subscriptionID: $AZURE_SUBSCRIPTION_ID
hostedZoneName: $DOMAIN_NAME
environment: AzurePublicCloud
managedIdentity:
clientID: $USER_ASSIGNED_IDENTITY_CLIENT_ID
envsubst < clusterissuer-lets-encrypt-production.yaml | kubectl apply -f -
# certificate.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: www
spec:
secretName: www-tls
privateKey:
rotationPolicy: Always
commonName: www.$DOMAIN_NAME
dnsNames:
- www.$DOMAIN_NAME
usages:
- digital signature
- key encipherment
- server auth
issuerRef:
name: letsencrypt-production
kind: ClusterIssuer
envsubst < certificate.yaml | kubectl apply -f -
# 생성된 각 오브젝트 상태 확인 (인증서 발급까지 약 1분 소요)
kubectl describe clusterissuer letsencrypt-production
cmctl status certificate www
cmctl inspect secret www-tls
|
1.5 샘플 웹앱 배포
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
| # deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: helloweb
labels:
app: hello
spec:
selector:
matchLabels:
app: hello
tier: web
template:
metadata:
labels:
app: hello
tier: web
spec:
containers:
- name: hello-app
image: us-docker.pkg.dev/google-samples/containers/gke/hello-app-tls:1.0
imagePullPolicy: Always
ports:
- containerPort: 8443
volumeMounts:
- name: tls
mountPath: /etc/tls
readOnly: true
env:
- name: TLS_CERT
value: /etc/tls/tls.crt
- name: TLS_KEY
value: /etc/tls/tls.key
volumes:
- name: tls
secret:
secretName: www-tls
kubectl apply -f deployment.yaml
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: helloweb
annotations:
service.beta.kubernetes.io/azure-dns-label-name: $AZURE_LOADBALANCER_DNS_LABEL_NAME
spec:
ports:
- port: 443
protocol: TCP
targetPort: 8443
selector:
app: hello
tier: web
type: LoadBalancer
envsubst < service.yaml | kubectl apply -f -
# LoadBalancer 외부 IP와 매핑되는 CNAME 레코드 등록
az network dns record-set cname set-record \
--zone-name $DOMAIN_NAME \
--cname $AZURE_LOADBALANCER_DNS_LABEL_NAME.$AZURE_DEFAULTS_LOCATION.cloudapp.azure.com \
--record-set-name www
# www.$DOMAIN_NAME 조회
dig www.$DOMAIN_NAME A
|
1.6 HTTPS 통신 및 인증서 확인