0

Setup Canary release trên Kubernetes với Nginx Ingress và tích hợp với Github Actions

Hello các bạn lại là mình đây 👋👋

Chúc các bạn đọc của mình cuối tuần vui vẻ mát mẻ 😁, kết thúc một tuần làm việc thì nay cuối tuần ta lại có dịp được ngồi với nhau rồi đây

Tiếp tục quay trở lại với series Học Kubernetes từ cơ bản đến "gần" nâng cao. Ở bài này ta sẽ tìm hiểu cách setup Canary release trên Kubernetes với Nginx Ingress, cùng với đó là tích hợp vào CICD như cách mà các dự án thực tế hay làm nhé.

Mặc áo phao rồi lên thuyền với mình nàoooooo 🛥️🛥️

Canary release là gì?

Theo cách hiểu của mình thì đây là cách mà ta triển khai ra các tính năng mới cho một nhóm người dùng thử nghiệm ban đầu, kiểu thay vì dùng 100% traffic thì đầu tiên ta rollout (tung ra) feature mới cho 10% users, collect feedback, monitor xem có lỗi không? oke thì tiếp tục cho 20%, rồi 50%, rồi 100%

Kiểu này mình thấy cực kì hợp với những product customer facing (user thật sử dụng), vì sẽ rất nguy hiểm nếu ta đẩy 100% ra ngay lập tức, chẳng may nếu có lỗi thì toàn bộ user sẽ bị ảnh hưởng. Nếu có vấn đề trong quá trình canary release thì ta đơn giản là stop và rollback về phiên bản stable đang chạy, giảm thiểu tối đa ảnh hưởng tới các users sẵn có.

Thuật ngữ "canary release" (hay "phát hành theo kiểu chim hoàng yến") xuất phát từ cách các thợ mỏ ở châu Âu trước đây dùng chim hoàng yến để cảnh báo khí độc trong hầm mỏ. Họ mang theo chim hoàng yến khi xuống hầm, vì chim rất nhạy cảm với khí độc như carbon monoxide. Nếu chim có dấu hiệu khó thở hoặc chết, đó là cảnh báo cho thợ mỏ về môi trường nguy hiểm, giúp họ có thể thoát ra kịp thời. (nghe có câu chuyện phết ý nhỉ 😂😂)

Giờ ta zô món chính của ngày hôm nay nhé 💪💪

Lấy K8S session

Như thường lệ ở các bài trước, để thực hành thì ta cần lấy session truy cập Kubernetes cluster của mình ở đây nhé: https://learnk8s.jamesisme.com/

Nhớ tick vào Require Domain để tí nữa ta dùng với Ingress nha 😉

Tạo canary release với Nginx ingress

Setup

Để setup canary release trên Kubernetes thì có nhiều giải pháp, nhưng tiện và đơn giản nhất mình thấy có nginx ingress, vì thường ta cũng đã cài sẵn vào cluster rồi, như ta vẫn làm từ đầu series tới giờ vậy. Do đó ta cũng dùng luôn nginx nhé. 👍️

Đầu tiên các bạn tạo cho mình folder k8s-canary-release và đưa file kubernetes-config vào đó nhé, ở bài này ta sẽ thao tác hoàn toàn ở folder này.

Để bắt đầu, ta tạo file deployment.yml với nội dung như sau:

apiVersion: v1
kind: Service
metadata:
  name: myapp-svc
  labels:
    app: myapp
spec:
  type: ClusterIP
  ports:
    - port: 80
      name: http
      targetPort: http
  selector:
    app: myapp
    
---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    app: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: maitrungduc1410/sample-node:latest
          ports:
            - containerPort: 3000
              name: http
          resources:
            requests:
              memory: "64Mi"
              cpu: "100m"
            limits:
              memory: "128Mi"
              cpu: "200m"

Ở trên mình để luôn Service và Deployment chung 1 file luôn. Bên trong thì mình có những thứ rất cơ bản mà mình đã giải thích xuyên suốt series này thôi 😁😁. 1 Service + 1 Deployment có 1 replica

Sau đó ta apply nhé:

kubectl apply -f deployment.yml --kubeconfig=./kubernetes-config

>>>
service/myapp-svc created
deployment.apps/myapp created

Sau đó ta get poget svc kiểm tra xem oke chưa nha:

kubectl get po --kubeconfig=./kubernetes-config

>>>
NAME                     READY   STATUS    RESTARTS   AGE
myapp-6866c5b586-q8cvq   1/1     Running   0          5s
kubectl get svc --kubeconfig=./kubernetes-config

>>>
NAME        TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
myapp-svc   ClusterIP   10.245.17.248   <none>        80/TCP    7s

Tiếp đó ta tạo file ingress.yml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp
spec:
  ingressClassName: "nginx"
  rules:
    - host: 6b4407.learnk8s.jamesisme.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: myapp-svc
                port:
                  name: http

Ở đây ta có 1 ingress trỏ vào myapp-svc mà ta vừa tạo ở trên, nếu các bạn chưa rõ Ingress là gì thì xem lại bài Bảo mật Nginx Ingress với Cert Manager trên Kubernetes của mình nha.

Nhớ thay tên domain của các bạn vào host cho đúng nhé.

Sau đó ta apply ingress:

kubectl apply -f ingress.yml --kubeconfig=./kubernetes-config

>>>
ingress.networking.k8s.io/myapp created

Và ta get ing để kiểm tra trạng thái của ingress:

kubectl get ing --kubeconfig=./kubernetes-config

>>>
NAME    CLASS   HOSTS                           ADDRESS   PORTS   AGE
myapp   nginx   6b4407.learnk8s.jamesisme.com             80      12s

Hiện như trên là oke rồi đoá 🤩🤩

Giờ ta thử truy cập từ trình duyệt từ địa chỉ http://6b4407.learnk8s.jamesisme.com xem như thế nào nhé, để đảm bảo là trình duyệt không tự redirect HTTP->HTTPS (do settings của các bạn), thì ta mở tab ẩn danh nhé:

Screenshot 2024-11-02 at 7.13.49 PM.png

Nhớ thay tên domain của các bạn vào cho đúng nha

Thấy như này là oke rồi nà, Hostname ở đây chính là tên pod của chúng ta

À hiện tại Chrome có settings mới là sẽ cảnh báo nếu ta muốn truy cập trang web không có HTTPS (kể cả mở từ ẩn danh):

Screenshot 2024-11-02 at 6.43.39 PM.png

Ta cứ bấm Continue to site là được nhé

Oke xong bước setup rồi giờ ta mới zô món chính của chính nè 🤣🤣

Tạo canary release

Lý do mình nói setup canary release với Nginx ingress rất tiện vì họ cho phép ta cấu hình qua annotation, rất dễ dàng: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#canary

Về cơ bản là ta sẽ dùng:

  • nginx.ingress.kubernetes.io/canary: set bằng true để mark cái Ingress này là canary
  • nginx.ingress.kubernetes.io/canary-weight: trọng số, để Nginx tính tỉ lệ bao nhiêu phần trăm request sẽ ăn vào ingress rule này, mặc định tính theo thang 100 (0 -> 100), ta có thể lấy theo thang 10, 1000,... tuỳ ý

chú ý rằng "phần trăm" ở đây nó là xác suất ngẫu nhiên, chứ không phải chắc chắn cứ 100 thì có 10 hay 20, có thể hơi chênh lệch chút, ví dụweight=20 thì số request thực tế là 18 hay 22. Nhưng số request càng nhiều thì cái tỉ lệ nó sẽ càng tiệm cận weigth

Âu cây, giờ ta tạo deployment-canary.yml với nội dung như sau:

apiVersion: v1
kind: Service
metadata:
  name: myapp-svc-canary
  labels:
    app: myapp-canary
spec:
  type: ClusterIP
  ports:
    - port: 80
      name: http
      targetPort: http
  selector:
    app: myapp-canary
    
---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-canary
  labels:
    app: myapp-canary
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp-canary
  template:
    metadata:
      labels:
        app: myapp-canary
    spec:
      containers:
        - name: myapp
          image: maitrungduc1410/sample-node:v1
          ports:
            - containerPort: 3000
              name: http
          resources:
            requests:
              memory: "64Mi"
              cpu: "100m"
            limits:
              memory: "128Mi"
              cpu: "200m"

Ở trên mình copy từ deployment.yml xong sửa name/label, và đổi tag image thành V1 (để tí nữa ta test cho dễ nhìn)

Sau đó ta apply:

kubectl apply -f deployment-canary.yml --kubeconfig=./kubernetes-config

>>>
service/myapp-svc-canary created
deployment.apps/myapp-canary created

Sau đó ta get pod và service kiểm tra đảm bảo mọi thứ oke:

kubectl get po --kubeconfig=./kubernetes-config

>>>
NAME                            READY   STATUS    RESTARTS   AGE
myapp-6866c5b586-q8cvq          1/1     Running   0          23m
myapp-canary-7fbfc575cf-4fr65   1/1     Running   0          12s
kubectl get svc --kubeconfig=./kubernetes-config

>>>
NAME               TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
myapp-svc          ClusterIP   10.245.17.248   <none>        80/TCP    23m
myapp-svc-canary   ClusterIP   10.245.139.247  <none>        80/TCP    15s

Tiếp đó, ta tạo 1 ingress mới ingress-canary.yml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "30"
spec:
  ingressClassName: "nginx"
  rules:
    - host: 70a8d3.learnk8s.jamesisme.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: myapp-svc-canary
                port:
                  name: http

Ta chú ý rằng cái host là ta phải dùng y hệt, bên cạnh đó ta có thêm 2 annotations, 1 để mark cái ingress rule này là canary, cái còn lại ý bảo là "30% requests hãy route vào ingress rule này"

Chú ý rằng nếu ta không có cái annotations kia, tức là ta có 2 ingress rules y hệt nhau, chung host, khác mỗi name thì khi apply sẽ báo lỗi như sau:

Error from server (BadRequest): error when creating "ingress-canary.yml": admission webhook "validate.nginx.ingress.kubernetes.io" denied the request: host "6b4407.learnk8s.jamesisme.com" and path "/" is already defined in ingress lk8s-6b4407/myapp

Oke ròi giờ ta apply nhé:

ka -f ingress-canary.yml --kubeconfig=./kubernetes-config

>>>
ingress.networking.k8s.io/myapp-canary created

Sau đó ta get ingress:

kubectl get ing --kubeconfig=./kubernetes-config

>>>
NAME           CLASS   HOSTS                           ADDRESS             PORTS   AGE
myapp          nginx   6b4407.learnk8s.jamesisme.com   k8s.jamesisme.com   80      27m
myapp-canary   nginx   6b4407.learnk8s.jamesisme.com   k8s.jamesisme.com   80      37s

Giờ nếu ta quay lại trình duyệt và F5 liên tục sẽ thấy request sẽ chuyển giữa bản chính và bản V1:

ezgif-3-00ab88cc19.gif

Để demo rõ hơn thì ta mở terminal và chạy script sau:

for i in $(seq 1 10); do curl -s 6b4407.learnk8s.jamesisme.com | grep "Hostname"; done

>>>
Hostname: myapp-6866c5b586-q8cvq
Hostname: myapp-6866c5b586-q8cvq
Hostname: myapp-6866c5b586-q8cvq
Hostname: myapp-6866c5b586-q8cvq
Hostname: myapp-6866c5b586-q8cvq
Hostname: myapp-6866c5b586-q8cvq
Hostname: myapp-6866c5b586-q8cvq
Hostname: myapp-6866c5b586-q8cvq
Hostname: myapp-6866c5b586-q8cvq
Hostname: myapp-canary-7fbfc575cf-4fr65, V1

Thay tên domain của các bạn vào cho đúng nhé

Ta chạy command đó vài lần sẽ thấy rằng xác suất request đi vào canary release khoảng 30% như ta đã khai báo ở ingress rule. Kể ra setup cũng dễ ý nhờ 😎😎

Khi làm thực tế thì ta sẽ chạy canary release và set canary-weight tăng dần, mỗi lần chạy trong một khoảng thời gian "đủ" để ta theo dõi (monitor) các chỉ số (metrics) và xác định là "ok có thể tăng thêm traffic". Oke rồi thì ta có thể apply lại ingress và tăng canary-weight, khi nào tới 100% thì ta chỉ cần delete cái ingress myapp-canary là nó sẽ quay về sử dụng toàn bộ cái ingress myapp ban đầu ta tạo

Nhưng đương nhiên ta không muốn làm bước tăng traffic kia một cách thủ công rồi, ví dụ kiểu anh em DevOps sẽ setup trước hết, và để cho anh em dev chỉ zô bấm cái nút là được thôi. Giờ ta tới bước tiếp theo là tích hợp Canary release vào Github Actions để tự động hoá quy trình nhé. Múccccccc 💪💪💪

Tích hợp với Github Actions

Ở bước này ta sẽ làm những thứ sau:

  • Tạo CICD để thực hiện canary release (10%, 50%, 100%)
  • Nếu fail (reject) thì thực hiện rollback. Nếu success thì full release

Setup Github Actions

Đầu tiên ta tạo 1 repo mới tên là viblo-k8s-canary-release, ta nhớ tick chọn Add a README file nhé:

Screenshot 2024-11-02 at 9.39.28 PM.png

Thực tế là ta chỉ cần tạo 1 file bất kì có sẵn để lát ta mở nó từ Web IDE được thôi, chứ ta cũng không động vào file README này 😁

Screenshot 2024-11-02 at 9.40.36 PM.png

Tiếp theo chúng ta vào Settings > Actions secrets and variables > New repository secret để tạo biến môi trường lưu file kubeconfig để tí nữa bên trong CICD nó còn có permission để apply trên Kubernetes:

Screenshot 2024-11-02 at 10.07.44 PM.png

Tên biến ta để là KUBECONFIG, nội dung thì ta copy y hệt từ file kubernetes-config của các bạn từ local:

Screenshot 2024-11-02 at 10.09.03 PM.png

Sau đó ta cần tạo environment cho 10%, 50%, 100% và full release (production)

Lí do ta cần dùng tới environment là vì Github chỉ support Approval cho các job có environment. Approval là kiểu job sẽ Pending và chờ cho có người approve thì mới thực hiện, vì ở project thực tế thường là ta sẽ chạy canary release và monitor 1 khoảng thời gian sau đó người release sẽ bấm Approve để tiếp tục

Screenshot 2024-11-02 at 10.11.17 PM.png

Ta sẽ tạo các environment sau:

  • canary-10
  • canary-50
  • canary-100: 100% đã vào canary release, nhưng chưa clean up resource
  • production: clean up resource, full release

Chú ý rằng khi tạo environment thì ta cần tick chọn Required reviewers, sau đó nhập vào username của reviewer, ở đây mình nhập username của mình luôn, các bạn nên nhập của các bạn. Xong thì ta bấm Save Protection Rules, các option còn lại để mặc định:

Screenshot 2024-11-02 at 10.13.41 PM.png

Sau khi tạo environment xong thì ta có như sau:

Screenshot 2024-11-02 at 10.25.19 PM.png

Sau đó ta quay trở lại trang chính của repo và các bạn bấm dấu . (chấm) để mở Web IDE:

Screenshot 2024-11-02 at 9.41.30 PM.png

Tiếp theo ta lần lượt tạo các file manifest y như ta làm ở local:

Screenshot 2024-11-02 at 9.44.46 PM.png

nhớ thay tên domain của các bạn vào cho đúng nhé

Có 1 điểm chú ý là với ingress-canary ta sẽ không set weight cho nó, mà ta sẽ set ở phía CICD lát nữa:

Screenshot 2024-11-02 at 9.45.12 PM.png

Nếu ta không set weigth thì ingress-canary.yml rule đó sẽ không có tác dụng mà nó sẽ dùng ingress.yml

Sau đó ta tạo folder .github, trong đó tạo tiếp folder workflows, bên trong ta tạo file deploy.yml:

name: "Kubernetes Canary Release with Rollback"

on:
  workflow_dispatch: # Manually triggered for flexibility

jobs:
  canary-deployment:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Kubeconfig
        run: |
          echo "${{ secrets.KUBECONFIG }}" > ./kubernetes-config

      - name: Setup kubectl
        uses: azure/setup-kubectl@v4

      - name: Clean up if needed
        run: |
          kubectl delete -f deployment-canary.yml --kubeconfig=./kubernetes-config
          kubectl delete -f ingress-canary.yml --kubeconfig=./kubernetes-config

      - name: Deploy Stable to Kubernetes
        run: |
          kubectl apply -f deployment.yml --kubeconfig=./kubernetes-config
          kubectl apply -f ingress.yml --kubeconfig=./kubernetes-config

      - name: Deploy Canary to Kubernetes
        run: |
          kubectl apply -f deployment-canary.yml --kubeconfig=./kubernetes-config
          kubectl apply -f ingress-canary.yml --kubeconfig=./kubernetes-config

  canary-10-percent:
    runs-on: ubuntu-latest
    environment:
      name: canary-10
      url: https://6b4407.learnk8s.jamesisme.com # Your canary environment URL
    needs: canary-deployment
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Kubeconfig
        run: |
          echo "${{ secrets.KUBECONFIG }}" > ./kubernetes-config

      - name: Setup kubectl
        uses: azure/setup-kubectl@v4

      - name: Set Canary to 10% Traffic
        run: |
          kubectl annotate ingress myapp-canary nginx.ingress.kubernetes.io/canary-weight="10" --overwrite --kubeconfig=./kubernetes-config

  canary-50-percent:
    runs-on: ubuntu-latest
    environment:
      name: canary-50
    needs: canary-10-percent
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Kubeconfig
        run: |
          echo "${{ secrets.KUBECONFIG }}" > ./kubernetes-config

      - name: Setup kubectl
        uses: azure/setup-kubectl@v4

      - name: Set Canary to 50% Traffic
        run: |
          kubectl annotate ingress myapp-canary nginx.ingress.kubernetes.io/canary-weight="50" --overwrite --kubeconfig=./kubernetes-config

  canary-100-percent:
    runs-on: ubuntu-latest
    environment:
      name: canary-100
    needs: canary-50-percent
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Kubeconfig
        run: |
          echo "${{ secrets.KUBECONFIG }}" > ./kubernetes-config

      - name: Setup kubectl
        uses: azure/setup-kubectl@v4

      - name: Set Canary to 100% Traffic
        run: |
          kubectl annotate ingress myapp-canary nginx.ingress.kubernetes.io/canary-weight="100" --overwrite --kubeconfig=./kubernetes-config

  finalize-deployment:
    runs-on: ubuntu-latest
    needs: canary-100-percent
    environment: production
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Kubeconfig
        run: |
          echo "${{ secrets.KUBECONFIG }}" > ./kubernetes-config

      - name: Setup kubectl
        uses: azure/setup-kubectl@v4

      - name: Complete Deployment by removing previous deployment
        run: |
          kubectl delete -f deployment.yml --kubeconfig=./kubernetes-config
          kubectl delete -f ingress.yml --kubeconfig=./kubernetes-config
          kubectl annotate ingress myapp-canary nginx.ingress.kubernetes.io/canary- --kubeconfig=./kubernetes-config
          kubectl annotate ingress myapp-canary nginx.ingress.kubernetes.io/canary-weight- --kubeconfig=./kubernetes-config

Ở trên ta có:

  • job đầu tiên là canary-deployment sẽ luôn chạy, không cần approval: ở bước này ta sẽ deploy cả bản stable và canary, chú ý rằng vì bản canary chưa có weight nên nó sẽ chưa có tác dụng
  • --ignore-not-found=true ý là ta cứ delete resource và ignore không throw lỗi nếu không tìm thấy resource
  • Ở các bước Set up Kubeconfig ta sẽ lấy giá trị của kubeconfig ở Github Secret và viết (write) nó vào file kubernetes-config
  • Ta chỉ cần Checkout code ở các job mà có kubectl apply
  • Mỗi bước ta sẽ thực hiện trên 1 environment, vì Github chỉ support Approval cho các job có environment. Approval là kiểu job sẽ Pending và chờ cho có người approve thì mới thực hiện, vì ở project thực tế thường là ta sẽ chạy canary release và monitor 1 khoảng thời gian sau đó người release sẽ bấm Approve để tiếp tục.
  • Ở job finalize-deployment ta sẽ xoá cái deployment hiện tại và full release cái deployment của canary, chú ý ở bước này để remove annotation thì ta thêm dấu -("trừ") vào cuối của tên annotation

Xong sau đó ta Commit & Push lên master nhé, commit message ta viết gì cũng được, mình để là feat: add k8s manifest, add cicd:

Screenshot 2024-11-02 at 10.34.05 PM.png

Xong sau đó ta quay trở lại repo > Actions và Run Workflow như sau:

Screenshot 2024-11-02 at 10.30.15 PM.png

Sau khi ta start thì sẽ thấy như sau:

Screenshot 2024-11-03 at 12.05.09 AM.png

Screenshot 2024-11-03 at 12.05.16 AM.png

Và khi tới bước release canary-10 ta sẽ thấy báo waiting approval như sau:

Screenshot 2024-11-03 at 12.05.44 AM.png

Ta bấm vào Review deployments, sau đó tick chọn canary-10 và nhập comment nếu muốn, cuối cùng là ta bấm Approva and deploy

Screenshot 2024-11-03 at 12.07.48 AM.png

Sau đó ta sẽ thấy tiến trình bắt đầu:

Screenshot 2024-11-03 at 12.09.06 AM.png

Khi thành công ta sẽ thấy báo như sau:

Screenshot 2024-11-03 at 12.09.35 AM.png

Ta thấy canary-10 đã oke, có in ra cả URL nhưng Github mặc định để HTTPS, trong khi ingress của ta chưa setup HTTPS nên nếu ta mở trực tiếp sẽ thấy Chrome cảnh báo đó nha 😆

Giờ ta có thể test như ở phần trước:

for i in $(seq 1 10); do curl -s 6b4407.learnk8s.jamesisme.com | grep "Hostname"; done

>>>
Hostname: myapp-6866c5b586-4zfr8
Hostname: myapp-6866c5b586-4zfr8
Hostname: myapp-6866c5b586-4zfr8
Hostname: myapp-6866c5b586-4zfr8
Hostname: myapp-6866c5b586-4zfr8
Hostname: myapp-6866c5b586-4zfr8
Hostname: myapp-6866c5b586-4zfr8
Hostname: myapp-canary-7fbfc575cf-sdwrt, V1
Hostname: myapp-6866c5b586-4zfr8
Hostname: myapp-6866c5b586-4zfr8

vì hiện tại weight=10 nên tỉ lệ request trúng V1 khá thấp 😅😅

Ta có thể kiểm tra lại cho chắc bằng cách describe ing

kubectl describe ing myapp-canary --kubeconfig=./kubernetes-config

>>>
Name:             myapp-canary
Labels:           <none>
Namespace:        lk8s-6b4407
Address:          k8s.jamesisme.com
Ingress Class:    nginx
Default backend:  <default>
Rules:
  Host                           Path  Backends
  ----                           ----  --------
  6b4407.learnk8s.jamesisme.com  
                                 /   myapp-svc-canary:http (10.244.0.19:3000)
Annotations:                     nginx.ingress.kubernetes.io/canary: true
                                 nginx.ingress.kubernetes.io/canary-weight: 10
Events:
  Type    Reason  Age                    From                      Message
  ----    ------  ----                   ----                      -------
  Normal  Sync    4m59s (x3 over 8m39s)  nginx-ingress-controller  Scheduled for sync

oke thấy weight=10 roài 👍️👍️

Tiếp tục ta quay trở lại và start release 50% traffic nha:

Screenshot 2024-11-03 at 12.15.33 AM.png

Oke thì ta check describe ing xem nhé, sau đó ta cũng test với command này cho chắc nha:

for i in $(seq 1 10); do curl -s 6b4407.learnk8s.jamesisme.com | grep "Hostname"; done

Sau đó ta tiếp tục làm tương tự cho 100% traffic, kiểm tra oke thì ta tới bước cuối finalize-deployment:

Screenshot 2024-11-03 at 12.18.17 AM.png

Khi mọi thứ oke ta check sẽ thấy traffic hiện tại sẽ luôn vào V1:

Screenshot 2024-11-03 at 12.21.13 AM.png

get deploy cũng sẽ thấy chỉ còn 1 cái:

kubectl get deploy --kubeconfig=./kubernetes-config

>>>
NAME           READY   UP-TO-DATE   AVAILABLE   AGE
myapp-canary   1/1     1            1           16m

Rollback

Bây giờ, cái ta muốn là tại bất kì bước release canary nào, nếu người release bấm Reject (làm pipeline failed), thì ta sẽ tiến hành rollback

Giờ ta update lại file .github/workflows/deploy.yml (vẫn dùng Cloud IDE), và thêm vào 1 job mới:

name: "Kubernetes Canary Release with Rollback"

on:
  workflow_dispatch: # Manually triggered for flexibility

jobs:
  canary-deployment:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Kubeconfig
        run: |
          echo "${{ secrets.KUBECONFIG }}" > ./kubernetes-config

      - name: Setup kubectl
        uses: azure/setup-kubectl@v4

      - name: Clean up if needed
        run: |
          kubectl delete -f deployment-canary.yml --kubeconfig=./kubernetes-config --ignore-not-found=true
          kubectl delete -f ingress-canary.yml --kubeconfig=./kubernetes-config --ignore-not-found=true

      - name: Deploy Stable to Kubernetes
        run: |
          kubectl apply -f deployment.yml --kubeconfig=./kubernetes-config
          kubectl apply -f ingress.yml --kubeconfig=./kubernetes-config

      - name: Deploy Canary to Kubernetes
        run: |
          kubectl apply -f deployment-canary.yml --kubeconfig=./kubernetes-config
          kubectl apply -f ingress-canary.yml --kubeconfig=./kubernetes-config

  canary-10-percent:
    runs-on: ubuntu-latest
    environment:
      name: canary-10
      url: http://5b3604.learnk8s.jamesisme.com # Your canary environment URL
    needs: canary-deployment
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Kubeconfig
        run: |
          echo "${{ secrets.KUBECONFIG }}" > ./kubernetes-config

      - name: Setup kubectl
        uses: azure/setup-kubectl@v4

      - name: Set Canary to 10% Traffic
        run: |
          kubectl annotate ingress myapp-canary nginx.ingress.kubernetes.io/canary-weight="10" --overwrite --kubeconfig=./kubernetes-config

  canary-50-percent:
    runs-on: ubuntu-latest
    environment:
      name: canary-50
    needs: canary-10-percent
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Kubeconfig
        run: |
          echo "${{ secrets.KUBECONFIG }}" > ./kubernetes-config

      - name: Setup kubectl
        uses: azure/setup-kubectl@v4

      - name: Set Canary to 50% Traffic
        run: |
          kubectl annotate ingress myapp-canary nginx.ingress.kubernetes.io/canary-weight="50" --overwrite --kubeconfig=./kubernetes-config

  canary-100-percent:
    runs-on: ubuntu-latest
    environment:
      name: canary-100
    needs: canary-50-percent
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Kubeconfig
        run: |
          echo "${{ secrets.KUBECONFIG }}" > ./kubernetes-config

      - name: Setup kubectl
        uses: azure/setup-kubectl@v4

      - name: Set Canary to 100% Traffic
        run: |
          kubectl annotate ingress myapp-canary nginx.ingress.kubernetes.io/canary-weight="100" --overwrite --kubeconfig=./kubernetes-config

  finalize-deployment:
    runs-on: ubuntu-latest
    needs: canary-100-percent
    environment: production
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Kubeconfig
        run: |
          echo "${{ secrets.KUBECONFIG }}" > ./kubernetes-config

      - name: Setup kubectl
        uses: azure/setup-kubectl@v4

      - name: Complete Deployment by removing previous deployment
        run: |
          kubectl delete -f deployment.yml --kubeconfig=./kubernetes-config
          kubectl delete -f ingress.yml --kubeconfig=./kubernetes-config
          kubectl annotate ingress myapp-canary nginx.ingress.kubernetes.io/canary- --kubeconfig=./kubernetes-config
          kubectl annotate ingress myapp-canary nginx.ingress.kubernetes.io/canary-weight- --kubeconfig=./kubernetes-config

  rollback:
    runs-on: ubuntu-latest
    if: failure() # Executes if any of the previous jobs fail
    needs: [canary-10-percent, canary-50-percent, canary-100-percent, finalize-deployment]
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Kubeconfig
        run: |
          echo "${{ secrets.KUBECONFIG }}" > ./kubernetes-config

      - name: Rollback to Stable
        run: |
          kubectl apply -f deployment.yml --kubeconfig=./kubernetes-config
          kubectl apply -f ingress.yml --kubeconfig=./kubernetes-config

          kubectl delete -f deployment-canary.yml --kubeconfig=./kubernetes-config
          kubectl delete -f ingress-canary.yml --kubeconfig=./kubernetes-config

Ở trên ta thêm vào 1 job rollback, job này sẽ phụ thuộc vào các jobs được khai báo ở needs, và nếu bất kì job nào failed (if: failure()) thì sẽ thực hiện rollback, ở bước này ta đơn giản là apply cái release ban đầu và xoá cái canary release đi

Giờ ta lưu file deploy.yml này lại, commit, sau đó trigger Github Actions lần nữa, và ta bấm Reject ở bất kì bước nào:

Screenshot 2024-11-04 at 12.28.42 AM.png

Ta sẽ thấy job rollback sẽ chạy ngay sau đó:

Screenshot 2024-11-04 at 12.29.43 AM.png

Khi get po ta cũng sẽ chỉ thấy còn cái release ban đầu, và 100% traffic sẽ đi vào pod này:

kubectl get po --kubeconfig=./kubernetes-config

>>>
NAME                     READY   STATUS    RESTARTS   AGE
myapp-6866c5b586-thg6p   1/1     Running   0          11m

Tổng kết và thân ái

Ở bài này phần setup Github Actions mình làm ở mức khá đơn giản, trong thực tế thường là ta sẽ cần một pipeline phức tạp hơn nhiều để đảm bảo rằng quá trình thực hiện release một cách cẩn thận, chỉn chu nhất, thu thập/monitor đầy đủ metrics trước khi đẩy tính năng ra cho nhiều users dùng hơn.

Thêm một điểm nữa là vì hiện tại Github (và cả Gitlab) chỉ support Approval cho environment, chứ mình thấy đẹp nhất là support approval mà không cần environment, vì ta dùng environment cho canary release có vẻ chưa thật sự đúng 100% bản chất của environment lắm 😂

Hi vọng rằng qua bài này các bạn đã hiểu thêm về cách setup Canary release trên Kubernetes Cluster với Nginx Ingress, cùng với đó là tích hợp vào CICD để tự động hoá quy trình.

Chúc các bạn buổi tối vui vẻ và hẹn gặp lại các bạn vào những bài sau 👋👋


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí