終於在前面把該拆成範本的YAML差不多都搞定了。
不過如果這樣子就要開始應用在多個不同的Azure DevOps專案,並且是.Net開發的系統而且使用Docker Image的話,可能就會碰到一個需要思考的問題,那就是每個.Net系統都要有一個自己的Dockerfile檔案嗎?
為什麼會這樣子說呢?如果你還記得前面「基本版-建立CI Pipeline(2)」這篇文章裡面使用到的Dockerfile,最後一行的內容是下面這樣:
ENTRYPOINT ["dotnet", "IronmanWeb.dll"]
也就是說在EntryPoint裡面使用dotnet指令傳入的是.Net DLL的名稱,不同的系統會編譯出不同的DLL名稱,所以這個內容如果是寫死在Dockerfile裡面,那麼每一個.Net系統都需要有一份自己的Dockerfile…
我知道現在新版本的Visual Studio已經有功能可以自動從專案或方案自動產生出來相對應的Dockerfile,不過…其實不少開發人員是搞不懂裡面的內容是在做什麼的。
而且每一個系統需要一份Dockerfile,那如果忘了產生,或是內容忘了更新,或是檔案沒有傳到版控亦或是被不小心刪掉了呢?
或許你會說…檔案不是放在各別專案的Pipelines Git Repo嗎?開發人員不是碰不到這個Repository?
對…,放在各別專案的Pipelines Git Repo是不歸開發人員管,但是檔案是開發人員產生之後給你?還是你幫每一個專案都寫一份Dockerfile?弄一個範本的Dockerfile然後新專案再根據DLL名稱去更改?
上面不管是哪一個方法都可行,放在Source Code的Git Repo裡面,從YAML範本中去checkout sources找到指定檔名的Dockerfile,或是放在Pipelines Git Repo裡面,檔案由開發人員提供或是用範本改DLL名稱都可以。
不過更好的方式是在Templates Git Repo裡面有一份.Net用的Dockerfile,不同的系統傳入不同的DLL名稱,也就是在Dockerfile裡面加上ARG和ENV的使用,下面比較一下修改前的Dockerfile和修改後的Dockerfile差異吧!
# 原本的Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine
WORKDIR /app
COPY . .
ENTRYPOINT ["dotnet", "IronmanWeb.dll"]
# 修改後的Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine
# 加上ARG,名稱為TARGET_FILE
ARG TARGET_FILE
WORKDIR /app
COPY . .
# 加上ENV,名稱為APP,設定值是/app/${TARGET_FILE}
ENV APP=/app/${TARGET_FILE}
# 改用CMD執行dotnet並且使用APP環境變數
CMD dotnet ${APP}
# 加上EXPOSE對外開放80 Port
EXPOSE 80
改成新的內容之後,對應的steps/docker-build.yaml step template也要修改一下:
parameters:
- name: imgRepository
type: string
- name: imgTags
type: object
- name: buildDockerfile
type: string
default: Build.Dockerfile
- name: buildContext
type: string
- name: containerRegistry
type: string
# 增加buildArgs參數
- name: buildArgs
type: string
default: ''
steps:
- task: Docker@2
displayName: Build image
inputs:
repository: '${{ parameters.imgRepository }}'
command: 'build'
Dockerfile: ${{ parameters.buildDockerfile }}
buildContext: ${{ parameters.buildContext }}
# 原本是arguments: '--no-cache',利用條件判斷決定要不要傳入--build-arg
${{ if eq(parameters.buildArgs, '') }}:
arguments: '--no-cache'
${{ else }}:
arguments: '--no-cache --build-arg ${{ parameters.buildArgs }}'
tags: ${{ parameters.imgTags }}
- task: Docker@2
displayName: "Login to Container Registry"
inputs:
command: login
containerRegistry: ${{ parameters.containerRegistry }}
- task: Bash@3
displayName: Push docker image
inputs:
targetType: 'inline'
script: |
docker push -a ${{ parameters.imgRepository }}
當然相對應的jobs/buildImage.yaml job template也要加上對應的參數設定:
parameters:
- name: artifactName
type: string
default: OutputFiles
- name: unzip
type: boolean
default: false
- name: zipFileName
type: string
default: ''
- name: unzipToFolderPath
type: string
default: ''
# 因為Dockerfile從範本的Git Repo取得,所以要加上定義的名稱
- name: templateResourceName
type: string
default: templates
- name: imgRepository
type: string
- name: imgTags
type: object
- name: buildDockerfile
type: string
default: Build.Dockerfile
- name: buildContext
type: string
- name: containerRegistry
type: string
# 加上buildArgs參數
- name: buildArgs
type: string
default: ''
jobs:
- job: BuildImage
dependsOn: BuildCode
steps:
# 加上checkout從範本所在的Git Repo取得Dockerfile
- checkout: ${{ parameters.templateResourceName }}
path: ${{ parameters.templateResourceName }}
clean: true
- template: ../steps/download-pipeline-artifacts.yaml
parameters:
artifactName: ${{ parameters.artifactName }}
unzip: ${{ parameters.unzip }}
zipFileName: ${{ parameters.zipFileName }}
unzipToFolderPath: ${{ parameters.unzipToFolderPath }}
- template: ../steps/docker-build.yaml
parameters:
imgRepository: ${{ parameters.imgRepository }}
imgTags: ${{ parameters.imgTags }}
buildDockerfile: ${{ parameters.buildDockerfile }}
buildContext: ${{ parameters.buildContext }}
containerRegistry: ${{ parameters.containerRegistry }}
# 用條件判斷要不要加上buildArgs參數設定
${{ if ne(parameters.buildArgs, '') }}:
buildArgs: ${{ parameters.buildArgs }}
在stages/dotnet-build-stage.yaml stage template的部份則是增加startFileName參數,讓專案的Pipeline YAML傳入真正要執行的檔名:
parameters:
# buildCode.yaml parameters
- name: sourcePath
type: string
default: $(Build.SourcesDirectory)
- name: slnOrCsprojName
type: string
default: Pipeline.sln
# buildImage.yaml parameters
- name: artifactName
type: string
default: OutputFiles
- name: unzip
type: boolean
default: false
- name: zipFileName
type: string
default: ''
- name: unzipToFolderPath
type: string
default: ''
- name: imgRepository
type: string
- name: imgTags
type: object
- name: buildDockerfile
type: string
default: Build.Dockerfile
- name: buildContext
type: string
- name: containerRegistry
type: string
# 讓專案的YAML傳入真正要執行的DLL檔名
- name: startFileName
type: string
# 因為Dockerfile從範本的Git Repo取得,所以要加上定義的名稱
- name: templateResourceName
type: string
default: templates
stages:
- stage: BuildSourceAndImage
displayName: 編譯程式碼和建立Docker Image
jobs:
- template: ../jobs/buildCode.yaml
parameters:
sourcePath: ${{ parameters.sourcePath }}
slnOrCsprojName: ${{ parameters.slnOrCsprojName }}
- template: ../jobs/buildImage.yaml
parameters:
# 加上templateResourceName參數
templateResourceName: ${{ parameters.templateResourceName }}
artifactName: ${{ parameters.artifactName }}
unzip: ${{ parameters.unzip }}
zipFileName: ${{ parameters.zipFileName }}
unzipToFolderPath: ${{ parameters.unzipToFolderPath }}
imgRepository: ${{ parameters.imgRepository }}
imgTags: ${{ parameters.imgTags }}
buildDockerfile: ${{ parameters.buildDockerfile }}
buildContext: ${{ parameters.buildContext }}
containerRegistry: ${{ parameters.containerRegistry }}
# 因為在.Net系統一定會使用buildArgs,並且設定TARGET_FILE這個在Dockerfile內設定的ARG,所以不用條件判斷
buildArgs: "TARGET_FILE=${{ parameters.startFileName }}"
最後就是在專案的CI Pipeline YAML內增加startFileName的參數設定就行了,這部份只貼有修改的部份YAML內容:
stages:
- template: stages/dotnet-build-stage.yaml@templates
parameters:
sourcePath: $(Build.SourcesDirectory)
slnOrCsprojName: $(slnOrCsprojName)
artifactName: '$(pipelineArtifact)'
unzip: true
zipFileName: buildResult.zip
unzipToFolderPath: $(System.ArtifactsDirectory)/buildImage
imgRepository: $(imgRepository)
imgTags: |
latest
buildDockerfile: $(buildDockerfile)
buildContext: $(System.ArtifactsDirectory)/buildImage
containerRegistry: $(imgRegistryService)
# 加上下面這兩行
startFileName: IronmanWeb.dll
templateResourceName: templates
上面加上startFileName的部份更好的方式應該是透過變數來設定,因為別的地方可能也會用到專案的名稱(以這邊的例子就是IronmanWeb),所以或許會是下面這樣:
variables:
projectName: IronmanWeb
startFileName: $(projectName).dll
這樣和projectName一樣的地方就只需要設定一次,修改也不會漏改其它地方。