😊

Azure Pipelines×Bicep×マネージドDevOpsプールによるAzure自動構築入門

に公開

はじめに

Azureでリソースを構築するときには、IaCの選択肢としてはTerraformやBicepがメジャーどころにあります。ノウハウの多さからTerraformを選択することも多かったのですが、最近「マルチクラウドならともかく、AzureだけのシステムならばBicepの方がいいのでは?」と考えるようになりました。
理由としては、Azure純正のIaCであることから「Azureサポートを受けやすい」「Terraformで対応していない痒い所に手が届く」という印象が強いためです。筆者自身、Terraformを使ったCI/CDは実装したことがありますが、Bicepを使った場合のハマりポイントが分からなかったため、整理も含めてアウトプットを行います。

検証観点

  • CI/CDパイプラインが正しく実行できること
    • マージトリガーや静的解析を含む
  • Bicepのモジュール化構成
  • Azure Pipelinesの新エージェントの利用

Bicep単位での動作確認

Bicep単体でリソースが構築できることの確認です。Azure CLIを使ってデプロイを実施しました。筆者は検証簡略のために、Azure Portal上のCloud ShellにBicepをアップロードして実施しました。そのため、az loginなどの認証は割愛します。

ResourceGroup作成用のBicep
sample.bicep
targetScope = 'subscription'
param resourceGroupName string = 'test430-bicep-rg'
param Location string = 'japaneast'
resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = {
  name: resourceGroupName
  location: Location
}
az deployment sub create --location japaneast --template-file sample.bicep

CI/CDパイプラインの実装とBicepの組み込み

BicepコードとAzure Pipelines(yaml)は、別のリポジトリで管理することにしています。役割やライフサイクルの違うファイルは別のリポジトリ管理を目的としています。
CI/CDパイプラインの構成としては、Pull Requestの際に実行されるCIパイプラインマージトリガーで実行されるCDパイプラインの2構成としています。
CIパイプラインでは、Validateを使ったBicepファイルの構文チェックと、Azure CLIを使ったWhat-if(ドライラン)を実施しています。validate はコードの構文チェックであり、ドライランではないことに注意してください。

CIパイプライン
CI.yaml
trigger:
- none

resources:
 repositories:
  - repository: iac-test
    type: git
    name: <ADOのプロジェクト名>/iac-test
    ref: refs/heads/feature*

pool:
  vmImage: ubuntu-latest

variables:
- name: deploymentDefaultLocation
  value: japaneast

jobs:
- job:
  steps:
  - checkout: iac-test

  - task: AzureResourceManagerTemplateDeployment@3
    inputs:
      deploymentScope: 'Subscription'
      azureResourceManagerConnection: 'iac-test-sc'
      subscriptionId: '<サブスクリプションID>'
      location: '$(deploymentDefaultLocation)'
      templateLocation: 'Linked artifact'
      csmFile: 'sample.bicep'
      deploymentMode: 'Validation'
      deploymentName: '$(Build.BuildNumber)'
  
  - task: AzureCLI@2
    inputs:
      azureSubscription: 'iac-test-sc'
      scriptType: 'bash'
      scriptLocation: 'inlineScript'
      inlineScript: |
        az deployment sub what-if \
          --location japaneast \
          --template-file sample.bicep

CDパイプライン
CD.yaml
trigger: none

pool:
  vmImage: ubuntu-latest

resources:
  repositories:
  - repository: iac-test
    type: git
    name: <ADOのプロジェクト名>/iac-test
    ref: develop
    trigger: 
      branches: 
        include: 
          - develop

variables:
- name: deploymentDefaultLocation
  value: japaneast

jobs:
- job:
  steps:
  - checkout: iac-test

  - task: AzureResourceManagerTemplateDeployment@3
    inputs:
      deploymentScope: 'Subscription'
      azureResourceManagerConnection: 'iac-test-sc'
      subscriptionId: '<サブスクリプションID>'
      location: '$(deploymentDefaultLocation)'
      templateLocation: 'Linked artifact'
      deploymentMode: 'Incremental'
      deploymentName: '$(Build.BuildNumber)'
      csmFile: sample.bicep

プルリクエストを作成すると、CIパイプラインが実行されます。以下は実行結果の抜粋です。実際に作成されるリソースが事前に表示されていることが分かります(赤枠箇所)。
CI完了

プルリクエストがマージされると、CDパイプラインが実行されます。以下は実行結果の抜粋です。CIパイプラインで表示されていたWhat-ifは表示されていないことは違いとして見られます。
CD完了

実際にResourceGroupも作成されていました。
リソース実機確認

Bicepのモジュール化

簡易なBicepを使ったCI/CDは実施できました。次は、実プロジェクトを意識してBicepのモジュール化に取り組んでみます。
モジュール化では、再利用可能なテンプレートを各Azureサービスに定義します。モジュールを呼び出す際に、環境識別子(prdやstg)といった可変部分のみを渡すことで、実装コストを抑えることができるメリットがあります。
モジュール化やフォルダ構成は正解はありませんが、今回は以下の構成にて実施します。

/root
│
├── modules                  # 再利用可能なBicepモジュール
│   ├── vnet.bicep           # 仮想ネットワークの定義
│
├── param                    # 環境ごとのパラメータファイル
│   ├── prd                 # 本番環境
│   │   ├── common.bicepparam # 環境識別子などの共通パラメータ
│   ├── stg                  # ステージング環境
│   └── dev                  # 開発環境
│
├── main.bicep               # モジュールを読み込んで実行するファイル
├── .gitignore
└── README.md
vnet.bicep
vnet.bicep
@description('Vnet Bicep File')
param Environment string
param vnetName string = 'vnet-${Environment}-test'
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' = {
  name: vnetName
  location: resourceGroup().location
  properties: {
    addressSpace: {
      addressPrefixes: [
        '10.0.0.0/16'
      ]
    }
    subnets: [
      {
        name: 'Subnet-1'
        properties: {
          addressPrefix: '10.0.0.0/24'
        }
      }
      {
        name: 'Subnet-2'
        properties: {
          addressPrefix: '10.0.1.0/24'
        }
      }
    ]
  }
}

common.bicepparam
common.bicepparam
using '../../main.bicep'

param Environment = 'prd'

main.bicep
main.bicep
@description('Environment')
param Environment(1) string

@description('This is Main.Use module file')
module vnet 'modules/vnet.bicep' = {
  name: 'Vnet-deploy' //省略可能パラメータ
  params:{
    Environment(2):Environment(1)
  }

}

ファイルが分割するため、各ファイルの対応関係を以下に整理しています。特に変数が複数のファイルにまたがるため、そこを中心にマッピングしています。
モジュールファイル対応関係

また、パイプラインの実装も変更する必要がありました。変更後のyamlファイルも掲載します。

CIパイプライン
CI.yaml
trigger:
- none

resources:
 repositories:
  - repository: iac-test
    type: git
    name: <ADOのプロジェクト名>/iac-test
    ref: refs/heads/feature*

pool:
  vmImage: ubuntu-latest

variables:
- name: deploymentDefaultLocation
  value: japaneast

jobs:
- job:
  steps:
  - checkout: iac-test

  - task: AzureResourceManagerTemplateDeployment@3
    inputs:
      deploymentScope: 'Resource Group'
      azureResourceManagerConnection: 'iac-test-sc'
      subscriptionId: '<サブスクリプションID>'
      action: 'Create Or Update Resource Group'
      resourceGroupName: 'test430-bicep-rg'
      location: '$(deploymentDefaultLocation)'
      templateLocation: 'Linked artifact'
      csmFile: 'main.bicep'
      csmParametersFile: './param/prd/common.bicepparam'
      deploymentMode: 'Validation'
      deploymentName: '$(Build.BuildNumber)'
  
  - task: AzureCLI@2
    inputs:
      azureSubscription: 'iac-test-sc'
      scriptType: 'bash'
      scriptLocation: 'inlineScript'
      useGlobalConfig: false
      inlineScript: |
        az bicep uninstall
        az config set bicep.use_binary_from_path=false
        az bicep install
        
        # what-if実行(bicepparam使用可)
        az deployment group what-if \
          --resource-group test430-bicep-rg \
          --template-file main.bicep \
          --parameters ./param/prd/common.bicepparam

CDパイプライン
CD.yaml
trigger: none

pool:
  vmImage: ubuntu-latest

resources:
  repositories:
  - repository: iac-test
    type: git
    name: <ADOのプロジェクト名>/iac-test
    ref: develop
    trigger: 
      branches: 
        include: 
          - develop

variables:
- name: deploymentDefaultLocation
  value: japaneast

jobs:
- job:
  steps:
  - checkout: iac-test

  - task: AzureResourceManagerTemplateDeployment@3
    inputs:
      deploymentScope: 'Resource Group'
      azureResourceManagerConnection: 'iac-test-sc'
      subscriptionId: '<サブスクリプションID>'
      action: 'Create Or Update Resource Group'
      resourceGroupName: 'test430-bicep-rg'
      location: '$(deploymentDefaultLocation)'
      templateLocation: 'Linked artifact'
      csmFile: 'main.bicep'
      csmParametersFile: './param/prd/common.bicepparam'
      deploymentMode: 'Incremental'
      deploymentName: '$(Build.BuildNumber)'

パイプラインのモジュール化前後の変更概要は以下の通りです。

No 変更点 変更理由
1 deploymentScopeResourceGroupに修正 Ventがリソースグループを前提としてリソースのため、最小限のスコープ適用
2 csmParametersFile: './param/prd/common.bicepparam'を新規で追加 パラメータファイル化に伴い
3 Azure CLIのオプションをgroupに変更 No1と同様の理由
4 Azure CLIのオプションにparameters ./param/prd/common.bicepparamを追加 No2と同様の理由

CI/CDの流れはモジュール化前と同じです。マージトリガーで実行後にリソースを確認すると、環境識別子(Environment)がprdになっているリソースが作成されています。
モジュール化CD完了後のリソース実機確認

CI/CDの実行基盤のマネージドDevOpsプール化

メジャーなCI/CDエージェントとしては、MSホステッドエージェントやSelf-Hostedエージェントが存在します。
それぞれ、Vnet内のリソースへの操作が困難・実行基盤(VMやVMSS)の維持が必要というデメリットが存在しました。それを解消してくれるのが、マネージド DevOps プール(MDP)というサービスです。
ここまでの検証では、MSホステッドエージェントで進めてきました。これを、MDPに置き換えてたいと思います。
MDPのリソースをAzure Portalから作成する必要があります。具体的な手順は公式ドキュメントに従えば実施できるため割愛します。

リソースの作成後は、引き続き公式ドキュメントに従いyamlで指定するエージェントプールを修正するだけです。結果は割愛しますが、サブネットのCIDRの修正がMDPで実施できました。

MDPを使った感想

  1. 起動時間について
    エージェントの起動にはMSホステッドよりも時間がかかりました(特にCIパイプラインが顕著でした)。理由は、MDPの作成時に「Fresh agent every time」という設定にしているためです。予め利用時間が分かっているならばキャッシュ状態での起動をした方が良いです。4回しかMDPエージェントを起動していませんが、かかる時間はまちまちでした。MS側での割り当て次第なのでしょうか。

  2. エージェントの状態の可視化が容易に
    エージェントの状態(いつ起動したのか・終了したのかなど)は、Azureポータルのメトリックから確認ができます。
    MDPポータル画面

まとめ

BicepをAzure Pipelinesに統合し、新たなCI/CDエージェントとしてマネージドDevOpsプールを利用しました。Terraformで実施していた時と細かな違いはありつつも、最低限実現したいことは実施できそうです。また、MDPの利用も思ったより簡単であったため、今後はCI/CDエージェントの候補として浮上してくると感じしました。
以下は、今回の検証を通じた次回以降の整理ポイントです。また時間を見つけて整理したいと思います。

1. Bicepでデプロイスコープをサブスクリプションにしたままでも、リソースグループ以外のリソースの作成は可能なのか?
→ (UPDATE) 結論、可能です。ただし、この時にはBicepの書き方を工夫する必要があります。

  • 注意点1:スコープ定義
    リソースグループはSubscriptionがスコープになり、Vnetなどのリソースグループ配下に作るリソースはResourceGroupがスコープになるため、スコープの定義を工夫する必要があります。
    今回、リソースグループとVnetの作成を実装し、かつ共にモジュール化を図りました。
    BicepではデフォルトでスコープがResourceGroupのため、リソースグループ作成のBicepコードではSubscriptionスコープを明示する必要があります。
    また、モジュール化することによって、モジュール側はスコープを再定義することができます。これによって、Vnet側はResourceGroupスコープでのデプロイが可能になります。参考
  • 注意点2:リソース名の受け渡し
    リソースグループのモジュールのouputsで宣言したリソース名を、Vnet側に渡すという初期実装を行いましたが、Bicepのエラーが出ました。
    Bicepでスコープを指定する際、デプロイ開始時点で確定している値を使用する必要があります。outputsの出力値は、厳密にはデプロイ開始後に決まる値です。そのため、スコープの制約にかかってしまいました。
    スコープに渡したかったリソースグループ名を、bicepparam側に記載する(モジュール側に記載しない)ことで解決しました。

修正したコードも記載します。参考にしてください。

resourcegroup.bicep
resourcegroup.bicep
targetScope = 'subscription'
param resourceGroupName string
param Location string = 'japaneast'
resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = {
  name: resourceGroupName
  location: Location
}
common.bicepparam
common.bicepparam
using '../../main.bicep'

param Environment = 'prd'
param RgName = 'bicep-${Environment}-rg'
main.bicep
main.bicep
targetScope = 'subscription'
@description('Environment')
param Environment string
param RgName string
@description('RG')
module rg 'modules/resorcegroup.bicep' = {
  name: 'RG-deploy'
  scope: subscription()
  params:{
    resourceGroupName:RgName
  }
}

@description('This is Main.Use module file')
module vnet 'modules/vnet.bicep' = {
  name: 'Vnet-deploy' //省略可能パラメータ
  scope: resourceGroup(RgName)
  params:{
    Environment:Environment
  }
  dependsOn: [
    rg
  ]
}

2. .bicepparamは2つ以上存在することが可能なのか?(1つのparamの中で別のparamを呼び出すしかない?)
→ (UPDATE)Azure PipelinesでAzureResourceManagerTemplateDeployment@3を利用する場合には、1つのbicepparamしか指定できなさそうです。そのため、ファイルの肥大化がリスクにありますが、1つのパラメータファイルを追記していく方法しかなさそうです。
一方で、以下の回避策も取ることができます。

  • bicepparamではなく、bicepファイルにパラメータを記載する。
    パラメータファイルも1つのモジュールとして扱い、その中にパラメータ定義(param)とアウトプット(outputs)を記載します。
    パラメータを利用したいファイルからは、該当するパラメータファイル(bicep)を参照します。
    これによって、パラメータファイルの分割が可能です。また、AzureResourceManagerTemplateDeployment@3上では明確にパラメータファイルとして引数に渡す必要もありません。
  • AzureResourceManagerTemplateDeployment@3タスクではなく、インラインスクリプトでaz deploymentコマンドを実施する
    az deploymentのオプションである--parametersに、複数のbicepparamを渡すことができます。これによって実現は可能です。
    inlineScript: |
      az deployment group create \
        --resource-group $(resourceGroupName) \
        --template-file main.bicep \
        --parameters ./param/prd/common.bicepparam ./param/prd/additional.bicepparam
  1. bicepの静的解析にはどのようなことができるのか?
  2. MDPに元々インストールされていないソフトウェアを扱うためにはどうすればいいか?
    → (UPDATE)Pipelinesのyamlの中で、インストールスクリプトを使ってインストールするのが一番早そう。

参考にしたドキュメント

Discussion