K8S 9: Setup External DNS + Cert Manager + Nginx Ingress Controller Wilcard

Yêu cầu đã tạo GKE Cluster, đã mua 1 domain riêng, kiểu `your-domain.net`, đã setup service CloudDNS trong GCP console, để sử dụng dc `your-domain.net`

Table of Contents

Yêu Cầu

  • Đã tạo GKE Cluster
  • Đã mua 1 domain riêng, kiểu your-domain.net
  • Đã setup service CloudDNS trong GCP console, để sử dụng dc your-domain.net:

Cách Làm

0. Setup environment variables

Các biến này sẽ dùng xuyên suốt trong bài:

export PROJECT_ID="your-project-id"
export DOMAIN="your-domain.net"
export SUBDOMAIN="your-subdomain.your-domain.net"
export YOUR_EMAIL_ADDRESS="your-mail-address"
# Cloud DNS service account nên là unique để tránh lỗi khi issue Certificate, nên mình cho thêm hậu tố `date` vào như sau:   
export CLOUD_DNS_SA="certmng-cdns-$(date +%d%m%Y-%H)"

1. Install Helm 2

mkdir ~/environment
cd ~/environment
curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get > get_helm.sh
chmod +x get_helm.sh
./get_helm.sh

cat <<EoF > ~/environment/rbac.yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: tiller
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: tiller
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: ServiceAccount
    name: tiller
    namespace: kube-system
EoF

Sau mỗi lần xóa đi tạo lại cluster, bạn đều cần làm bước sau để install Tiller (còn gọi là helm server-side) lên cluster

kubectl apply -f ~/environment/rbac.yaml
helm init --service-account tiller

check version:

helm version
Client: &version.Version{SemVer:"v2.16.1", GitCommit:"bbdfe5e7803a12bbdf97e94cd847859890cf4050", GitTreeState:"clean"}
Server: &version.Version{SemVer:"v2.16.1", GitCommit:"bbdfe5e7803a12bbdf97e94cd847859890cf4050", GitTreeState:"clean"}

2. Install Nginx Ingress Controller

helm install -n nginx-ingress stable/nginx-ingress --namespace nginx-ingress
k get pods -A
NAMESPACE     NAME                                                       READY   STATUS    RESTARTS   AGE
nginx-ingress   nginx-ingress-controller-5cbd846c5f-nmhwz                     1/1     Running   0          20s
nginx-ingress   nginx-ingress-default-backend-576b86996d-5c4dh                1/1     Running   0          20s

3. Install External DNS

cat > ./externaldns-values.yaml <<EOF
rbac:
  create: true
provider: google
interval: "1m"
policy: sync # or upsert-only
domainFilters: [ '${DOMAIN}' ]
source: ingress
registry: txt
txt-owner-id: my-identifier
EOF

install by helm:

helm install -n external-dns stable/external-dns -f externaldns-values.yaml --namespace nginx-ingress

check:

k get pods -A
NAMESPACE      NAME                                                        READY   STATUS    RESTARTS   AGE
nginx-ingress        external-dns-6df4c8c96d-cwvl2                               1/1     Running   0          47s

Nếu bạn dự định dùng link “your-subdomain.your-domain.net” và wildcard cho “*.your-subdomain.your-domain.net” thì cần annotate nó vào service ingress-controller như sau:

kubectl annotate service nginx-ingress-controller "external-dns.alpha.kubernetes.io/hostname=${SUBDOMAIN}.,*.${SUBDOMAIN}." -n nginx-ingress --overwrite

Sau khi annotate thì bạn sẽ thấy trên CloudDNS tự động xuất hiện các A record và TXT record của domain (ảnh):

4. Setup sample app (Echo App)

Tạo 1 web app đơn giản để test việc access vào domain:

cat > ./echo-app.yaml <<EOF
apiVersion: v1
kind: Service
metadata:
  name: echo
spec:
  ports:
  - port: 80
    targetPort: 5678
  selector:
    app: echo

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo
spec:
  selector:
    matchLabels:
      app: echo
  replicas: 1
  template:
    metadata:
      labels:
        app: echo
    spec:
      containers:
      - name: echo
        image: hashicorp/http-echo
        args:
        - "-text=Echo!"
        ports:
        - containerPort: 5678
EOF

apply để install app:

k apply -f echo-app.yaml

check:

k get pods,svc -A
NAMESPACE     NAME                                                       READY   STATUS    RESTARTS   AGE
default       echo-84bb76dddf-pgtdl                                      1/1     Running   0          11s

NAMESPACE       NAME                                    TYPE           CLUSTER-IP      EXTERNAL-IP      PORT(S)                      AGE
default         service/echo                            ClusterIP      10.68.64.124    <none>           80/TCP                       24s

5. Create Ingress

Ingress sẽ route traffic từ 1 domain cụ thể vào service của Echo App

cat > ./echo-ingress.yaml <<EOF
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: echo-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: ${SUBDOMAIN}
    http:
      paths:
      - path: /
        backend:
          serviceName: echo
          servicePort: 80
  - host: "*.${SUBDOMAIN}"
    http:
      paths:
      - path: /
        backend:
          serviceName: echo
          servicePort: 80
EOF

apply ingress:

k apply -f echo-ingress.yaml

check trên browser (vào link sau your-subdomain.your-domain.net), hiển thị như sau là ok (ảnh):

🎉 Như vậy web đã có HTTP, để có HTTPS thì cần dùng Cert Manager, các bước tiếp theo sẽ setup HTTPS

6. Install Cert Manager

kubectl apply --validate=false -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.12/deploy/manifests/00-crds.yaml
kubectl create namespace cert-manager
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install \
  -n cert-manager \
  --namespace cert-manager \
  --version v0.12.0 \
  jetstack/cert-manager

check:

kubectl get pods --namespace cert-manager
NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-5fd56d487b-j862g              1/1     Running   0          2m5s
cert-manager-cainjector-6bdbb96457-9scgc   1/1     Running   0          2m5s
cert-manager-webhook-6f78788cd-x2rrs       1/1     Running   0          2m5s

Tạo key của Service Account với quyền “DNS Admin”:

gcloud config set project ${PROJECT_ID}
gcloud config set compute/region asia-northeast1
gcloud config set compute/zone asia-northeast1-a

Tạo service account:

gcloud iam service-accounts create ${CLOUD_DNS_SA} \
  --display-name "Service Account to support ACME DNS-01 challenge."

Tạo service account Key:

gcloud iam service-accounts keys create --iam-account \
  ${CLOUD_DNS_SA}@${PROJECT_ID}.iam.gserviceaccount.com /tmp/cloud-dns-key.json

Binding cái role DNS Admin vào Service account đó:

gcloud projects add-iam-policy-binding --role roles/dns.admin ${PROJECT_ID} \
  --member=serviceAccount:${CLOUD_DNS_SA}@${PROJECT_ID}.iam.gserviceaccount.com

Tạo secret dùng để issue Certificate:

kubectl create secret generic clouddns-service-account --from-file=service-account-key.json=/tmp/cloud-dns-key.json --namespace=cert-manager

check:

kubectl describe secret clouddns-service-account -n cert-manager
Name:         clouddns-service-account
Namespace:    cert-manager
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
service-account-key.json:  2335 bytes

tạo ClusterIssuer staging

cat > ./staging-issuer.yaml <<EOF
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    # The ACME server URL
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    # Email address used for ACME registration
    email: ${YOUR_EMAIL_ADDRESS}
    # Name of a secret used to store the ACME account private key
    privateKeySecretRef:
      name: letsencrypt-staging

    # ACME DNS-01 provider configurations
    solvers:
    - dns01:
        clouddns:
          # The Google Cloud project in which to update the DNS zone
          project: ${PROJECT_ID}
          # A secretKeyRef to a google cloud json service account
          serviceAccountSecretRef:
            name: clouddns-service-account
            key: service-account-key.json
EOF

apply issuer:

kubectl apply -f staging-issuer.yaml

tạo ClusterIssuer production:

cat > ./production-issuer.yaml <<EOF
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    # The ACME server URL
    server: https://acme-v02.api.letsencrypt.org/directory
    # Email address used for ACME registration
    email: ${YOUR_EMAIL_ADDRESS}
    # Name of a secret used to store the ACME account private key
    privateKeySecretRef:
      name: letsencrypt-prod

    # ACME DNS-01 provider configurations
    solvers:
    - dns01:
        clouddns:
          # The Google Cloud project in which to update the DNS zone
          project: ${PROJECT_ID}
          # A secretKeyRef to a google cloud json service account
          serviceAccountSecretRef:
            name: clouddns-service-account
            key: service-account-key.json
EOF

apply issuer:

kubectl apply -f production-issuer.yaml

check:

k get clusterissuer -A
NAME                  READY   AGE
letsencrypt-prod      True    20s
letsencrypt-staging   True    99s

Issue staging Certificate cho domain:

cat > ./echo-certificate-staging.yaml <<EOF
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
  name: echo-tls-cert-staging
  namespace: default
spec:
  secretName: echo-tls-secret-staging
  issuerRef:
    name: letsencrypt-staging
    kind: ClusterIssuer
  commonName: ${SUBDOMAIN}
  dnsNames:
    - ${SUBDOMAIN}
    - "*.${SUBDOMAIN}"
EOF

apply để issue certificate:

k apply -f echo-certificate-staging.yaml

Quá trình issue certificate phải chờ khoảng 5 phút thì mới Issued thành công được, vì Issue cho wildcard mất thời gian

Nếu thành công sẽ hiện như sau:

k describe cert echo-tls-cert-staging
Status:
  Conditions:
    Last Transition Time:  2019-12-14T14:54:05Z
    Message:               Certificate is up to date and has not expired
    Reason:                Ready
    Status:                True
    Type:                  Ready
  Not After:               2020-03-13T13:54:05Z
Events:
  Type    Reason     Age    From          Message
  ----    ------     ----   ----          -------
  Normal  GeneratedKey  5m25s  cert-manager  Generated a new private key
  Normal  Requested     5m25s  cert-manager  Created new CertificateRequest resource "echo-tls-cert-staging-2906129357"
  Normal  Issued        61s    cert-manager  Certificate issued successfully

Khi đã confirm thành công thì có thể làm bước sau (Chú ý là phải confirm chắc chắn là staging Certificate echo-tls-cert-staging đã đc issue thành công thì mới làm tiếp production Certificate, bởi vì letsencrypt production Certificate rất hạn chế số lần issue, làm đi làm lại với 1 subdomain có thể bị lỗi ko thể làm lại)

Issue production Certificate cho domain:

cat > ./echo-certificate-prod.yaml <<EOF
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
  name: echo-tls-cert-prod
  namespace: default
spec:
  secretName: echo-tls-secret-prod
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  commonName: ${SUBDOMAIN}
  dnsNames:
    - ${SUBDOMAIN}
    - "*.${SUBDOMAIN}"
EOF

apply để issue certificate:

k apply -f echo-certificate-prod.yaml

check :

k describe cert echo-tls-cert-prod	
Status:	
  Conditions:	
    Last Transition Time:  2019-12-14T14:54:05Z	
    Message:               Certificate is up to date and has not expired	
    Reason:                Ready	
    Status:                True	
    Type:                  Ready	
  Not After:               2020-03-13T13:54:05Z	
Events:	
  Type    Reason     Age    From          Message	
  ----    ------     ----   ----          -------	
  Normal  GeneratedKey  4m18s  cert-manager  Generated a new private key
  Normal  Requested     4m18s  cert-manager  Created new CertificateRequest resource "echo-tls-cert-prod-2591535419"
  Normal  Issued        5s     cert-manager  Certificate issued successfully	

7. Create Ingress with TLS

TLS ở đây sẽ sử dụng Certificate mà chúng ta đã issue thành công ở bước trước, để secure cho app của mình:

cat > ./echo-ingress-prod.yaml <<EOF
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: echo-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  tls:
  - hosts:
    - ${SUBDOMAIN}
    - "*.${SUBDOMAIN}"
    secretName: echo-tls-secret-prod
  rules:
  - host: ${SUBDOMAIN}
    http:
      paths:
      - path: /
        backend:
          serviceName: echo
          servicePort: 80
  - host: "*.${SUBDOMAIN}"
    http:
      paths:
      - path: /
        backend:
          serviceName: echo
          servicePort: 80
EOF

apply ingress:

k apply -f echo-ingress-prod.yaml

Check trên browser (vào link your-subdomain.your-domain.net), confirm có HTTPS và Certificate Valid như hình sau là OK:

Check wildcard hoạt động OK bằng cách thêm 1 prefix bất kỳ (ví dụ như test) vào link trên để thành như sau: test.your-subdomain.your-domain.net, nếu vẫn có HTTPS và Certificate Valid thì có nghĩa là wildcard đã hoạt động thành công:

8. Clean up

  • Chắc chắn rằng bạn đã xóa Cluster, VPC
  • Vào Service Cloud DNS, xóa các record được sinh ra bởi ExternalDNS
  • Vào IAM - IAM xóa member certmng-cdns-* mà bạn đã tạo
  • Vào IAM - Service Account xóa service account certmng-cdns-* mà bạn đã tạo

Done!