charles
Charles
gitlab

Gitlab CI yml 較好的寫法

Gitlab CI yml 較好的寫法
0 views
12 min read
#gitlab

目前 CI/CD Repo 已有套用了模組化和分專案的寫法

  1. module 資料夾的用法將共用性質比較高的動作使用模組的方式管理 如:rules 的管理、build image、Cloud Run 部署、GKE 部署、CI_COMMIT_SHA 轉換

  2. project 資料夾的用法將組別內的 gitlab-ci.yml 檔案統一管理,repo 中只需 Include 即可

  3. 善用 gitlab variable

    • 很常會遇到相同的 script 只是因為 環境不同 導致需要將相同的 script copy & paste
    • 可以將相同的 script 使用 function 包起來,在每個獨立的 Job 內使用 variable 帶進去即可 解決重複問題
  4. CI Job 命名規則提供參考:'CI Stages 名稱-CI Job 對應環境-CI Job 描述'

    • build-env-*
    • deploy-env-*
  5. 之前的做法,會發現有很多指令都是重複性比較高的

之前的寫法

# 執行步驟
stages:
  - "lint" 
  - "unit_test"
  - "build"
  - "deploy"
  - "integration_test"

# 需要引入的檔案
include:
  - "/variables.yml"

# lint 檢查
vuelint:
  stage: "lint"
  image: ${IMAGE}
  tags: # gitlab-runner tag
    - ${RUNNER_TAG}
  script: # 填寫:單元測試需要執行的指令
    - echo "run lint check script"

# 單元測試
unit-test:
  stage: "unit_test"
  image: ${IMAGE}
  tags: # gitlab-runner tag
    - ${RUNNER_TAG}
  script: # 填寫:單元測試需要執行的指令
    - echo "run unit-test script"


# 整合測試提供給 SR 團隊觸發
integration-test:
  stage: "integration_test"
  image: ${IMAGE}
  tags:
    - ${RUNNER_TAG}
  script: # 填寫:需要執行的指令
    - echo "run integration test script"
  rules:  # 需要加上這行規則,加上了才可以讓 SR team 觸發。這行規則會在 sprint 的時候觸法做整合測試
    - if: $CI_COMMIT_BRANCH =~ "/^sprint-/" || $CI_PIPELINE_SOURCE == "pipeline"

build-dev:
  stage: "build"
  image: ${IMAGE}
  needs: ["unit-test"] # needs 的用法會是上一個 job 完成,在執行這個 Jobs
  tags: # gitlab-runner tag
    - ${RUNNER_TAG}
  script: # 填寫:部署環境需要執行的指令
    - echo "build docker images only"
  rules: # regular expression
    - if: $CI_COMMIT_BRANCH =~ "/^feature-/"

build-demo:
  stage: "build"
  image: ${IMAGE}
  needs: ["unit-test"]
  tags: # gitlab-runner tag
    - ${RUNNER_TAG}
  script: # 填寫:部署環境需要執行的指令
    - echo "build docker images only"
  rules:
    - if: $CI_COMMIT_BRANCH =~ "/^qa-/"

build-production:
  stage: "build"
  image: ${IMAGE}
  needs: ["unit-test"]
  tags: # gitlab-runner tag
    - ${RUNNER_TAG}
  script: # 填寫:部署環境需要執行的指令
    - echo "build docker images only"
  rules:
    - if: $CI_COMMIT_BRANCH =~ "/^production-/"

deploy-dev:
  stage: "build"
  image: ${IMAGE}
  needs: ["dev-build"] # needs 的用法會是上一個 job 完成,在執行這個 Jobs
  tags: # gitlab-runner tag
    - ${RUNNER_TAG}
  script: # 填寫:部署環境需要執行的指令
    - echo "Deploy docker images only"
  rules: # regular expression
    - if: $CI_COMMIT_BRANCH =~ "/^feature-/"

deploy-demo:
  stage: "build"
  image: ${IMAGE}
  needs: ["unit-test"]
  tags: # gitlab-runner tag
    - ${RUNNER_TAG}
  script: # 填寫:部署環境需要執行的指令
    - echo "Deploy docker images only"
  rules:
    - if: $CI_COMMIT_BRANCH =~ "/^qa-/"

deploy-production:
  stage: "build"
  image: ${IMAGE}
  needs: ["unit-test"]
  tags: # gitlab-runner tag
    - ${RUNNER_TAG}
  script: # 填寫:部署環境需要執行的指令
    - echo "Deploy docker images only"
  rules:
    - if: $CI_COMMIT_BRANCH =~ "/^production-/"
    
build-it:
  stage: "build"
  image: ${IMAGE}
  needs: ["unit-test"]
  tags: # gitlab-runner tag
    - ${RUNNER_TAG}
  script: # 填寫:部署環境需要執行的指令
    - echo "build docker images only"
  rules: 
    - if: $CI_COMMIT_BRANCH =~ "/^sprint-/"

deploy-it:
  stage: "build"
  image: ${IMAGE}
  needs: ["it-build"]
  tags: # gitlab-runner tag
    - ${RUNNER_TAG}
  script: # 填寫:部署環境需要執行的指令
    - echo "Deploy docker images only"
  rules: 
    - if: $CI_COMMIT_BRANCH =~ "/^sprint-/"

將重讀性的 (High) 的指令使用 module 包起來

如:rules 的管理、build image 的做法、Cloud Run 部署、GKE 部署、CI_COMMIT_SHA 轉換

CI_COMMIT_SHA 轉換

.get-commit-sha:
  convert-time:
    - export COMMITSHA=$(git show -s --format=%ct $CI_COMMIT_SHA)

rules 的管理

.reference:
  rules:
    # DEV
    build_image_dev:
      - if: $CI_COMMIT_BRANCH =~ "/^feature-/"
    deploy_dev:
      - if: $CI_COMMIT_BRANCH =~ "/^feature-/"
    # QA
    build_image_qa:
      - if: $CI_COMMIT_BRANCH =~ "/^qa-/"
    deploy_qa:
      - if: $CI_COMMIT_BRANCH =~ "/^qa-/"
    # PROD
    build_image_prod:
      - if: $CI_COMMIT_BRANCH =~ "/^production-/"
    deploy_prod:
      - if: $CI_COMMIT_BRANCH =~ "/^production-/" && $CI_PIPELINE_SOURCE == "web"
        when: manual
      - if: $CI_PIPELINE_SOURCE == "pipeline"
        when: never
    # IT(pending)
    rules_of_integration_test:
      - if: $CI_COMMIT_BRANCH =~ "/^sprint-pending/" || $CI_PIPELINE_SOURCE == "web"
    build_image_integration_test:
      - if: $CI_COMMIT_BRANCH =~ "/^sprint-pending/"
    deploy_integration_test:
      - if: $CI_COMMIT_BRANCH =~ "/^sprint-pending/"

build image 管理

include:
  - local: /module/jobs-convert-time.yml
  
.build-image :
  gcp-cloud-build:
    - !reference [.get-timestamp, convert-time]
    - gcloud builds submit --region=${REGION} --config=cloudbuild.yaml . --substitutions=_CLOUD_IMAGE_URL="${REGISTRY_REGION}/${RPA_PROJECT_ID}/${REGISTRY_FOLDER}/${REGISTRY_FILE}:$IMAGE_ENV_$TIMESTAMP",_CLOUD_RUN_SVC_NAME="${CLOUD_RUN_SVC}",_REGION=${REGION}

  docker-ci:
    - !reference [.get-timestamp, convert-time]
    - echo "TIMESTAMP=>$TIMESTAMP"
    - echo "TIMESTAMP=>"$TIMESTAMP
    - gcloud auth configure-docker ${REGISTRY_REGION}
    - docker build -t "$REGISTRY_FILE":"$IMAGE_ENV"_"$TIMESTAMP" .
    - echo "\n檢視本地镜像"
    - docker images
    - echo "\n标记本地镜像"
    - docker tag "$REGISTRY_FILE":"$IMAGE_ENV"_"$TIMESTAMP" ${REGISTRY_REGION}/${RPA_PROJECT_ID}/"$REGISTRY_FOLDER"/"$REGISTRY_FILE":"$IMAGE_ENV"_"$TIMESTAMP"
    - echo "\n上傳至gcp..."
    - docker push ${REGISTRY_REGION}/${RPA_PROJECT_ID}/"$REGISTRY_FOLDER"/"$REGISTRY_FILE":"$IMAGE_ENV"_"$TIMESTAMP"
    - docker rmi ${REGISTRY_REGION}/${RPA_PROJECT_ID}/"$REGISTRY_FOLDER"/"$REGISTRY_FILE":"$IMAGE_ENV"_"$TIMESTAMP"
    - docker rmi "$REGISTRY_FILE":"$IMAGE_ENV"_"$TIMESTAMP"

Cloud Run 部屬

include:
  - local: /module/jobs-convert-time.yml

.gcp-deploy:
  gcp-cloud-run:
    - !reference [.get-timestamp, convert-time]
    - gcloud run deploy "$CLOUD_RUN_SVC" --image=${REGISTRY_REGION}/${PROJECT_ID}/"$REGISTRY_FOLDER"/"$REGISTRY_FILE":"$IMAGE_ENV"_"$TIMESTAMP" --region=${REGION} --service-account=${SA_ID}

GKE 部屬

  • GKE 部署有分成使用原生 kubectl 和 helm charts 的部署方式。
  • 會有兩種部署方式,會比較容易應付不同的 repo 部署。
.template-deployment-gke:
  script:
    - export TIMESTAMP=$(git show -s --format=%ct $CI_COMMIT_SHA)
    - gcloud container clusters get-credentials $CLUSTER --region=${CLUSTER_REGION}
    - kubectl get namespaces
    - kubectl config get-contexts
    - kubectl config set-context --current --namespace=$NAMESPACE
    - kubectl set image deployment/$DEPLOYMENT_NAME $CONTAINER_NAME=${REGISTRY_REGION}/${RPA_PROJECT_ID}/"$REGISTRY_FOLDER"/"$REGISTRY_FILE":"$IMAGE_ENV"_"$TIMESTAMP"
    - kubectl rollout restart deployment/$DEPLOYMENT_NAME
  
  deploy-kubectl:
    # check 變數 TARGET_ENV,空字串則使用 branch name
    - |
      if [ -n "$TARGET_ENV" ]; then
        TARGET_ENV="$TARGET_ENV";
      else 
        TARGET_ENV="$CI_COMMIT_REF_NAME";
      fi
    - gcloud container clusters get-credentials $CLUSTER --region=${CLUSTER_REGION}
    - kubectl config set-context --current --namespace=$NAMESPACE
    - kubectl set image deployment/$DEPLOYMENT_NAME $CONTAINER_NAME=${REGISTRY_REGION}/${RPA_PROJECT_ID}/"$REGISTRY_FOLDER"/"$REGISTRY_FILE":"$IMAGE_ENV"_"$TIMESTAMP"
    - kubectl rollout restart deployment/$DEPLOYMENT_NAME

  gcloud-auth:
    - gcloud components install kubectl
    - gcloud components install gke-gcloud-auth-plugin

  docker-login:
    - gcloud auth activate-service-account --key-file=${LOCAL_RPA_SA}
    - gcloud auth configure-docker asia-northeast1-docker.pkg.dev
    - gcloud config set project ${RPA_PROJECT_ID}
    - gcloud container clusters get-credentials $CLUSTER --region=${CLUSTER_REGION}

  deploy-helm:
    - export TIMESTAMP=$(git show -s --format=%ct $CI_COMMIT_SHA)
    - kubectl config set-context --current --namespace=$NAMESPACE
    - helm upgrade --install "$DEPLOYMENT_NAME" oci://${REGISTRY_REGION}/${RPA_PROJECT_ID}/bi-rpa-helm/$HELM_VERSION --namespace ${NAMESPACE} --reuse-values -f helm_value/"$TARGET_ENV".yaml --set deployment.container.image=${REGISTRY_REGION}/${RPA_PROJECT_ID}/"$REGISTRY_FOLDER"/"$REGISTRY_FILE":"$TARGET_ENV"_"$TIMESTAMP" --atomic

將重讀性 (medium) 的指令使用 module 包起來 如:CI 建立 Docker Images 和部署服務的 Job。

Docker 封裝 Image 的 function

  • 將重複性的 script 用 function 的方式包起來。降低維護成本
.build_docker_template: &build_docker_template ## 因為下面的 build dev & qa & production 都會使用到 所以拉出來寫避免重複
  stage: "build"
  tags:
    - ${RUNNER_GCLOUD}
  services:
    - docker:dind
  image:
    name: ${GOOGLE_IMAGE}
  script:
    - !reference [.gcloud-auth, rpa-config]
    - echo "Copy Env File"
    - cp $ENV_FILE .env
    - echo 'VITE_ENV='$IMAGE_ENV >> .env
    - cat .env
    # Using Docker CI Build
    - !reference  [.build-image, docker-ci]

# CI Job for dev
build-dev-docker:
  needs: ["unit-test"]
  <<: *build_docker_template ## 會去跑上面的.build_docker_template: &build_docker_template 部分
  variables:
    IMAGE_ENV: "dev"
    REGISTRY_FOLDER : ${PLATFORM_REG_NAME}
  rules: # regular expression
    - !reference [.reference, rules, build_image_dev]

# CI Job for qa
build-qa-docker:
  needs: ["unit-test"]
  <<: *build_docker_template ## 會去跑上面的.build_docker_template: &build_docker_template 部分
  variables:
    IMAGE_ENV: "qa"
    REGISTRY_FOLDER : ${PLATFORM_REG_NAME}
  rules:
    - !reference [.reference, rules, build_image_qa]

# CI Job for prod
build-production-docker:
  needs: ["unit-test"]
  <<: *build_docker_template ## 會去跑上面的.build_docker_template: &build_docker_template 部分
  variables:
    IMAGE_ENV: "production"
    REGISTRY_FOLDER : ${PLATFORM_REG_NAME}
  rules:
    - !reference [.reference, rules, build_image_prod]

Trigger Cloud Run 更新 Image

.deploy_cr_template: &deploy_cr_template ## 因為下面的 deploy dev & qa & production 都會使用到 所以拉出來寫避免重複
  stage: "deploy"
  tags:
    - ${RUNNER_GCLOUD}
  services:
    - docker:dind
  image:
    name: ${GOOGLE_IMAGE}
  script:
    - !reference [.gcloud-auth, rpa-config]
    - !reference [.gcp-deploy, gcp-cloud-run]

# CI Job dev
deploy-dev-gcp-cloud-run:
  needs: ["build-dev-docker"]
  <<: *deploy_cr_template ## 會去跑上面的.deploy_cr_template: &deploy_cr_template 部分
  variables:
    IMAGE_ENV: "dev"
    CLOUD_RUN_SVC: "dev-vite-rpa-platform"
    REGISTRY_FOLDER : ${PLATFORM_REG_NAME}
  rules: # regular expression
    - !reference [.reference, rules, deploy_dev]

# CI Job qa
deploy-qa-gcp-cloud-run:
  needs: ["build-qa-docker"]
  <<: *deploy_cr_template ## 會去跑上面的.deploy_cr_template: &deploy_cr_template 部分
  variables:
    IMAGE_ENV: "qa"
    CLOUD_RUN_SVC: "demo-rpa-pltf-ap"
    REGISTRY_FOLDER : ${PLATFORM_REG_NAME}
  rules:
    - !reference [.reference, rules, deploy_qa]

# CI Job prod
deploy-production-gcp-cloud-run:
  needs: ["build-production-docker"]
  <<: *deploy_cr_template ## 會去跑上面的.deploy_cr_template: &deploy_cr_template 部分
  variables:
    IMAGE_ENV: "production"
    CLOUD_RUN_SVC: "prd-rpa-pltf-ap"
    REGISTRY_FOLDER : ${PLATFORM_REG_NAME}
  rules:
    - !reference [.reference, rules, deploy_prod]

GKE 更新 Docker Image & restart pod

.deploy_gke_template: &deploy_gke_template ## 因為下面的 deploy dev & qa & production 都會使用到 所以拉出來寫避免重複
  stage: "deploy"
  tags:
    - ${RUNNER_GCLOUD}
  services:
    - docker:dind
  image:
    name: ${CUSTOM_GOOGLE_IMAGE}
  script:
    - !reference [.gcloud-auth, rpa-config]
    - !reference [.template-deployment-gke, deploy-kubectl]

# CI Job dev
deploy-dev-gke:
  needs: ["build-dev-docker"]
  <<: *deploy_gke_template ## 會去跑上面的.deploy_gke_template: &deploy_gke_template 部分
  variables:
    TARGET_ENV: "dev"
    CLUSTER: ${DEV_CLUSTER}
    NAMESPACE: "bi-rpa-platform"
    REGISTRY_FOLDER : "bi-rpa-platform"
    DEPLOYMENT_NAME: "frontend-deployment"
    HELM_VERSION: "app-dev"
  rules: # regular expression
    - !reference [.reference, rules, deploy_dev]

# CI Job qa
deploy-qa-gke:
  needs: ["build-qa-docker"]
  <<: *deploy_gke_template ## 會去跑上面的.deploy_gke_template: &deploy_gke_template 部分
  variables:
    TARGET_ENV: "qa"
    CLUSTER: ${QA_CLUSTER}
    NAMESPACE: "bi-rpa-platform"
    REGISTRY_FOLDER : "bi-rpa-platform"
    DEPLOYMENT_NAME: "frontend-deployment"
    HELM_VERSION: "app-qa"
  rules:
    - !reference [.reference, rules, deploy_qa]

# CI Job prod
deploy-production-gke:
  needs: ["build-production-docker"]
  <<: *deploy_gke_template ## 會去跑上面的.deploy_gke_template: &deploy_gke_template 部分
  variables:
    TARGET_ENV: "production"
    CLUSTER: ${PROD_CLUSTER}
    NAMESPACE: "bi-rpa-platform"
    REGISTRY_FOLDER : "bi-rpa-platform"
    DEPLOYMENT_NAME: "frontend-deployment"
    HELM_VERSION: "app-production"
  rules:
    - !reference [.reference, rules, deploy_prod]

使用 Helm chart 更新 GKE

.deploy_helm_template: &deploy_helm_template ## 因為下面的 deploy dev & qa & production 都會使用到 所以拉出來寫避免重複
  stage: "deploy"
  tags:
    - ${RUNNER_GCLOUD}
  services:
    - docker:dind
  image:
    name: ${CUSTOM_GOOGLE_IMAGE}
  script:
    - !reference [.gcloud-auth, rpa-config]
    - !reference [.template-deployment-gke, gcloud-auth]
    - !reference [.template-deployment-gke, docker-login]
    - !reference [.template-deployment-gke, deploy-helm]

# CI Job dev
deploy-dev-gke:
  needs: ["build-dev-docker"]
  <<: *deploy_helm_template ## 會去跑上面的.deploy_helm_template: &deploy_helm_template 部分
  variables:
    TARGET_ENV: "dev"
    CLUSTER: ${DEV_CLUSTER}
    NAMESPACE: "bi-rpa-platform"
    REGISTRY_FOLDER : "bi-rpa-platform"
    DEPLOYMENT_NAME: "frontend-deployment"
    HELM_VERSION: "app-dev"
  rules: # regular expression
    - !reference [.reference, rules, deploy_dev]

# CI Job qa
deploy-qa-gke:
  needs: ["build-qa-docker"]
  <<: *deploy_helm_template ## 會去跑上面的.deploy_helm_template: &deploy_helm_template 部分
  variables:
    TARGET_ENV: "qa"
    CLUSTER: ${QA_CLUSTER}
    NAMESPACE: "bi-rpa-platform"
    REGISTRY_FOLDER : "bi-rpa-platform"
    DEPLOYMENT_NAME: "frontend-deployment"
    HELM_VERSION: "app-qa"
  rules:
    - !reference [.reference, rules, deploy_qa]

# CI Job Prod
deploy-production-gke:
  needs: ["build-production-docker"]
  <<: *deploy_helm_template ## 會去跑上面的.deploy_helm_template: &deploy_helm_template 部分
  variables:
    TARGET_ENV: "production"
    CLUSTER: ${PROD_CLUSTER}
    NAMESPACE: "bi-rpa-platform"
    REGISTRY_FOLDER : "bi-rpa-platform"
    DEPLOYMENT_NAME: "frontend-deployment"
    HELM_VERSION: "app-production"
  rules:
    - !reference [.reference, rules, deploy_prod]