本篇紀錄讓 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
沒有留言:
張貼留言