LoginSignup
0
1

Azure DevOps PipelineからBicep deploy

Last updated at Posted at 2024-04-09

今更ながらBicepの勉強。ただのMSLearnのまとめです。

Azure DevOps Pipeline自体の説明が多くなったが、復習ということで。

準備

Service ConnectionにAzureへの接続情報を設定

Azure PipelineからAzureにアクセスするためにService Connectionを利用する。

image.png
 
image.png
 
image.png

サービスプリンシパル(マニュアル)

サービスプリンシパルを手動で作る場合の手順。
Automaticの場合はいかが自動で行われる。

Bicepファイルを手動でDeployするときはその人間の認証情報で実行される。PipelineからAzureを操作するのにはサービスプリンシパルを利用する。原則Pipeline実行にはマネージドIDは不可。
image.png

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が追加されているのを確認できる。
image.png

Bicep file作成

ちょっと長いけど、作るリソースは3つだけ。
image.png

※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から。
     
    image.png
    シークレットに設定や実行時にオーバーライドが選択可能。
    image.png
     
    image.png
    定義した変数はさっきのyamlにあるように$で利用可能。
     
  • Variable Groupという変数を共有できる機能もある
    パイプライン専用の変数はここには表示されない。
    image.png

実行

Runを押すとVariablesが表示される。
image.png
 
上書き可能にしたものはここで変更可能。
image.png
image.png

デプロイ履歴を確認したところ、日付になってた。deploymentName: $(Build.BuildNumber)の引数によるものかと。
image.png

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で紹介されている代表的なものは以下。

  • 順次実行
     
    image.png
// 何もしない普通にやると順次になる
stages:

- stage: Test
  jobs:
  - job: Test

- stage: DeployUS
  jobs: 
  - job: DeployUS

- stage: DeployEurope
  jobs: 
  - job: DeployEurope
  • 依存関係あり実行(並列という意味ではない)

image.png

// dependsOnで前のジョブに依存するように指定(正直イマイチ必要性がわからない。。)
stages:

- stage: Test
  jobs:
  - job: Test

- stage: DeployUS
  dependsOn: Test
  jobs: 
  - job: DeployUS

- stage: DeployEurope
  dependsOn: Test
  jobs: 
  - job: DeployEurope
  • 条件指定実行

image.png

// 前のステップが失敗したらRollbackを実行する
stages:

- stage: Test
 jobs:
 - job: Test

- stage: Deploy
 dependsOn: Test
 jobs: 
 - job: Deploy

- stage: Rollback
 condition: failed('Deploy')
 jobs: 
 - job: Rollback

IaCでのValidation

こんな感じで順を追って実行していく。

image.png

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)

前のStageが失敗しているのでDeployまでいかない。
image.png

What Ifして人が承認する

上記でもそこそこいい気がするが、What Ifをして、その結果を元に本当にDeployに問題がなさそうか人の承認を取り入れたい。
image.png

Environmentという機能でDeploy Jobという特殊なJobを作る必要がある。
image.png

image.png

Approvalsを選択
image.png

承認者を選択
image.png

// 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)

メールが届いた。
image.png

Review approvalを押すと実行中の画面になる。
image.png

PreviewのJobを確認するとWhat Ifの結果が見れる。
image.png

OKであればReviewからApprove。
image.png

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"

テスト結果の確認

image.png

Testsタブで結果の確認。
image.png

テスト失敗時のRollback or Rollforward

az deployment group create --rollback-on-errorで直前の成功したDeploment、もしくは過去の特定のDeploymentを再実行するということができる。

が、deploymentmodeがcompleteでない限り、作成されたリソースは削除されないので、完全に戻すことは難しい。そのため、ダメだった個所を修正してDeployしなおすRollForward推奨。

最後に

Bicepの勉強もこれで一区切りにします。本当はPipelineの上級者向けやBicepの上級もあるけど、それはまたいつか。。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1