やりたいこと
Azure DevOps での CI/CD をトライアル的にやってみたので、その内容を書いてみます。
お試し感強めなのでご参考まで!
パイプライン内容
パイプラインで行うのは Terraform での Azure リソースの管理で、構成は以下のような形。
Developer が PR を発行すると PR 時用のパイプラインが動作し、各種 Validation を行う。その後 terraform plan
を行い、その内容の確認 (手動) を必須にしている。確認が取れれば PR パイプラインが成功して Merge 可能になる。
Merge された後は Merge 用のパイプラインが動作し、まず承認者によるリリースの承認を求めて、そこで承認が得られれば実際に terraform apply
を実行する、という流れになる。
パイプライン作成
Azure DevOps でのパイプライン作成は、クラシックパイプラインと YAML パイプラインがあり、クラシックは GUI 上で作成していくが、YAML の方はその名の通り YAML ファイルで作成する。
今回は他の SCM ツールでの CI/CD でも使われている YAML ファイルで作成する。以下のドキュメントを参考に作成していく。
作成の際は VSCode などのエディタで作成してもよいが、Azure DevOps 上で編集すると Intelligence も使えるので便利。
Stage, Job, Step の関係
各 Job は1つの Agent で実行される という点に注意。Job 毎に環境が分かれてしまうので、ある Job で何かツールをインストールしても別の Job ではそのツールを使えない。そのため、ツールのインストールなどは Job 毎に行う必要がある。また、あるジョブで作成したファイルを別のジョブで参照することも基本的にはできない (Artifact として扱えばできるかも?)。
Azure への接続
Azure へ接続 (認証) するにはいくつか方法があるが、今回は専用のサービスプリンシパルを作成してその認証情報を使う。権限のスコープは用途に応じて最小限にするのが良いためリポジトリ毎にサービスプリンシパルを作成するのがベター。
Azure へのサービスコネクタを事前に作成して、その認証情報をパイプラインから呼び出す形になる。
- task: AzureCLI@2
inputs:
azureSubscription: {YOUR_SERVICE_CONNECTION}
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az --version
az account show
addSpnToEnvironment: true
Terraform 拡張機能
Terraform を使うにあたり、自前でスクリプトを書いてインストールしても良いが手間なので拡張機能を利用する。
「Get it free」をクリックしてリポジトリのある Organization にインストールする。
使う際は以下のようにタスクを呼び出してパラメータを設定する。
- task: TerraformTaskV4@4
displayName: terraform init
inputs:
provider: 'azurerm'
command: 'init'
workingDirectory: {WORKING_DIR}
backendServiceArm: {YOUR_SERVICE_CONNECTION}
backendAzureRmResourceGroupName: {YOUR_RESOURCE_GROUP}
backendAzureRmStorageAccountName: {YOUR_STORAGE_ACCOUNT}
backendAzureRmContainerName: {YOUR_CONATINER}
backendAzureRmKey: {YOUR_STATEFILE}
PR トリガ
ここが一番分かりづらいところ。
YMAL スキーマの中に trigger
や pr
があり当初はそれらを使おうと思っていたがどうもうまく動作しなかった。よく調べてみると、GUI 側の設定でオーバーライドする必要があるそう。
承認 (検証)
何らかリリースする際に内容を確認したり承認が必要だったりという場合には、手動検証と承認チェックを利用できる。
手動検証
手動検証のタスクを利用することで、それ以降のフローを一時停止させて再開 or 拒否するかの確認をできるようになる。
承認チェック
リリースする環境を DevOps 上で事前に作成 (定義) しておき、その環境を YAML ファイル内で job に割り当てることでそのジョブを実行する前に承認を求められるようになる。
PR パイプラインと Merge パイプライン
Azure DevOps ではパイプラインを複数作成可能で、それぞれ別の YAML ファイルに記述できる。
1つのファイルでどちらもトリガできるとは思うが、分かりづらくなるのでファイル分割した。
作成した YAML ファイル
ということで作成した2つの YAML ファイルは以下。正直パイプラインの完成度はイマイチなのでご参考までです!
pool:
vmImage: ubuntu-latest
# trigger 設定は Pull Request 時に CI を動作させるために、
# DevOps の GUI 設定で無効化している
# ファイルに残している理由:
# 1. trigger 設定を削除するとすべてのブランチへの Merge 時にトリガされてしまう
# 2. trigger: none にすると PR 時のトリガも動作しなくなってしまう
# https://zenn.dev/nuits_jp/articles/2023-07-02-azure-pipelines-pr-trigger
trigger:
- main
stages:
- stage: Validation
jobs:
- job: Format
steps:
- task: TerraformInstaller@0
displayName: install terraform
inputs:
terraformVersion: 'latest'
- script: |
cd infra/ && pwd
terraform fmt -check
if [ $? -ne 0 ]; then echo "You need to format"; exit 1; fi
displayName: terraform fmt
- job: Lint
steps:
- script: |
curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
tflint --version
displayName: install tflint
- script: |
cd infra/ && pwd
tflint --init
tflint
displayName: tflint
- job: Validate
steps:
- task: TerraformInstaller@0
displayName: install terraform
inputs:
terraformVersion: 'latest'
- task: TerraformTaskV4@4
displayName: terraform init
inputs:
provider: 'azurerm'
command: 'init'
workingDirectory: {WORKING_DIR}
backendServiceArm: {YOUR_SERVICE_CONNECTION}
backendAzureRmResourceGroupName: {YOUR_RESOURCE_GROUP}
backendAzureRmStorageAccountName: {YOUR_STORAGE_ACCOUNT}
backendAzureRmContainerName: {YOUR_CONATINER}
backendAzureRmKey: {YOUR_STATEFILE}
- task: TerraformTaskV4@4
displayName: terraform validate
inputs:
provider: 'azurerm'
command: 'validate'
workingDirectory: {YOUR_WORKING_DIR}
- stage: Planning
jobs:
- job: Plan
steps:
- task: TerraformInstaller@0
displayName: install terraform
inputs:
terraformVersion: 'latest'
- task: TerraformTaskV4@4
displayName: terraform init
inputs:
provider: 'azurerm'
command: 'init'
workingDirectory: {WORKING_DIR}
backendServiceArm: {YOUR_SERVICE_CONNECTION}
backendAzureRmResourceGroupName: {YOUR_RESOURCE_GROUP}
backendAzureRmStorageAccountName: {YOUR_STORAGE_ACCOUNT}
backendAzureRmContainerName: {YOUR_CONATINER}
backendAzureRmKey: {YOUR_STATEFILE}
- task: TerraformTaskV4@4
displayName: terraform plan
inputs:
provider: 'azurerm'
command: 'plan'
workingDirectory: {YOUR_WORKING_DIR}
environmentServiceNameAzureRM: {YOUR_SERVICE_CONNECTION}
- stage: RequestManualValidation
jobs:
- job: ManualValidation
pool: server
steps:
- task: ManualValidation@0
displayName: check terraform plan
timeoutInMinutes: 1440 # task times out in 1 day
inputs:
notifyUsers: |
{USER_EMAIL}
instructions: 'Please validate the build configuration and resume'
onTimeout: 'reject'
pool:
vmImage: ubuntu-latest
trigger:
branches:
include:
- main
paths:
include:
- {TARGET_PATH}
stages:
- stage: Provisioning
jobs:
- deployment: Apply
environment: Check Terraform Plan
strategy:
runOnce:
deploy:
steps:
- checkout: self
- task: TerraformInstaller@0
displayName: install terraform
inputs:
terraformVersion: 'latest'
- task: TerraformTaskV4@4
displayName: terraform init
inputs:
provider: 'azurerm'
command: 'init'
workingDirectory: {WORKING_DIR}
backendServiceArm: {YOUR_SERVICE_CONNECTION}
backendAzureRmResourceGroupName: {YOUR_RESOURCE_GROUP}
backendAzureRmStorageAccountName: {YOUR_STORAGE_ACCOUNT}
backendAzureRmContainerName: {YOUR_CONATINER}
backendAzureRmKey: {YOUR_STATEFILE}
- task: TerraformTaskV4@4
displayName: terraform apply
inputs:
provider: 'azurerm'
command: 'apply'
workingDirectory: {WORKING_DIR}
environmentServiceNameAzureRM: {YOUR_SERVICE_CONNECTION}
以上です。