Azure Bicepの基礎

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

https://learn.microsoft.com/ja-jp/training/paths/fundamentals-bicep/

https://learn.microsoft.com/ja-jp/azure/azure-resource-manager/bicep/best-practices

第二弾中級についてこちら。

https://qiita.com/smr1/items/114de293af6d4029a1c5

第三弾DevOps piplelineからはこちら。

https://qiita.com/smr1/items/fac36c9b34fadb546021

# Azure Resource Managerとは
> Azure Resource Manager は、Azure 内でリソースをデプロイおよび管理するときに使用されるサービス。Resource Manager を使用して、リソースを作成、更新、削除することができます。

# Deployもリソースである
> Azure での "デプロイ" とは、デプロイ操作を表す特別なリソースです。 デプロイは、リソースの種類が Microsoft.Resources/deployments である Azure リソースです。icep デプロイを送信すると、デプロイ リソースが作成または更新されます。

# 文法
## 準備
事前にSubscriptionIDを控えておいて以下を実施。
```
az bicep install && az bicep upgrade
az login
az account set --subscription {subscriptionid}
az account show

// 既定のリソース グループを設定する
az configure --defaults group="{Resource Group名}"
```

## 基礎的なBicep
なんとなく眺めていれば構造が見えてくる。
```
// コマンド実行時に引数として受け付けるにはparam
// =が定義されていれば規定値
param location string = 'japaneast'

// uniquestringを利用して、ResourceGroup単位でユニークな値を生成
// こんな感じになる、toylaunchxunjewfivysf2
param storageAccountName string = 'toylaunch${uniqueString(resourceGroup().id)}'
param appServiceAppName string = 'toylaunch${uniqueString(resourceGroup().id)}'

// 引数に制限を設ける
@allowed([
  'nonprod'
  'prod'
])
param environmentType string

// Bicep内の変数はvar、引数によって三項演算子で条件分岐
var appServicePlanName = 'toy-product-launch-plan'
var storageAccountSkuName = (environmentType == 'prod') ? 'Standard_GRS' : 'Standard_LRS'
var appServicePlanSkuName = (environmentType == 'prod') ? 'P2v3' : 'F1'

// deployするリソースはresourceで定義
resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: storageAccountSkuName
  }
  kind: 'StorageV2'
  properties: {
    accessTier: 'Hot'
    minimumTlsVersion: 'TLS1_2'
  }
}

resource appServicePlan 'Microsoft.Web/serverFarms@2022-03-01' = {
  name: appServicePlanName
  location: location
  sku: {
    name: appServicePlanSkuName
  }
}

resource appServiceApp 'Microsoft.Web/sites@2022-03-01' = {
  name: appServiceAppName
  location: location
  properties: {
    serverFarmId: appServicePlan.id
    httpsOnly: true
  }
}

```

```
// こんな感じで実行
az deployment group create `
   --template-file main2.bicep `
   --parameters environmentType=nonprod

// 制限でダメだったらInvalidTemplateエラーになる
'The provided value for the template parameter 'environmentType' is not valid. The value 'nonprodsfdas' is not part of the allowed value(s): 'nonprod,prod'.'
```

## Module
親のmainテンプレートから部品としてのModuleを呼び出す。Moduleでは、関係のあるリソースの固まりをできるだけ自己完結する形にまとめる。

```
// mainテンプレート
param location string = 'japaneast'
param appServiceAppName string = 'toylaunch${uniqueString(resourceGroup().id)}'

@allowed([
  'nonprod'
  'prod'
])
param environmentType string

// mainテンプレートからModuleの呼び出し
// mainからの相対パスで指定
module appService 'modules/appService.bicep' = {
  name: 'appService'
  params: {
    location: location
    appServiceAppName: appServiceAppName
    environmentType: environmentType
  }
}

// モジュールからの値を取得して出力する
output appServiceAppHostName string = appService.outputs.appServiceAppHostName
```

```
// module側
param location string
param appServiceAppName string

@allowed([
  'nonprod'
  'prod'
])
param environmentType string

var appServicePlanName = 'toy-product-launch-plan'
// mainから受け取った引数で条件分岐
var appServicePlanSkuName = (environmentType == 'prod') ? 'P2v3' : 'F1'

resource appServicePlan 'Microsoft.Web/serverFarms@2022-03-01' = {
  name: appServicePlanName
  location: location
  sku: {
    name: appServicePlanSkuName
  }
}

resource appServiceApp 'Microsoft.Web/sites@2022-03-01' = {
  name: appServiceAppName
  location: location
  properties: {
    serverFarmId: appServicePlan.id
    httpsOnly: true
  }
}

// ModuleからMainテンプレートに必要な値を返却　※複数定義可能
output appServiceAppHostName string = appServiceApp.properties.defaultHostName
```

Azureポータルからのデプロイ履歴ではModuleが分かれて表示される。Module毎にDeployリソースが作成される。同じModuleを再度Deployすると上書きされてしまうので、デプロイ履歴を持ちたい場合はModule名を日付＋時間等で一意にする必要がある。（Pipelineでやる場合、MainとModuleをコピー＋RenameしてそれをDeployするのがいいかも？）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/f7a726ac-288c-0385-6b92-d8775ae7b74e.png)

デプロイ履歴の詳細のInputとOutputからBicep
内で定義した入力（param）と出力（output）が確認できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/f8b14d74-f1f6-1699-bdbe-dd2781cd979a.png)



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/9582280e-7218-81b6-d809-50e568d6a93f.png)

### Moduleの依存関係
以下の様にすればVNetを先に作って、VMで利用するように依存関係を設定できる。自動的に関係が認識され、順番に待機&作成してくれる。逆に言うと、待機するので時間がかかる。
```
@description('Username for the virtual machine.')
param adminUsername string

@description('Password for the virtual machine.')
@minLength(12)
@secure()
param adminPassword string

module virtualNetwork 'modules/vnet.bicep' = {
  name: 'virtual-network'
}

module virtualMachine 'modules/vm.bicep' = {
  name: 'virtual-machine'
  params: {
    adminUsername: adminUsername
    adminPassword: adminPassword
    // vnetのOutputを利用している
    subnetResourceId: virtualNetwork.outputs.subnetResourceId
  }
}
```

### Module化のTips
https://learn.microsoft.com/ja-jp/training/modules/create-composable-bicep-files-using-modules/2-create-use-bicep-modules?tabs=grouping

- Bicep Visualizerを使ってModule化の単位の目安とするとよいかも
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/75fb42a4-0e7d-2dc6-4655-4dd23f74d7f3.png)
　
- モジュールは入れ子にすることも可能だが、階層は浅めにとどめる
　
- 原則としてはモジュールは再利用されるものなので、モジュールにビジネスルールの既定値（StorageAccountのSku等）を持たせるのは推奨されない、Main側で持つ
　
- 複雑なDeployの場合は大きなMainと多数のModuleより、複数の小さ目のMainとそれに紐づく少数のModuleの方がTest、Debugしやすい
　
 - Moduleを作ると、Transpile時には1つのARMTemplateに結合される

## パラメータ
- string: 任意のテキストを入力できます
- int: 数値を入力できます
- bool: ブール値 (true または false) 値を表します
- object と array: 構造化データとリストを表します

#### object
```
// objectで関連する値をまとめる
// objectのプロパティは型定義がない
param appServicePlanSku object = {
  name: 'F1'
  tier: 'Free'
  capacity: 1
}

// 利用
sku: {
  name: appServicePlanSku.name
  tier: appServicePlanSku.tier
  capacity: appServicePlanSku.capacity
}
```

#### array
```
param cosmosDBAccountLocations array = [
  {
    locationName: 'australiaeast'
  }
  {
    locationName: 'southcentralus'
  }
  {
    locationName: 'westeurope'
  }
]
```

#### パラメータファイル
パラメータは直打ちではなくてあらかじめファイルとして準備しておくことができる。環境毎に作成するのが推奨。

Microsoftの例の命名規則としては、{main等の利用ファイル名}.{dev等の環境名}.parameters.jsonとなる。例：main.parameters.dev.json。
```
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "appServicePlanInstanceCount": {
      "value": 3
    },
    "appServicePlanSku": {
      "value": {
        "name": "P1v3",
        "tier": "PremiumV3"
      }
    },
    "cosmosDBAccountLocations": {
      "value": [
        {
          "locationName": "australiaeast"
        },
        {
          "locationName": "southcentralus"
        },
        {
          "locationName": "westeurope"
        }
      ]
    }
  }
}
```

デプロイ時にはこんな感じでパラメータファイルを利用するように指定する。以下の様に一部ファイルの値をコマンドでデプロイ時にオーバーライドすることも可能。
```
// appServicePlanInstanceCountをオーバーライド
az deployment group create `
  --template-file main.bicep `
  --parameters main.parameters.json `
               appServicePlanInstanceCount=5
```

## デコレータ
descriptionは最低限推奨。Intellisenseやエラーメッセージ等いろんなところで表示される。
```
// パラメータの条件等を指定可能
@allowed([
  'dev'
  'test'
  'prod'
])
@minLength(5)
@maxLength(24)
@minValue(1)
@maxValue(10)
@description('desription for the parameter.')
param someParameter string
```

## セキュアなパラメータ
基本的にはManaged Identityを利用して資格情報をファイルから追い出すのが推奨。

### secureデコレータ
secureを使うとログに出力されないようになる。
```
// secureの場合は以下は非推奨
//- 既定値を定義してべた書き
//- outputで出力
//- パラメータファイルに定義してGitにチェックイン
@secure()
param sqlServerAdministratorLogin string
```
### KeyVault連携
https://learn.microsoft.com/ja-jp/azure/azure-resource-manager/templates/key-vault-parameter?tabs=azure-cli

KeyVaultで事前に以下の設定が必要。
```
az keyvault update  --name ExampleVault --enabled-for-template-deployment true
```
また、ユーザーはMicrosoft.KeyVault/vaults/deploy/actionの権限が必要。

#### パラメータファイル内
Bicepの中からKeyVaultを参照する。
```
// パラメータファイルの中で普通のvalueの代わりにreferenceを利用
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "sqlServerAdministratorLogin": {
      "reference": {
        "keyVault": {
          "id": "/subscriptions/f0750bbe-ea75-4ae5-b24d-a92ca601da2c/resourceGroups/PlatformResources/providers/Microsoft.KeyVault/vaults/toysecrets"
        },
        "secretName": "sqlAdminLogin"
      }
    }
  }
}
```

#### モジュールにシークレットを渡す
mainからmoduleにシークレットを渡すには、main内でexistingを利用して既存のKeyVaultからデータを取得する。
```
// 新規作成ではなく既存資源の参照
resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
  name: keyVaultName
}

module applicationModule 'application.bicep' = {
  name: 'application-module'
  params: {
    apiKey: keyVault.getSecret('ApiKey')
  }
}
```
モジュール側ではsecretデコレータ付きのparamで受け取る。

この状態でデプロイしてみると、入力を求められ、入力時に表示されないようになる。
```
Please provide securestring value for 'sqlServerAdministratorLogin' (? for help): 
Please provide securestring value for 'sqlServerAdministratorPassword' (? for help):
```

## 条件付きDeploy：if文と三項演算子
条件に応じたResourceのデプロイ有無判断。if文をModuleに紐づけてしまうのもよい。
```
@allowed([
  'Development'
  'Production'
])
param environmentName string

var auditingEnabled = environmentName == 'Production'

resource auditingSettings 'Microsoft.Sql/servers/auditingSettings@2021-11-01-preview' = if (auditingEnabled) {
  parent: server
  name: 'default'
  properties: {
    state: 'Enabled'
    storageEndpoint: environmentName == 'Production' ? auditStorageAccount.properties.primaryEndpoints.blob : ''
    storageAccountAccessKey: environmentName == 'Production' ? listKeys(auditStorageAccount.id, auditStorageAccount.apiVersion).keys[0].value : ''
  }
}
```

## Loop
### ループの基本
- foreach
```
param storageAccountNames array = [
  'saauditus'
  'saauditeurope'
  'saauditapac'
]

resource storageAccountResources 'Microsoft.Storage/storageAccounts@2021-09-01' = [for storageAccountName in storageAccountNames: {
  name: storageAccountName
  location: resourceGroup().location
  kind: 'StorageV2'
  sku: {
    name: 'Standard_LRS'
  }
}]
```
- for i in next
```
param subnetNames array = [
  'api'
  'worker'
]

resource virtualNetwork 'Microsoft.Network/virtualNetworks@2021-08-01' = {
  name: 'teddybear'
  location: resourceGroup().location
  properties: {
    addressSpace: {
      addressPrefixes: [
        '10.0.0.0/16'
      ]
    }
    subnets: [for (subnetName, i) in subnetNames: {
      name: subnetName
      properties: {
        addressPrefix: '10.0.${i}.0/24'
      }
    }]
  }
}
```
- object配列をfor
```
param subnets array = [
  {
    name: 'frontend'
    ipAddressRange: '10.10.0.0/24'
  }
  {
    name: 'backend'
    ipAddressRange: '10.10.1.0/24'
  }
]

var subnetsProperty = [for subnet in subnets: {
  name: subnet.name
  properties: {
    addressPrefix: subnet.ipAddressRange
  }
}]

resource virtualNetwork 'Microsoft.Network/virtualNetworks@2021-08-01' = {
  name: 'teddybear'
  location: resourceGroup().location
  properties:{
    addressSpace:{
      addressPrefixes:[
        addressPrefix
      ]
    }
    subnets: subnetsProperty
  }
}
```

- 条件付き for（イメージ的にはwhere）
```
param sqlServerDetails array = [
  {
    name: 'sqlserver-we'
    location: 'westeurope'
    environmentName: 'Production'
  }
  {
    name: 'sqlserver-eus2'
    location: 'eastus2'
    environmentName: 'Development'
  }
  {
    name: 'sqlserver-eas'
    location: 'eastasia'
    environmentName: 'Production'
  }
]

@secure()
@description('The administrator login username for the SQL server.')
param sqlServerAdministratorLogin string

@secure()
@description('The administrator login password for the SQL server.')
param sqlServerAdministratorLoginPassword string

resource sqlServers 'Microsoft.Sql/servers@2021-11-01-preview' = [for sqlServer in sqlServerDetails: if (sqlServer.environmentName == 'Production') {
  name: sqlServer.name
  location: sqlServer.location
  properties: {
    administratorLogin: administratorLogin
    administratorLoginPassword: administratorLoginPassword
  }
  tags: {
    environment: sqlServer.environmentName
  }
}]
```

### batchSizeデコレータでn並列でループ
通常は並列でDeployされるが、batchSizeのnの値によって、何並列で実行するかを決定可能。1にすれば1,2,3...という風に順次デプロイされる。
```
@batchSize(n)
resource appServiceApp 'Microsoft.Web/sites@2021-03-01' = [for i in range(1,3): {
  name: 'app${i}'
  // ...
}]
```

### for loopで回して複数のoutputがあるとき
```
// main.bicepからfor loopでModuleを複数回呼び出す
module databases 'modules/database.bicep' = [for location in locations: {
  name: 'database-${location}'
  params: {
    location: location
    sqlServerAdministratorLogin: sqlServerAdministratorLogin
    sqlServerAdministratorLoginPassword: sqlServerAdministratorLoginPassword
  }
}]
...省略...
// moduleの出力を配列で受け取ってoutputする
output serverInfo array = [for i in range(0, length(locations)): {
  name: databases[i].outputs.serverName
  location: databases[i].outputs.location
  fullyQualifiedDomainName: databases[i].outputs.serverFullyQualifiedDomainName
}]
```

```
// module側、database.bicep
...省略...
// outputが複数個、これが複数回呼ばれてmainに返却される
output serverName string = sqlServer.name
output location string = location
output serverFullyQualifiedDomainName string = sqlServer.properties.fullyQualifiedDomainName
```

# 静的解析ツール
bicepを静的解析ツールで作成段階からセキュリティ等をチェックできればShift Leftにもつながる。VSCodeのBicep拡張やSonarでもある程度できる。

- PSRule for Azure
これがどれくらいメジャー、キャッチアップできているのか正直不明なのだが、Bicepに対してMicrosoftのWell Architected Frameworkに沿って自動テストをしてくれる。ちゃんと運用できればこれはすごい。

https://zenn.dev/openjny/articles/dbf98272d2317b

https://aadojo.alterbooth.com/entry/2023/12/17/000000

Deployした後ならCloud Posture ManagementでAzure Defender for CloudのAzure Secure Scoreとかもあるけど。

https://learn.microsoft.com/ja-jp/azure/defender-for-cloud/secure-score-security-controls