今更ながらBicepの勉強。ただのMSLearnのまとめです。
Azure DevOps Pipeline自体の説明が多くなったが、復習ということで。
Biceps基礎はこちら。
準備
Service ConnectionにAzureへの接続情報を設定
Azure PipelineからAzureにアクセスするためにService Connectionを利用する。
サービスプリンシパル(マニュアル)
サービスプリンシパルを手動で作る場合の手順。
Automaticの場合はいかが自動で行われる。
Bicepファイルを手動でDeployするときはその人間の認証情報で実行される。PipelineからAzureを操作するのにはサービスプリンシパルを利用する。原則Pipeline実行にはマネージドIDは不可。
SPを作るときに同時に権限付与する。パイプラインで実行する処理に合わせてSPに権限をRBACで付与する。推奨はResource Groupの共同作成者Contributorロール。
az role assignment create \
--assignee APPLICATION_ID \
--role Contributor \
--scope RESOURCE_GROUP_ID \
--description "The deployment pipeline for the company's website needs to be able to create resources within the resource group."
// Response
{
"appId": "{appid}",
"displayName": "ToyWebsitePipeline",
"password": "{pwd}",
"tenant": "{tenant}"
}
コマンドの結果パスワードが返ってきている。
Entra IDでApp Registrationが追加されているのを確認できる。
Bicep file作成
※Bicepは全部で共通なので最後に載せる。
Bicep DeployのためのPipeline yaml作成
AzureResourceManagerTemplateDeployment@3タスクを使う。
trigger: none
pool:
vmImage: ubuntu-latest
variables:
- name: deploymentDefaultLocation
value: westus3
jobs:
- job:
steps:
- task: AzureResourceManagerTemplateDeployment@3
inputs:
connectedServiceName: $(ServiceConnectionName)
deploymentName: $(Build.BuildNumber)
location: $(deploymentDefaultLocation)
resourceGroupName: $(ResourceGroupName)
csmFile: deploy/main.bicep
overrideParameters: >
-environmentType $(EnvironmentType)
-deployToyManualsStorageAccount $(DeployToyManualsStorageAccount)
Variableを設定
パラメータの持ち方・渡し方はいくつかある。
- 渡さないでbicepの中の既定値を利用する
- Pipelineのyamlからデプロイタスクを呼ぶときに渡す
- DevOpsのエディタからパイプライン専用のVariablesで設定する
MSLearnではこのやり方で。以下の右上のVariablesから。
シークレットに設定や実行時にオーバーライドが選択可能。
定義した変数はさっきのyamlにあるように$で利用可能。
- Variable Groupという変数を共有できる機能もある
パイプライン専用の変数はここには表示されない。
実行
Runを押すとVariablesが表示される。
上書き可能にしたものはここで変更可能。
デプロイ履歴を確認したところ、日付になってた。deploymentName: $(Build.BuildNumber)の引数によるものかと。
Pipelineトリガー
// ブランチやパスフィルター
trigger:
branches:
include:
- main
paths:
exclude:
- docs
include:
- deploy
// もしくはTriggerの代わり or 追加でcron式で自動実行
schedules:
- cron: "0 0 * * *"
displayName: Daily environment restore
branches:
include:
- main
// 短時間で複数の更新があった時などは平行して実行される
// IaCのデプロイパイプラインは順次実行の方がよい、batch: true推奨
trigger:
batch: true
branches:
include:
- main
Stageを使う
基本的に上記までだけでも動くが、IaCの場合Validationやステージを利用して柔軟に設定する。これらのジョブはステージごとに新しいエージェントで動く。
※上記の記事を丸パクリしているだけなので、直接読んだ方がいい。。
ConditionやDependsOnについても色々細かくカスタマイズ可能。
Stageのパターン
MSLearnで紹介されている代表的なものは以下。
// 何もしない普通にやると順次になる
stages:
- stage: Test
jobs:
- job: Test
- stage: DeployUS
jobs:
- job: DeployUS
- stage: DeployEurope
jobs:
- job: DeployEurope
- 依存関係あり実行(並列という意味ではない)
// dependsOnで前のジョブに依存するように指定(正直イマイチ必要性がわからない。。)
stages:
- stage: Test
jobs:
- job: Test
- stage: DeployUS
dependsOn: Test
jobs:
- job: DeployUS
- stage: DeployEurope
dependsOn: Test
jobs:
- job: DeployEurope
- 条件指定実行
// 前のステップが失敗したらRollbackを実行する
stages:
- stage: Test
jobs:
- job: Test
- stage: Deploy
dependsOn: Test
jobs:
- job: Deploy
- stage: Rollback
condition: failed('Deploy')
jobs:
- job: Rollback
IaCでのValidation
こんな感じで順を追って実行していく。
Linting
Validation、構文チェック。
// 未使用変数、かつsecure()で、かつ既定値ありのパラメータを定義
@secure()
param someSecretVal string = 'testSecret!'
// Linting = ARM TemplateへTranspileしてみる
az bicep build --file main.bicep
// 警告が表示される
C:\work\Bicep\Pipeline\deploy> az bicep build --file main.bicep
C:\work\Bicep\Pipeline\deploy\main.bicep(2,7) : Warning no-unused-params: Parameter "someSecretVal" is declared but never used. [https://aka.ms/bicep/linter/no-unused-params]
C:\work\Bicep\Pipeline\deploy\main.bicep(2,28) : Warning secure-parameter-default: Secure parameters should not have hardcoded defaults (except for empty or newGuid()). [https://aka.ms/bicep/linter/secure-parameter-default]
bicepconfig.jsonで警告をエラーにするとかエラーレベルの設定が可能。(これはVisual Studio Codeで書いているときにも警告として表示されるので、その時に対応してくれればよい。Mustではないかもしれないが。。)
その他として、ここでPSRuleを実行するのもいいかも(個々人の環境作るのが大変な場合?)
Preflight検証
既に使用されているリソース名やリージョンによってデプロイ可否とかをデプロイ前にチェックする。
DeploymentModeでValidationにする。
- Incremental(既定)
- Complete(既存削除+完全新規)
- Validation(検証)
なので、以下のコマンドでは実際のデプロイはされない。
- stage: Validate
jobs:
- job: Validate
steps:
- task: AzureResourceManagerTemplateDeployment@3
inputs:
connectedServiceName: 'MyServiceConnection'
location: $(deploymentDefaultLocation)
deploymentMode: Validation // DeploymentModeをValidationにする!
resourceGroupName: $(ResourceGroupName)
csmFile: deploy/main.bicep
Stageを踏まえたDeploy
上記2点を踏まえると、以下のようなYamlができる。
# triggerやpoolとかvariablesとか省略
stages:
- stage: Lint
jobs:
- job: LintCode
displayName: Lint code
// 警告をエラーにレベルを変更しないとこのままだと警告だけで通る
steps:
- script: |
az bicep build --file deploy/main.bicep
name: LintBicepCode
displayName: Run Bicep linter
- stage: Validate
jobs:
- job: ValidateBicepCode
displayName: Validate Bicep code
steps:
- task: AzureResourceManagerTemplateDeployment@3
name: RunPreflightValidation
displayName: Run preflight validation
inputs:
connectedServiceName: $(ServiceConnectionName)
location: $(deploymentDefaultLocation)
deploymentMode: Validation
resourceGroupName: $(ResourceGroupName)
csmFile: deploy/main.bicep
overrideParameters: >
-environmentType $(EnvironmentType)
- stage: Deploy
jobs:
- job: Deploy
steps:
- task: AzureResourceManagerTemplateDeployment@3
name: Deploy
displayName: Deploy to Azure
inputs:
connectedServiceName: $(ServiceConnectionName)
deploymentName: $(Build.BuildNumber)
location: $(DeploymentDefaultLocation)
resourceGroupName: $(ResourceGroupName)
csmFile: deploy/main.bicep
overrideParameters: >
-environmentType $(EnvironmentType)
What Ifして人が承認する
上記でもそこそこいい気がするが、What Ifをして、その結果を元に本当にDeployに問題がなさそうか人の承認を取り入れたい。
Environmentという機能でDeploy Jobという特殊なJobを作る必要がある。
// Triggerや検証部分等は省略
// 承認の設定とかをYamlファイルで定義できないのが残念に感じる
- stage: Preview
jobs:
- job: PreviewAzureChanges
displayName: Preview Azure changes
steps:
- task: AzureCLI@2
name: RunWhatIf
displayName: Run what-if
inputs:
azureSubscription: $(ServiceConnectionName)
scriptType: 'bash'
scriptLocation: 'inlineScript'
// What Ifの実行
inlineScript: |
az deployment group what-if \
--resource-group $(ResourceGroupName) \
--template-file deploy/main.bicep \
--parameters environmentType=$(EnvironmentType)
- stage: Deploy
jobs:
- deployment: DeployWebsite
displayName: Deploy website
environment: Website
strategy:
runOnce:
deploy:
steps:
// Environementを利用する場合、新しくファイルをチェックアウト(ダウンロード)し、
// その環境にデプロイする
- checkout: self
- task: AzureResourceManagerTemplateDeployment@3
name: DeployBicepFile
displayName: Deploy Bicep file
inputs:
connectedServiceName: $(ServiceConnectionName)
deploymentName: $(Build.BuildNumber)
location: $(deploymentDefaultLocation)
resourceGroupName: $(ResourceGroupName)
csmFile: deploy/main.bicep
overrideParameters: >
-environmentType $(EnvironmentType)
PreviewのJobを確認するとWhat Ifの結果が見れる。
Deploy結果をテストする
もうそろそろお腹一杯になってきた。。。
いわゆるスモークテストの実行。ここではPowershellベースのPesterを使う。
面白いけど複雑なのでリンクも載せておく。
Pesterによるスモークテストの実行
以下のファイルを作成する。
テストでは前段階でDeployしたApp ServiceのHost名を受け取って、それに対して単純に疎通確認だけを行う。
Pesterでテストケースを作成
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $HostName
)
Describe 'Toy Website' {
It 'Serves pages over HTTPS' {
$request = [System.Net.WebRequest]::Create("https://$HostName/")
$request.AllowAutoRedirect = $false
$request.GetResponse().StatusCode |
Should -Be 200 -Because "the website requires HTTPS"
}
}
bicepでouputを定義
bicep側でoutputとしてDeployしたApp ServiceのHostNameを出力していることを確認する。これを最終的にPesterのスクリプトに渡したい。
@description('The Azure region into which the resources should be deployed.')
param location string = resourceGroup().location
@description('The type of environment. This must be nonprod or prod.')
@allowed([
'nonprod'
'prod'
])
param environmentType string
@description('Indicates whether to deploy the storage account for toy manuals.')
param deployToyManualsStorageAccount bool = false
@description('A unique suffix to add to resource names that need to be globally unique.')
@maxLength(13)
param resourceNameSuffix string = uniqueString(resourceGroup().id)
var appServiceAppName = 'toy-website-${resourceNameSuffix}'
var appServicePlanName = 'toy-website-plan'
var toyManualsStorageAccountName = 'toyweb${resourceNameSuffix}'
// Define the SKUs for each component based on the environment type.
var environmentConfigurationMap = {
nonprod: {
appServicePlan: {
sku: {
name: 'F1'
capacity: 1
}
}
toyManualsStorageAccount: {
sku: {
name: 'Standard_LRS'
}
}
}
prod: {
appServicePlan: {
sku: {
name: 'S1'
capacity: 2
}
}
toyManualsStorageAccount: {
sku: {
name: 'Standard_ZRS'
}
}
}
}
var toyManualsStorageAccountConnectionString = deployToyManualsStorageAccount
? 'DefaultEndpointsProtocol=https;AccountName=${toyManualsStorageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${toyManualsStorageAccount.listKeys().keys[0].value}'
: ''
resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = {
name: appServicePlanName
location: location
sku: environmentConfigurationMap[environmentType].appServicePlan.sku
}
resource appServiceApp 'Microsoft.Web/sites@2022-03-01' = {
name: appServiceAppName
location: location
properties: {
serverFarmId: appServicePlan.id
httpsOnly: true
siteConfig: {
appSettings: [
{
name: 'ToyManualsStorageAccountConnectionString'
value: toyManualsStorageAccountConnectionString
}
]
}
}
}
resource toyManualsStorageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' =
if (deployToyManualsStorageAccount) {
name: toyManualsStorageAccountName
location: location
kind: 'StorageV2'
sku: environmentConfigurationMap[environmentType].toyManualsStorageAccount.sku
}
output appServiceAppHostName string = appServiceApp.properties.defaultHostName
// Outputはこんな感じになる
{
"appServiceAppHostName": {
"type": "String",
"value": "toy-website-xunjewfivysf2.azurewebsites.net"
}
}
PipelineでDeployしたApp ServiceのHostNameを変数としてPesterのJobに渡す
変数の渡し方がかなりややこしい。
// 普通にPipelineの中でStage間に変数を受け渡すのならこれで済むのだが
$[ stageDependencies.{stage名}.{job/deployment名}.outputs['変数名'] ]
// task.setvariableにより動的にパイプラインの実行中に変数を設定している
// bicepの出力がjson形式のため、平文に変換してやる必要がある、jqというコマンドがそれをしている
- bash: |
echo "##vso[task.setvariable variable=appServiceAppHostName;isOutput=true]$(echo $DEPLOYMENT_OUTPUTS | jq -r '.appServiceAppHostName.value')"
name: SaveDeploymentOutputs
displayName: Save deployment outputs into variables
env:
DEPLOYMENT_OUTPUTS: $(deploymentOutputs)
// 詳しくは公式へ
https://learn.microsoft.com/ja-jp/training/modules/test-bicep-code-using-azure-pipelines/9-exercise-add-test-stage-pipeline?pivots=cli
trigger:
batch: true
branches:
include:
- main
pool:
vmImage: ubuntu-latest
variables:
- name: deploymentDefaultLocation
value: westus3
stages:
- stage: Lint
jobs:
- job: LintCode
displayName: Lint code
steps:
- script: |
az bicep build --file deploy/main.bicep
name: LintBicepCode
displayName: Run Bicep linter
- stage: Validate
jobs:
- job: ValidateBicepCode
displayName: Validate Bicep code
steps:
- task: AzureResourceManagerTemplateDeployment@3
name: RunPreflightValidation
displayName: Run preflight validation
inputs:
connectedServiceName: $(ServiceConnectionName)
location: $(deploymentDefaultLocation)
deploymentMode: Validation
resourceGroupName: $(ResourceGroupName)
csmFile: deploy/main.bicep
overrideParameters: >
-environmentType $(EnvironmentType)
- stage: Preview
jobs:
- job: PreviewAzureChanges
displayName: Preview Azure changes
steps:
- task: AzureCLI@2
name: RunWhatIf
displayName: Run what-if
inputs:
azureSubscription: $(ServiceConnectionName)
scriptType: "bash"
scriptLocation: "inlineScript"
inlineScript: |
az deployment group what-if \
--resource-group $(ResourceGroupName) \
--template-file deploy/main.bicep \
--parameters environmentType=$(EnvironmentType)
- stage: Deploy
jobs:
- deployment: DeployWebsite
displayName: Deploy website
environment: Website
strategy:
runOnce:
deploy:
steps:
- checkout: self
- task: AzureResourceManagerTemplateDeployment@3
name: DeployBicepFile
displayName: Deploy Bicep file
inputs:
connectedServiceName: $(ServiceConnectionName)
deploymentName: $(Build.BuildNumber)
location: $(deploymentDefaultLocation)
resourceGroupName: $(ResourceGroupName)
csmFile: deploy/main.bicep
overrideParameters: >
-environmentType $(EnvironmentType)
deploymentOutputs: deploymentOutputs
- bash: |
echo "##vso[task.setvariable variable=appServiceAppHostName;isOutput=true]$(echo $DEPLOYMENT_OUTPUTS | jq -r '.appServiceAppHostName.value')"
name: SaveDeploymentOutputs
displayName: Save deployment outputs into variables
env:
DEPLOYMENT_OUTPUTS: $(deploymentOutputs)
- stage: SmokeTest
jobs:
- job: SmokeTest
displayName: Smoke test
variables:
appServiceAppHostName: $[ stageDependencies.Deploy.DeployWebsite.outputs['DeployWebsite.SaveDeploymentOutputs.appServiceAppHostName'] ]
steps:
- task: PowerShell@2
name: RunSmokeTests
displayName: Run smoke tests
inputs:
targetType: inline
script: |
$container = New-PesterContainer `
-Path 'deploy/Website.Tests.ps1' `
-Data @{ HostName = '$(appServiceAppHostName)' }
Invoke-Pester `
-Container $container `
-CI
- task: PublishTestResults@2
name: PublishTestResults
displayName: Publish test results
condition: always()
inputs:
testResultsFormat: NUnit
testResultsFiles: "testResults.xml"
テスト結果の確認
テスト失敗時のRollback or Rollforward
az deployment group create --rollback-on-errorで直前の成功したDeploment、もしくは過去の特定のDeploymentを再実行するということができる。
が、deploymentmodeがcompleteでない限り、作成されたリソースは削除されないので、完全に戻すことは難しい。そのため、ダメだった個所を修正してDeployしなおすRollForward推奨。
最後に
Bicepの勉強もこれで一区切りにします。本当はPipelineの上級者向けやBicepの上級もあるけど、それはまたいつか。。