Bicep デプロイ時に Azure Key Vault を使用して、セキュリティで保護されたパラメーター値を渡したメモ

## 概要

[BicepでFunctionsを作成](https://qiita.com/hibohiboo/items/6a49ed68e6560594278b)した。
Functionsでデータベースの接続文字列を環境変数に格納したが、secureにデプロイするためKey Vaultを使いたい。

[ソースコード](https://github.com/hibohiboo/async-ttrpg/blob/f1f95ad45bdbe1a8d1768afd53634552425956aa/infra/bin/addKeyVault.sh)

## KeyVaultのシークレット作成

```bash:infra/bin/.env
# ;が含まれているので "" で囲む
DATABASE_URL="sqlserver://hoge.database.windows.net:1433;database=fuga;user=piyo;password=piyopiyo;encrypt=true"
RESOURCE_GROUP_NAME='async-ttrpg-hogehoge'
KEY_VAULT_NAME='AsyncTrpgKeyVault'
KEY_VAULT_SECRET_NAME='AsyncTrpgDatabaseURL'
```


```bash:infra/bin/addKeyVault.sh
#!/bin/bash

# ディレクトリパス取得
BIN_DIR=$(cd $(dirname $0) && pwd)
BICEP_DIR=$(cd $BIN_DIR/../biceps && pwd)

# 環境変数読み込み
source $BIN_DIR/.env
LOCATION=japaneast
keyVaultName=$KEY_VAULT_NAME
keyVaultSecretName=$KEY_VAULT_SECRET_NAME
subscriptions=$(az account show | jq -r '.id')
userObjectId=$(az ad signed-in-user show | jq -r '.id')

if az keyvault list --query "[?contains(name, '$keyVaultName')].name" -o tsv | grep -q "$keyVaultName"; then
  echo "Key Vault '$keyVaultName' が既に存在します。"
  exit 0
fi

az keyvault create --name $keyVaultName --location $LOCATION --enabled-for-template-deployment true --resource-group $RESOURCE_GROUP_NAME

# Key Vaultが作成されるまで待機
echo "Waiting for Key Vault to be available..."
while true; do
  kv=$(az keyvault show --name $keyVaultName --resource-group $RESOURCE_GROUP_NAME --query "properties.provisioningState" -o tsv)
  if [ "$kv" == "Succeeded" ]; then
    echo "Key Vault is available."
    break
  fi
  echo "Key Vault is not yet available. Waiting..."
  sleep 5
done

az role assignment create --role "Key Vault Secrets Officer" --assignee $userObjectId --scope /subscriptions/$subscriptions/resourceGroups/$RESOURCE_GROUP_NAME/providers/Microsoft.KeyVault/vaults/$keyVaultName
az keyvault secret set --vault-name $keyVaultName --name $keyVaultSecretName --value $DATABASE_URL --output none
```

<details><summary>設定不足によるエラー</summary><div>

key vaultの作成後、利用できるまでの間にシークレットを使おうとするとエラーとなる。

```
Resource provider 'Microsoft.KeyVault' used by this operation is not registered. We are registering for you. 
```



```
If role assignments, deny assignments or role definitions were changed recently, please observe propagation time. 
```


```
Cannot set policies to a vault with '--enable-rbac-authorization' specified 
```


</div></details>

## Azure Functions作成

```json:infra/biceps/main.parameters.json
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "functionsRuntime": {
      "value": {
        "runtime": "node",
        "linuxFxVersion": "Node|20",
        "kind": "functionapp,linux",
        "extensionVersion": "~4"
      }
    }
  }
}
```

```diff:main/biceps/main.bicep
param location string = resourceGroup().location
param keyVaultName string
@description('The runtime version of the Azure Functions app.')
param functionsRuntime object

var storageAccountName = '${uniqueString(resourceGroup().id)}azfunctions'

resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
  name: keyVaultName
}

module myFunctionsStorage 'core/storage/storage-account.bicep' = {
  name: 'myFunctionsStorage'
  params: {
    location: location
    storageAccountName: storageAccountName
  }
}

module myFunctions 'core/host/functions.bicep' = {
  name: 'myFunctions'
  params: {
    location: location
+    databaseUrl: keyVault.getSecret('AsyncTrpgDatabaseURL')
    storageAccountName: storageAccountName
    kind: functionsRuntime.kind
    linuxFxVersion: functionsRuntime.linuxFxVersion
    extensionVersion: functionsRuntime.extensionVersion
  }
}

output appServiceAppHostName string = myFunctions.outputs.appServiceAppHostName
```

```diff:infra/core/host/functions.bicep
param storageAccountName string
param location string
param kind string
param linuxFxVersion string
param extensionVersion string
+ @secure()
param databaseUrl string

var functionAppName = '${uniqueString(resourceGroup().id)}azfunctionsapp'

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' existing = {
  name: storageAccountName
}

resource functionApp 'Microsoft.Web/sites@2022-09-01' = {
  name: functionAppName
  location: location
  kind: kind
  properties: {
    reserved: true
    siteConfig: {
      linuxFxVersion: linuxFxVersion
      appSettings: [
        {
          name: 'AzureWebJobsStorage'
          value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
        }
        {
          name: 'WEBSITE_CONTENTSHARE'
          value: toLower(functionAppName)
        }
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: extensionVersion
        }
        {
          name: 'FUNCTIONS_WORKER_RUNTIME'
          value: 'node'
        }
        {
          name: 'DATABASE_URL'
          value: databaseUrl
        }
      ]
    }
  }
}

output appServiceAppHostName string = functionApp.properties.defaultHostName
```

## 実行
```bash:infra/bin/createFunction.bash
#!/bin/bash

# ディレクトリパス取得
BIN_DIR=$(cd $(dirname $0) && pwd)
BICEP_DIR=$(cd $BIN_DIR/../biceps && pwd)

# 環境変数読み込み
source $BIN_DIR/.env
LOCATION=japaneast

cd $BICEP_DIR && az deployment group create \
  --name functionsDeployment \
  --template-file main.bicep \
  --parameters main.parameters.json \
  --parameters \
    keyVaultName=$KEY_VAULT_NAME \
  -g $RESOURCE_GROUP_NAME
  ```
  

## 参考
[Bicep デプロイ時に Azure Key Vault を使用して、セキュリティで保護されたパラメーター値を渡す](https://learn.microsoft.com/ja-jp/azure/azure-resource-manager/bicep/key-vault-parameter?tabs=azure-cli)
[演習：パラメーターを使用して再利用可能な Bicep テンプレートを構築する](https://learn.microsoft.com/ja-jp/training/modules/build-reusable-bicep-templates-parameters/)