Bicep を使って Azure リソースを作ってみた

この記事は、 KENTEM TechBlog アドベントカレンダー2024 14日目、12月14日の記事です。

Azure でサーバー管理を行っていて開発用に同じ環境を何度も作るのが嫌になる。作った環境の設定ミスに気づくのに時間がかかった。という経験はございませんか?
そんな私の様なあなたに Bicep をご紹介します。

Bicep とは

Bicep は Azure リソースの作成に特化した固有言語です。

Bicep と Terraform

Bicep と同様に Azure リソースの作成ができる Terraform という言語があり、こちらは Azure と AWS の両方に対応しています。

実は Terraform にも手を出してみたことがあるのですが、あまり時間もなかったのでよくわかっていないです。
その状態での私の感想は以下のとおりです。

Bicep の利点

  • Azure リソースに特化しているため、新しいサービスや機能への対応が速い(らしい)。
  • プロパティ名が ARM テンプレートの値と同じだったりするため、行き詰まった時はポータルで作った環境の ARM テンプレートを参考にできたりして便利。

Terraform の利点

  • AWS でも使えるので1つ習得しておくだけで 2 つの環境で使える。

本当はもっとたくさんの利点があると思うのですが、ちょっと使ってみたレベルの感想としてはこんな感じです。

Bicep を使う準備

Bicep を動かすために事前にインストールが必要です。

Bicep を作る

今回は ↑ の画像の様な環境を作成しました。

Bicep ファイル(main.bicep)は以下の通り。

// --------------------------- 外から指定してもらう引数 ---------------------------
@minLength(1)
@description('データベースへの接続文字列')
@secure()
param sqlConnectionString string

@minLength(1)
param otherApiUri string
@minLength(1)
@secure()
param apimSubscriptionKey string

@minLength(1)
@maxLength(15)
@description('特定環境用に各リソース名の末尾に付ける名前です。例:"app-{optionName}}"')
param optionName string

param location string = resourceGroup().location
param planName string = 'my-${optionName}'
param planSku string = 'P1v2'
param storageAccountName string = 'hoge'
param storageAccountRGName string = 'hoge'
param siteName string = 'my-api-${optionName}'
param functionName string = 'my-func-${optionName}'

@maxLength(32)
param functionsWebHostId string = 'func${replace(toLower(optionName), '-', '')}'

@description('Storage アカウントを取得')
resource storageaccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = {
  name: storageAccountName
  scope:  resourceGroup(storageAccountRGName)
}

@description('Application Insights を作成(App Service用)')
resource siteLogAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
  name: siteName
  location: location
}
resource siteApplicationInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: siteName
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    Request_Source: 'rest'
    WorkspaceResourceId: siteLogAnalytics.id
  }
}

@description('Application Insights を作成(Functions用)')
resource functionLogAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
  name: functionName
  location: location
}
resource functionApplicationInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: functionName
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    Request_Source: 'rest'
    WorkspaceResourceId: siteLogAnalytics.id
  }
}

@description('App Service プランを作成')
resource appServicePlan 'Microsoft.Web/serverfarms@2020-12-01' = {
  name: planName
  location: location
  sku: {
    name: planSku
    capacity: 1
  }
  kind: 'linux'
  properties: {
    reserved: true
  }
}

var storageConnectionString = 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};AccountKey=${storageaccount.listKeys().keys[0].value}'

@description('App Service を作成')
resource site 'Microsoft.Web/sites@2023-12-01' = {
  name: siteName
  location: location
  kind: 'app,linux'
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      numberOfWorkers: 1
      linuxFxVersion: 'DOTNETCORE|8.0'
      acrUseManagedIdentityCreds: false
      alwaysOn: true
      publishingUsername: 'REDACTED'

      appSettings: [
        {
          name:'APPINSIGHTS_INSTRUMENTATIONKEY'
          value: siteApplicationInsights.properties.InstrumentationKey
        }
        {
          name:'OtherApiUri'
          value: otherApiUri
        }
        {
          name:'ApimSubscriptionKey'
          value: apimSubscriptionKey
        }
      ]
      connectionStrings: [
        {
          name: 'Storage'
          connectionString: storageConnectionString
          type: 'Custom'
        }
        {
          name: 'Sql'
          connectionString: sqlConnectionString
          type: 'SQLAzure'
        }
      ]
    }
    httpsOnly: true
    scmSiteAlsoStopped: false
    clientAffinityEnabled: false
  }
}
@description('Functions のアプリ設定の一部をスロット固定にする')
resource siteConfig 'Microsoft.Web/sites/config@2021-03-01' = {
  name: 'slotConfigNames'
  parent: site
  properties: {
    appSettingNames: [
      'APPINSIGHTS_INSTRUMENTATIONKEY'
    ]
  }
}

@description('Functions を作成')
resource functions 'Microsoft.Web/sites@2023-12-01' = {
  name: functionName
  location: location
  kind: 'functionapp,linux'
  properties: {
    enabled: true
    reserved: true
    serverFarmId: appServicePlan.id
    siteConfig: {
      linuxFxVersion : 'DOTNET-ISOLATED|8.0'
      alwaysOn: true
      appSettings: [
        {
          name: 'AzureWebJobsDashboard'
          value: storageConnectionString
        }
        {
          name: 'AzureWebJobsStorage'
          value: storageConnectionString
        }
        {
          name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
          value: storageConnectionString
        }
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: '~4'
        }
        {
          name: 'FUNCTIONS_WORKER_RUNTIME'
          value: 'dotnet-isolated'
        }
        {
          name: 'WEBSITE_CONTENTSHARE'
          value: toLower(functionName)
        }
        {
          name:'APPINSIGHTS_INSTRUMENTATIONKEY'
          value: functionApplicationInsights.properties.InstrumentationKey
        }
        {
          name: 'AzureFunctionsWebHost__hostid'
          value: functionsWebHostId          
        }
        {
          name:'OtherApiUri'
          value: otherApiUri
        }
        {
          name:'ApimSubscriptionKey'
          value: apimSubscriptionKey
        }
      ]
      connectionStrings: [
        {
          name: 'Storage'
          connectionString: storageConnectionString
          type: 'Custom'
        }
        {
          name: 'Sql'
          connectionString: sqlConnectionString
          type: 'SQLAzure'
        }
      ]
    }
    httpsOnly: true
  }
}
@description('Functions のアプリ設定の一部をスロット固定にする')
resource functionConig 'Microsoft.Web/sites/config@2021-03-01' = {
  name: 'slotConfigNames'
  parent: functions
  properties: {
    appSettingNames: [
      'APPINSIGHTS_INSTRUMENTATIONKEY'
      'AzureFunctionsWebHost__hostid'
    ]
  }
}

Bicep を呼ぶための PowerShell ファイル(run.ps1)は以下の通り。

### ------------------------------ 作成したい環境によって変えてください。 ------------------------------
# ストレージのリソースグループ
$storageAccountResourceGroupName = ''

# 15文字以内
$optionName = ''

# 外部 API URI
$otherApiUri = ''

# API Management サブスクリプションキー(File API)
$apimSubscriptionKey = ''

# SQL データベースの接続文字列
$sqlConnectionString = ''
### --------------------------------------------------------------------------------------------------

# シェルのパスを実行中の ps1 ファイルの位置に移動する
$path = Split-Path -Parent $MyInvocation.MyCommand.Path
Set-Location $path

$resourceGroupName = 'my-' + $optionName

# Azure へのログイン
az login --tenant '《テナントID》'

# アクティブなサブスクリプションを設定
az account set --subscription '《サブスクリプション名》'

# ストレージのリソースグループがいるか確認
if(-not (az group exists --name $storageAccountResourceGroupName))
{
    exist;
}

# デプロイ用のリソースグループがいるか確認
if(-not (az group exists --name $resourceGroupName))
{
    # いなかったら作成
    az group create --name $resourceGroupName --location 'JapanWest'
}

# Bicep を使用してリソースを作成
az deployment group create --resource-group $resourceGroupName --template-file '.\main.bicep' --parameters `
    optionName=$optionName `
    otherApiUri=$otherApiUri `
    apimSubscriptionKey=$apimSubscriptionKey `
    sqlConnectionString=$sqlConnectionString

run.ps1main.bicep を同じフォルダーに配置し、 run.ps1 を実行するとこでリソースが作成されます。

反省点

SQL 接続

本来は実行時に接続文字列パラメーターとして渡すのではなく、Key Vault に設定した接続文字列をマネージドIDで取得できるようにする予定でした。 しかし Bicep で App Service にユーザー割り当ての ID を設定することができず、断念しました。

API Management のサブスクリプションキー

これは単純に存在を忘れていて、時間がなかったので後付けでパラメーターを渡すようにしました。

モジュール化

1つのファイルにすべてのリソースを書いている状態ですが、リソースごとにモジュール化したいと考えています。こちらは今後対応していきたいと思っています。

まとめ

今回は今まで Azure リソースの作成をポータルで行っていたのを Bicep 管理に移行する第1歩としてやってみました。
まだまだ改善点はある状態ですが、後は部内で広げてブラッシュアップしていけたらと思っています。
Azure リソースを管理しているみなさんも是非挑戦してみてください。

KENTEMでは、様々な拠点でエンジニアを大募集しています!
建設×ITにご興味頂いた方は、是非下記のリンクからご応募ください。
recruit.kentem.jp career.kentem.jp