前一篇文章提到CI Pipeline的規劃包含Build Code、Build Image、Deploy Dev to CloudRun,這一篇就來處理Build Image的部份,直接先看完成的YAML吧!
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'
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
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)
今天的YAML內容雖然是沿用前一篇的內容,但是從resources之後就有些改變。首先可以看到在resources之後加上了variables的區段,裡面設定了幾個不同的變數。
前三個是從昨天的BuildCode job中將一些內容抽離出來改為變數,除了方便在一開始設定並且若有用在不只一個地方而有修改的話只要改變數內容就不用擔心漏改。
後三個就是這一篇BuildImage會使用到的變數,就是Image Repository、Dockerfile以及在Azure DevOps設定的Service Connection名稱。
variables:
pipelineArtifact: output
buildResultZipName: buildResult.zip
slnOrCsprojName: IronmanWeb.sln
imgRepository: 'asia-east1-docker.pkg.dev/feisty-mechanic-363012/ironman2022/ironmanweb'
buildDockerfile: 'Dockerfile'
imgRegistryService: 'GCPArtifactRegistry'
為了要在Build完Docker Image之後推到GCP的Artifact Registry,所以需要建立一個Service Connection來存放身份驗證的設定,主要是這邊使用到的是內建的Docker task。還記得前面文章的json檔案嗎?又需要它的內容囉!檔案要好好保管好啊,後面還有它再度出場的時候。
建立Service Connection從Azure DevOps的專案設定(Project Settings)進入,按下「Create service connection」按鈕之後選擇「Docker Registry」的選項,接著要輸入的內容就和前面文章的最後面Docker Login差不多,而Docker Password的內容就把json檔案的內容全部複製之後貼進去就行了(我知道,有點長),最重要的就是記下Service connection name,回來貼到上面的imgRegistryService變數中。
對了,上面的buildDockerfile變數值是Dockerfile,這個dockerfile的內容如下,如果你儲存的檔名並不是使用預設的Dockerfile這個名稱,記得變數值要改喔!
FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine
WORKDIR /app
COPY . .
ENTRYPOINT ["dotnet", "IronmanWeb.dll"]
上面這個Dockerfile沒什麼特別,除了第一行使用的Image版本選用alpine這個比較小的Image之外,再來就是要執行dll名稱直接寫在裡面。
嗯?可以不這樣寫在裡面嗎?後面的文章就會知道了…
接下來第一個Job是前一篇的BuildCode Job,裡面的內容可以自己對照一下,把哪些小地方改成變數。
這篇的內容主要在第二個job,也就是BuildImage的部份。因為前一個BuildCode job裡面把編譯的結果都壓縮成zip,而且這邊是分開在兩個不同的Job執行後續的動作,所以在第二個Job需要先有一個下載Pipeline Artifact的動作,主要是不同Job其實是可以在不同的Agent執行,去年的這一篇文章有介紹過。
- task: DownloadBuildArtifacts@0
displayName: 下載Pipeline Artifact
inputs:
buildType: 'current'
cleanDestinationFolder: true
downloadType: 'single'
artifactName: '$(pipelineArtifact)'
downloadPath: '$(System.ArtifactsDirectory)/'
下載了前面Job上傳到Pipeline Artifact中的zip檔案之後,接下來當然是要把它解壓縮之後才能使用,解壓縮到$(System.ArtifactsDirectory)/BuildImage這個位置,這個位置會在後面用於buildContext的位置。
- task: ExtractFiles@1
displayName: Unzip zip
inputs:
archiveFilePatterns: '$(System.ArtifactsDirectory)/$(pipelineArtifact)/$(buildResultZipName)'
destinationFolder: '$(System.ArtifactsDirectory)/BuildImage'
cleanDestinationFolder: true
overwriteExistingFiles: true
接下來最後面的三個Task都是和Docker相關的動作,分別是Build、Login、Push。
- 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)
第一個Task是使用Docker task並且command設定為build,tags的部份是陣列類型,如果你建立的Image不只要使用latest為tag,那就多加一行,每一行一個tag。
第二個Task就是使用上面設定的Service Connection名稱執行登入的動作,如果不是透過Service Connection的話,就必須自己透過指令登入,那就會額外需要json檔案,比較麻煩一點,所以這邊先簡單做吧!
第三個Task使用的是Bash執行docker push的指令,其實和第一個Task一樣用Docker task也是可以,在command的部份改成push就行(利用UI的Task小助手方便設定),只是用Bash執行docker push指令也不難,剛好可以對照做個比較。
到這邊為止,已經將程式編譯和建置Docker Image的動作都完成了,可以試試在本機端把Image拉下來執行看看有沒有問題喔!
下一篇將會使用到gcloud cli工具佈署Dev環境的CloudRun,下回見。