2022年1月12日 星期三

EKS / Kubernetes: 使用 Helm 做升級管理與 CI/CD (Upgrade deployed container version by Helm and Code Build)

本篇紀錄讓 k8s 走入 CI/CD 化流程的過程。

佈署 Kubernetes 一陣子之後,對現有的軟體開始有 AWS CodeBuild CI/CD 自動化的需求,目前常見有幾種作法可以應對 CI/CD 的問題,這也可以算是情景分類:


  • (A) 佈署由應用程式端的專案團隊處理
    • 真正的意思: kubernets manifest 檔案都跟專案放在一起
    • 專案端持有的檔案:
      • 全部的 k8s manifest
      • build 的 dockerfile
    • DevOps 持有的檔案:
      • Ingress 端...等需要 certificate, cloud resource 的服務
  • (B) 佈署統一由 DevOps 進行處理
    • 真正的意思: kubernetes manifest 檔案在 DevOps 手上
    • 專案端持有的檔案:
      • build 的 dockerfile
    • DevOps 持有的檔案:
      • 全部的 k8s manifest

如果是 (A) 情境下的自動化佈署,其實十分容易,CodeBuild 直接 apply 就好,請參考 CodeBuild buildspec:

version: 0.2

phases:
  install:
    commands:
      # 私人的 Go Library Repository 如果有使用 AWS CodeCommit,需要這段進行前處理, go get, go download 才可以載得到私人倉庫
      - git config --global credential.helper '!aws codecommit credential-helper $@'
      - git config --global credential.UseHttpPath true
      - go env -w GOPRIVATE=git-codecommit.ap-southeast-1.amazonaws.com

      # install EKS / kubectl to codebuild
      - echo Installing app dependencies...
      # other version can check here: https://docs.aws.amazon.com/zh_tw/eks/latest/userguide/install-kubectl.html
      - curl -o kubectl https://amazon-eks.s3.us-west-2.amazonaws.com/1.20.4/2020-02-22/bin/linux/amd64/kubectl   
      - chmod +x ./kubectl
      - mv ./kubectl /usr/local/bin/kubectl
  
  pre_build:
    commands:
      # 登入 AWS ECR 服務
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com

      # 標註 Image 標籤的變數
      - IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)

      # 登入 EKS 服務
      - echo Entered the pre_build phase...
      - echo Logging in to Amazon EKS...
      - aws eks --region $AWS_DEFAULT_REGION update-kubeconfig --name $EKS_CLUSTER_NAME

  build:
    commands:
      # Build Docker image
      - go mod download
      - go mod vendor # 讓私人倉庫的 mod 先載到 vendor 資料夾,帶入 docker container
      - docker build --build-arg AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION --build-arg AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI . -t $IMAGE_REPO_NAME:$IMAGE_TAG -f dockerfiles/your_dockerfile_path.dockerfile
      - echo Build docker image completed...
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      
  post_build:
    commands:

      # push docker to ECR (push version as id)
      - echo Push image...
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      - echo Push image completed...

      # EKS apply can be
      - kubectl set image deployment/$DEPLOYMENT_NAME [container name]=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG

cache:
  paths:
    - /go/pkg/**/*

不過我的情境則是 (B),DevOps 了解所有需要佈署的應用程式,所以採用集中管理,Project 端只需要顧好一個可以被正常 Build 的 Docker Image (dockerfile) 和 buildspec 就可以了。

如此, Kubernetes manifest 的確是可以選擇要放在專案團隊的 Repo 還是移出來,我選擇了移出來,所以隨之面臨到問題,CodeBuild、 CodeDeploy 要如何幫 Kubernetes Deployment 升版?

你可以選擇把有 kubernetes mainfest 的 infrastructure 再把 k8s file 丟到 S3,然後這裡 Build 再拉,但是這個過程似乎造成了一個不即時的問題。

於是, Helm 就出來了。

Helm 不是只有常見的套件安裝而已,事實上我們也可以自己客製化一個自己的服務模板 (Charts),讓 CodeBuild 從 ECR 讀到這個模板 (跟 K8s mainfest 一樣的東西,差別在這是模板),讀到模板,只要再給一些要設定的變數,專案 CodeBuild 不需要放入 k8s manifest 就可以完成自動升版的需求了。

Helm 要做的幾件事很重要:
  • 可以帶變數客製化
  • 可以管理服務升級
  • 可以 hotfix 設定變數
  • DRY
為了 Helm 建置,要做的幾件事:
  • 建立一個自己服務用的 Helm
  • ECR Helm
  • DevOps 建立服務專案資料夾
  • 建立 Manage shell
  • 建立升版 Script、Buildspec

建立一個自己服務用的 Helm


假設你有 10 個近乎相同的應用程式,如果你寫成傳統 Kubernetes manifest,基本上就感到維護困難,使用 Helm 就可以讓你少寫或複製 10 個樣板,統一使用一種相近類型的樣板,套用一些設定就可以了。

要建立 Helm 模板,使用指令:
helm create [你的樣板名稱]

建立之後,就可以進去 Template (建議先全刪掉裡面預設內容),然後把服務變成通用模板,你還可以帶自己的模板變數,詳情可以參考這份專案:

https://github.com/hpcslag/infrastructure_boilerplate/tree/main/kubernetes/helm/standard-server



ECR Helm


完成模板建置之後,現在要把 Helm 推上 ECR,Helm 近年已經支援 OCI 協議了,可以直接跟 Docker Repository 放在一起,首先要在 ECR 上面建立一個專屬的 Repo,我這裡稱作 standard-server。

建立完成後,本地端在準備推上去之前,要先登入 Helm,跟登入 Dockerhub 或登入 ECR 很像:
export HELM_EXPERIMENTAL_OCI=1

aws ecr get-login-password \
  --region  ap-southeast-1 | helm registry login \
  --username AWS \
  --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.ap-southeast-1.amazonaws.com

*這個步驟要特別注意,之後如果執行 helm 相關指令發生連不上,注意可能是沒有登入。

登入之後,就可以打包 standard-server 這個目錄,然後把檔案推上 ECR:

helm package standard-server
helm push standard-server-0.1.0.tgz oci://$AWS_ACCOUNT_ID.dkr.ecr.ap-southeast-1.amazonaws.com/

這樣應該就可以成功推上去,推完之後可以把 tgz 刪掉。


DevOps 建立服務專案資料夾


Helm 隨時都可以使用,每個相關服務都會用同一個模板,但唯有環境變數是客製化的,所以要為每一個服務都建立專屬 Config (也可以建立在每個應用程式專案端,不過這裡為了統一管理 Key,所以由 DevOps 處理),以下是範例目錄結構:


  • my-server
    • manage.sh (這是一個方便管理這個服務的腳本)
    • secret.yaml (這是 secrets, 完全是 k8s 寫法)
    • values.yaml (這是 common config, helm 要讀取使用的)
    • psql.yaml (如果這個服務自己想 maintain 一個 psql,就自己加在這邊)

這邊只有 values.yaml 是要給 helm 讀取變數帶入模板的,一個範例是這樣的:


# Default values for my-server
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

# managed by cloudflare, from cloudflare to aws route 53
reverse_proxy_hostname: xxxxxxxxxxxx.your_domain.com

# route 53 lb auto create record to following dns record
domain_name: xxxxxxxxxxxx.your_domain.com

# aws certificate manager issued ssl
certificate_arn: arn://xxxxxxxxxxxxxxxxxxxxxxxxxxx


# machine setting
replicas: 2
strategy_type:
  type: Recreate
  rollingUpdate: null
  # type: RollingUpdate # careful, if app update will change sql table schema, then you cannot do this
  # rollingUpdate:
  #  maxSurge: 1
  #  maxUnavailable: 0

# app definition 
app_name: my-server
app_container_image: XXXXXXXX.dkr.ecr.ap-southeast-1.amazonaws.com/my-server
app_container_image_version: bf79085
app_port: 8080
app_configs:
  DATABASE_NAME: "xxxx"
  S3_REGION: "us-east-2"
  REDIS_ADDR: "redis-leader" # FQDN: http://redis-leader
  REDIS_PASSWORD: "redis"
  REDIS_PORT: "6379"
  env: production


建立 Manage shell



helm 指令不多,但是如果不傻瓜化就很容易出錯,因此我自己有做自動化的腳本:


# 記得執行之前,Helm 一定要先登入,執行上方登入腳本
set -e 

export HELM_EXPERIMENTAL_OCI=1

# first parameter is operation

namespace=#自訂要安裝的 namespace, 假設叫做: my-server-production, my-server-staging...

helm_chart_oci_repo=oci://$AWS_ACCOUNT_ID.dkr.ecr.ap-southeast-1.amazonaws.com/standard-server
helm_chart_oci_version=0.1.0 # 這個客製化 Helm Charts 的版本,剛才推上 0.1.0

# usage: ./manage.sh install

if [ "$1" = "install" ]; then # 安裝,安裝失敗最好要手動清掉重來
  
  echo "create namespace: $namespace..."

  kubectl create namespace $namespace

  echo "create secret file..."

  kubectl create -f secret.yaml

  echo "create helm charts... (with values.yaml)"

  helm install --namespace $namespace $namespace-release $helm_chart_oci_repo --version $helm_chart_oci_version -f values.yaml

  echo "installed"

fi;

if [ "$1" = "uninstall" ]; then
  
  echo "uninstall helm charts..." # 解除安裝

  helm uninstall --namespace $namespace $namespace-release

  echo "deleting secret.yaml..."

  kubectl delete -f secret.yaml

  echo "deleting namespace: $namespace..."

  kubectl delete namespace $namespace

  echo "uninstalled"

fi;

if [ "$1" = "upgrade" ]; then

  echo "please enter the container image version to upgrade: "
  
  read image_version # 請貼上 ECR 應用程式的版本 tag

  echo "upgrade helm charts"

  helm upgrade --cleanup-on-fail $namespace-release $helm_chart_oci_repo --version $helm_chart_oci_version --namespace $namespace --reuse-values --set app_container_image_version=$image_version

fi;

if [ "$1" = "value" ]; then

  echo "upgrade helm charts only value" # 只會更新 value.yaml

  helm upgrade $namespace-release $helm_chart_oci_repo --version $helm_chart_oci_version --namespace $namespace -f values.yaml

fi;

這是一個通用腳本,基本上 cover 安裝、更新、解除安裝、更新值的事情。

這個腳本最終會放在 DevOps 端每一個服務資料夾底下,方便管理就是了。



建立升版 Script、Buildspec


前置佈署準備跟 Helm 都完成了,剩下就是應用程式專案端要處理的一些事務,首先應用程式專案還需要多一個 upgrade_service.sh 這個指令檔,協助升級它的應用程式佈署。


set -e

export HELM_EXPERIMENTAL_OCI=1

echo "Login helm..."

aws ecr get-login-password \
  --region  $AWS_DEFAULT_REGION | helm registry login \
  --username AWS \
  --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com

echo "Upgrade chart..."
helm_chart_oci_repo=oci://$AWS_ACCOUNT_ID.dkr.ecr.ap-southeast-1.amazonaws.com/standard-server
helm_chart_oci_version=0.1.0

helm upgrade --cleanup-on-fail $1 $helm_chart_oci_repo --version $helm_chart_oci_version --namespace $2 --reuse-values --set app_container_image_version=$3


這個腳本也是一個通用腳本,可以放在很多個專案共用,這個腳本被執行之後,最後一段升級指令,就會用這個 Helm Charts 更新,而且有加上 flag: --reuse-value 保留之前的環境變數值,最後 --set 去設定那個 deployment 應用程式的版本,就可以完成升級。


最後一步,就是 buildspec 的建立:
version: 0.2

phases:
  install:
    commands:
      # 私人的 Go Library Repository 如果有使用 AWS CodeCommit,需要這段進行前處理, go get, go download 才可以載得到私人倉庫
      - git config --global credential.helper '!aws codecommit credential-helper $@'
      - git config --global credential.UseHttpPath true
      - go env -w GOPRIVATE=git-codecommit.ap-southeast-1.amazonaws.com

      # 安裝 EKS, Kubectl
      - echo Installing app dependencies...
      # other version can check here: https://docs.aws.amazon.com/zh_tw/eks/latest/userguide/install-kubectl.html
      - curl -o kubectl https://amazon-eks.s3.us-west-2.amazonaws.com/1.20.4/2020-02-22/bin/linux/amd64/kubectl   
      - chmod +x ./kubectl
      - mv ./kubectl /usr/local/bin/kubectl

      # 安裝  Helm
      - echo Install helm...
      - wget https://get.helm.sh/helm-v3.7.2-linux-amd64.tar.gz -O helm.tar.gz; tar -xzf helm.tar.gz
      - chmod +x ./linux-amd64/helm
      - mv ./linux-amd64/helm /usr/local/bin/helm
      - echo "Helm installed"
  
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com

      # auto versioning
      - IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)


      # 登入 EKS
      - echo Entered the pre_build phase...
      - echo Logging in to Amazon EKS...
      - aws eks --region $AWS_DEFAULT_REGION update-kubeconfig --name $EKS_CLUSTER_NAME

  build:
    commands:
      # Build Docker image
      - go mod download
      - go mod vendor # 私人模組做成 vendor, 不然 docker build 裡面無法載 CodeCommit Private Repo
      - docker build --build-arg AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION --build-arg AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI . -t $IMAGE_REPO_NAME:$IMAGE_TAG -f dockerfiles/your_docker_file_path.dockerfile
      - echo Build docker image completed...
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      
  post_build:
    commands:
      - echo Build completed on `date`

      # push docker to ECR (push version as id)
      - echo Push image...
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG

      # 更新 Kubernetes 版本升級
      - chmod +x ./upgrade_service.sh
      - ./upgrade_service.sh my-server-$GIT_BRANCH-release my-server-$GIT_BRANCH $IMAGE_TAG

cache:
  paths:
    - /go/pkg/**/*


這個腳本要注意的是最後一個步驟,我的 my-server-$GIT_BRANCH-release 是因為我的有區分 production, staging deploy,我是在 manage.sh 安裝腳本的 namespace 就訂好了,所以我會出現 my-server-staging 這種稱呼的 namespace,最終 manage.sh 安裝時,會指定 helm 的 release name 自動加一個 -release 在後面,名稱就會變成 my-server-staging-release。

這裡要注意的是,要給 CodeBuild 加上 CodeCommit, ECR 訪問的權限,否則會發生撈不到 ECR 的問題。


References:

https://qiita.com/yo24/items/75560c56779e4ce80ace

沒有留言:

張貼留言

© Mac Taylor, 歡迎自由轉貼。
Background Email Pattern by Toby Elliott
Since 2014