【2022鐵人賽】基本版-建立CI Pipeline(3)

Develop CI流程

上面這張圖是流程規劃說明裡面畫的Develop CI Pipeline流程,我們已經在前兩篇完成了Build(Code)、Build Image,剩下最後一步就是Deploy Dev環境,這一篇就來完成這最後一步吧!

trigger:
- none

pool:
  vmImage: ubuntu-latest

resources:
  repositories:
  - repository: sources
    type: git
    name: ironman2022/NetApp
    ref: Develop
    trigger:
      branches:
        include:
          - Develop

variables:
  pipelineArtifact: output
  buildResultZipName: buildResult.zip
  slnOrCsprojName: IronmanWeb.sln
  imgRepository: 'asia-east1-docker.pkg.dev/feisty-mechanic-363012/ironman2022/ironmanweb'
  buildDockerfile: 'Dockerfile'
  imgRegistryService: 'GCPArtifactRegistry'
  cloudRunServiceName: ironmanweb
  cloudRunPort: 8080
  cloudRunRegion: asia-east1
  cloudRunProjectId: feisty-mechanic-363012
  gcpAuthJsonFile: ironman2022-gcp-key.json

jobs:
  - job: BuildCode
    steps:
      - checkout: sources
        clean: true
      - script: |
          export UID=$(id -u)
          export GID=$(id -g)
          docker run --user $UID:$GID --rm \
          -v $(Build.SourcesDirectory):/tmp/source \
          -v $(Build.BinariesDirectory):/tmp/publish \
          -e DOTNET_CLI_HOME=/tmp/.dotnet \
          mcr.microsoft.com/dotnet/sdk:6.0-alpine \
          dotnet publish /tmp/source/$(slnOrCsprojName) \
          -c release \
          -o /tmp/publish
        displayName: Dotnet Build
      - task: ArchiveFiles@2
        displayName: 壓縮成zip
        inputs:
          rootFolderOrFile: $(Build.BinariesDirectory)
          includeRootFolder: false
          archiveType: 'zip'
          archiveFile: '$(Build.ArtifactStagingDirectory)/zipFiles/$(buildResultZipName)'
          replaceExistingArchive: true
      - task: PublishBuildArtifacts@1
        displayName: 上傳到Pipeline Artifact
        inputs:
          PathtoPublish: '$(Build.ArtifactStagingDirectory)/zipFiles/$(buildResultZipName)'
          ArtifactName: '$(pipelineArtifact)'
          publishLocation: 'Container'
  - job: BuildImage
    dependsOn: BuildCode
    steps:
      - task: DownloadBuildArtifacts@0
        displayName: 下載Pipeline Artifact
        inputs:
          buildType: 'current'
          cleanDestinationFolder: true
          downloadType: 'single'
          artifactName: '$(pipelineArtifact)'
          downloadPath: '$(System.ArtifactsDirectory)/'
      - task: ExtractFiles@1
        displayName: Unzip zip
        inputs:
          archiveFilePatterns: '$(System.ArtifactsDirectory)/$(pipelineArtifact)/$(buildResultZipName)'
          destinationFolder: '$(System.ArtifactsDirectory)/BuildImage'
          cleanDestinationFolder: true
          overwriteExistingFiles: true
      - task: Docker@2
        displayName: Build image
        inputs:
          repository: '$(imgRepository)'
          command: 'build'
          Dockerfile: $(buildDockerfile)
          buildContext: '$(System.ArtifactsDirectory)/BuildImage'
          arguments: '--no-cache'
          tags: |
            latest
      - task: Docker@2
        displayName: "Login to Container Registry"
        inputs:
          command: login
          containerRegistry: $(imgRegistryService)
      - task: Bash@3
        displayName: Push docker image
        inputs:
          targetType: 'inline'
          script: |
            docker push -a $(imgRepository)
  - job: DeployCloudRun
    dependsOn: BuildImage
    steps:
      - task: Bash@3
        displayName: Deploy docker image to cloudrun
        inputs:
          targetType: 'inline'
          script: |
            docker run --rm \
            -v $(Build.SourcesDirectory)/$(gcpAuthJsonFile):/gcp/cloudKey.json \
            asia.gcr.io/google.com/cloudsdktool/google-cloud-cli:latest \
            bash -c "gcloud auth login --cred-file=/gcp/cloudKey.json && gcloud run deploy $(cloudRunServiceName) --set-env-vars=Ironman=$(Build.BuildId) --image $(imgRepository) --region $(cloudRunRegion) --project $(cloudRunProjectId) --allow-unauthenticated"

哇!一來就是一長串的YAML內容…

不不不,你如果是用VSCode打開,幾乎可以把前面兩個Job折疊起來,這邊增加的DeployCloudRun Job也只有一個Bash的task,不多的。

就讓我娓娓道來這篇主要增加的內容吧!

  cloudRunServiceName: ironmanweb
  cloudRunPort: 8080
  cloudRunRegion: asia-east1
  cloudRunProjectId: feisty-mechanic-363012
  gcpAuthJsonFile: ironman2022-gcp-key.json

cloudRunServiceName就是圖中Cloud Run的名稱。

cloudRunPort設定為8080是在appsettings.json中設定了Kestrel的Http是使用8080 Port,也就是container內會監聽什麼Port,對應docker指令就是-p 80:8080。

cloudRunRegion則是CloudRun佈署的區域(機房)。

cloudRunProjectId可以直接從Google Cloud管理介面的URL得知,也就是在上圖畫面的時候,看一下瀏覽器上的網址列,「&project=」後面的就是了,或是選擇Project的下拉選單:

Google Cloud的Project Id

最後的gcpAuthJsonFile則是前面幾篇用來授權的Json檔案。不過這邊要補充一下,那時候在新增服務帳戶的時候還少加了一個「服務帳戶使用者」角色,所以漏加這個角色繼續做下去的話,就會碰到下面的錯誤訊息:

PERMISSION_DENIED: Permission ‘iam.serviceaccounts.actAs’ denied on service account

為了讓後面使用gcloud cli可以順利執行,所以要先在IAM裡面將前面新增的服務帳戶加上「服務帳戶使用者」角色:

新增「服務帳戶使用者」角色

增加角色之後不需要重新下載用於授權的Json檔案,因為有什麼角色權限不會寫在檔案裡。

Job越加越多,這三個Job之間其實是有相依性的,也就是說要先BuildCode之後才能夠BuildImage,接下來才能DeployCloudRun,所以在第二個和第三個Job底下分別要加上dependsOn的屬性:

- job: BuildImage
    dependsOn: BuildCode
- job: DeployCloudRun
    dependsOn: BuildImage

這個部份滿重要的,尤其是如果有額外購買CloudAgent的執行數量時,因為Job可以在不同的Agent執行,所以它可以在同一個Pipeline同時跑多個Job(沒有設定相依的dependsOn時),就算沒有額外購買CloudAgent的執行數量,沒設定dependsOn也無法保證它們的執行順序。BuildImage的Job相較前一篇有增加的也只有dependsOn這個屬性。

最後就是DeployCloudRun這個Job,裡面的內容也只有一個Bash的task,所以下面我直接貼bash script的部份:

            docker run --rm \
            -v $(Build.SourcesDirectory)/$(gcpAuthJsonFile):/gcp/cloudKey.json \
            asia.gcr.io/google.com/cloudsdktool/google-cloud-cli:latest \
            bash -c "gcloud auth login --cred-file=/gcp/cloudKey.json && gcloud run deploy $(cloudRunServiceName) --set-env-vars=Ironman=$(Build.BuildId) --image $(imgRepository) --region $(cloudRunRegion) --project $(cloudRunProjectId) --allow-unauthenticated"

在這裡是使用google的gcloud CLI工具來執行CloudRun的佈署,不過gcloud CLI工具還是需要安裝的,要嘛是需要先裝在Agent的電腦內,而且還要去爬官方的安裝文件知道怎麼安裝,不然就是安裝在Docker Image裡面。

我們使用的是ClougAgent,所以Agent的環境不是我們可以控制的,每次執行也是新的vm執行起來,所以選擇後者使用google建立的gcloud CLI的Docker Image會是最理想的選擇,除了從Docker Hub可以找到之外,官方文件也有提供不同Container Registry的選擇說明。

使用Container來執行gcloud CLI,我們可以省去安裝的麻煩事,只要會使用就可以了,這讓我們可以更專注在其它的設計部份。

script中的重點只有第二行和最後一行,分別是把授權用的Json檔案關聯到Container裡面,以便讓裡面的gcloud CLI工具可以讀取到內容進行login動作,以及最後一行包含lgoin的指令。

最後一行的指令有個重點,就是我們必須先使用gloud auth login的指令讓CLI工具登入,接著才能執行CloudRun的Deploy指令,也就是說要執行的指令有兩個,所以使用了「&&」這個管道符號讓它接續執行,但是直接這樣接在Image Repository後面是行不通的,「&&」符號的前面會跟最前面的docker run指令合起來作為第一個指令,後面的則是host環境接續docker run指令執行的第二個指令。

所以在這個地方必須是讓docker run執行起來的contianer是執行bash程式,後面接著要執行的指令字串(用引號包起來),也因為是一整個字串,所以沒辦法使用「\\」換行,就會是一行很長的指令,下面為了方便閱讀,把它們拆開來說明。

gcloud auth login --cred-file=/gcp/cloudKey.json

「&&」符號前的這一行是將gcloud CLI工具登入,使用–cred-file參數帶入Json檔案,後面的路徑是Container內的路徑,也就是前面-v設定的部份。

gcloud run deploy $(cloudRunServiceName) --set-env-vars=Ironman=$(Build.BuildId) --image $(imgRepository) --region $(cloudRunRegion) --project $(cloudRunProjectId) --allow-unauthenticated

「&&」符號後面的指令我依參數拆行來看應該就很清楚,因為大部份都是上面設定的變數,已經有說明過了。

–set-env-vars的參數是設定CloudRun放入的環境變數(還記得前面的Ironman環境變數嗎?),–allow-unauthenticated則是允許訪客瀏覽,不然CloudRun可能不會正常回應頁面。

gcloud run deploy $(cloudRunServiceName)
–set-env-vars=Ironman=$(Build.BuildId)
–image $(imgRepository)
–region $(cloudRunRegion)
–project $(cloudRunProjectId)
–allow-unauthenticated

關於CloudRun在gcloud CLI可以設定的更多參數部份,請參考官方文件的頁面,之後的文章還會把部份參數用上。

最後,在CI Pipeline成功執行完之後,就可以在對應的Task log中看到gcloud CLI工具吐出來的CloudRun網址,這樣就不用進入到GCP的管理介面去查看。

最後補充一個小細節,就是在這三個Job之中,只有BuildCode這個Job中有明確的加上checkout: sources,並且身份驗證的Json檔案是放在Pipelines這個Git Repository裡面,但是在後面的BuildImage和DeployCloudRun並沒有明確加上checkout動作卻會(可以)取得Pipelines這個Git Repository裡面的檔案,也沒有在resources.repositories底下設定Pipelines,主要是因為這裡的Pipeline YAML檔案就是放在Pipelines這個Git Repository裡面,所以隱含了checkout: self這個動作,替我們省下了一些設定。(下圖紅框與藍框的差異)

發佈留言