Azure Functions デプロイメントテンプレートを Terraform から Bicep に移行して学んでいること (1)

Azure Functions の様々な種類のトリガーを持つアプリとイベントソースのデプロイメントを以前は Terraform で書いていたが、故あって Bicep に移行する必要性が生じた。Bicep は少ししか触ったことが無いためほとんどど素人なので学んでいることを記録して記憶に残したい。私はまだ初心者なので、もし間違っていたらコメントをいただけるとありがたいです。

# Bicep と Terraform の使用感

## Terraform の良さ

　まず使ってみた感想だが、長年 Terraform 気に入って使っているが、Bicep に移行しても思ったより悪くない。仕組み的に考えて Bicep は ARM Template を単にプリコンパイルするシンタックスシュガーと思っていたのだが、単にそのことが十分良い効果をもたらしているようだ。

　私の理解が浅いかもしれないが自分が Terraform を好んでいたのは次のためだ

 * シンプルで分かりやすいシンタックス
 * モジュール化のしやすさ
 * ローカル実行
 * 関数
 * 豊富な機能（テンプレートなど）
 * 3rd パーティのツール [terratest](https://github.com/gruntwork-io/terratest)など

## Bicepで出来ること

Azure の REST API で使われるのは、ARM Template のリソースのフォーマットで、純粋な Json で正直書くのめんどくさい。だから、Terraform の方がずっと好みだった。Bicep は、ARM Template が上記っでいう「シンプルで分かりやすいシンタックス」になっただけならば、あまり興味がないと思っていた。実際 Bicep を触ってみると

* シンプルで分かりやすいシンタックス
* モジュール化のしやすさ
* ローカル実行は無理っぽいが、[デプロイメントスクリプト](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/deployment-script-bicep?tabs=PowerShell)を実行できる
* 関数
* 筋肉に関する名前（上腕二頭筋）筆者は筋トレ好き

さすがに豊富な機能は手元のスクリプトで作るしかないのだが、結構上記の問題だけでも解決できたのは非常に大きい。Terraform が好きとは言え Azure で使う時に Terraform に全く弱点がないわけではない。

## Terraform の弱点

他のプラットフォームで使う場合は良く知らないが、ARM/Bicep と比べた時に Terraform に弱点がないわけではない

* 最新APIがサポートされるまでのタイムラグがある 注：未サポートAPIバージョンは[azapi](https://registry.terraform.io/providers/Azure/azapi/latest/docs)で解決する（最高）
* アーキテクチャの違いによる、VNET/AccessControl設定等のデプロイの制約
* ステートの管理が若干弱く感じる（私のやり方がまずいだけかも）

Azureの開発チームは、新しい機能を実装すると、REST API で公開されることが多いが、そのAPIが固有のバージョンを持っていて、随時アップデートされる。アップデートされても、古いバージョンを指定できるのでブレーキングチェンジにはならないが、場合によっては新しいバージョンのAPIを使って新しく実装された機能を使いたいケースもあるだろう。そういった時も単なるシンタックスシュガーの、Bicep は有利だ。ただ、Terraform も上記でも書いている[azapi](https://registry.terraform.io/providers/Azure/azapi/latest/docs)があり、ARMテンプレートのリソースを Terraform からデプロイできるようになっているので、ほぼ同じと言えよう。

ただ、どちらかというと ２点目が少しつらくなることが多い。下記の図は、Bicep でデプロイする場合と、Terraform でデプロイする場合のイメージの違いを示している。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/e7082f88-b502-deb4-c138-81dcd453e3b4.png)

どういうことかというと、Bicep の場合は、すべてのリソースを一度に Azure REST API に送って、そちらの方で、ビルド、リソースの作成が行われる。

一方 Terraform の場合は、複数の REST Call を Azure REST API に対して可能であれば、依存関係を見てリソースごとに、並列実行で API 呼び出しを行い、Azure REST API の方で、それぞれのリソースの作成が終了したら、次に可能なリソースのAPI呼び出しを行っているというイメージ（だと思う。コントリビュートした過去の経験から）

　この設計の違いは、1st party と 3rd party の違いが出ていると思うが、Terraform が少し苦手な場面はアクセスコントロールとか、VNET の中のリソース等になる。アクセス制限の例が分かりやすいかもしれないが、あるリソース（例えば StorageAccount) に対して、アクセスを制限して、そのリソースに対してアクセスしたい（例えばQueue を作りたい）といったような場合、アクセス制限がされているので、クライアントがアクセスできない。回避策としては、自分のクライアントをホワイトリストに入れておけばいいが、会社のプロキシで制限されている場合は、会社のプロキシの outbound の IP Addresses をホワイトリストに入れることになる。
 　CIを使ってデプロイする場合は、CI で使われる Worker の IPAddress をホワイトリストに入れる、もしくは他に許可する方法を考えて実行すればよい。

  Bicep の場合は、１回でデプロイ・設定したい情報が１回で送付されるため、そういった制限には引っかからないことになる。

  **Note:** @shybayan さんがXでコメントをくれました。このふるまいに関しては、StorageAccountが ResourceManager でないAPIを使っているせいなので、azapiを使ってあげれば問題は解決しそうです！
  
  <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">詳しく説明すると Storage SDK を使って Container / Queue などを作成する場合は Storage の Data plane API を使うのに対し、ARM Template / Bicep の場合は Management API (Resource Manager) を使うといった具合です <a href="https://t.co/QZBiLBSIXg">https://t.co/QZBiLBSIXg</a></p>&mdash; Tatsuro Shibamura (@shibayan) <a href="https://twitter.com/shibayan/status/1784402389975658800?ref_src=twsrc%5Etfw">April 28, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# ローカル実行の代替え方法

Terraformであった機能で最高に便利だったのが、[Local Exec](https://developer.hashicorp.com/terraform/language/resources/provisioners/local-exec)という機能で、手元のスクリプトを実行できる機能だ。これは何が便利なのか？というと、Azure Functions でいうと、Azure REST API ではないものを取得したいというケースがある。例えば、Azure Functions Host のキーを取得したい場合などは、Azure REST API ではない。他にも、Event Grid Trigger とかだと、[Event Grid のリソースを設定するためには、一旦 Function App をデプロイして、アプリをデプロイした後に、キーを取得する必要があり](https://learn.microsoft.com/en-us/azure/azure-functions/functions-event-grid-blob-trigger?tabs=isolated-process%2Cnodejs-v4&pivots=programming-language-csharp)、そのキーのAPI は Azure REST API ではない。

 こういった時でも、local exec があれば対応できた。これ最高！知らなかったのだが、bicep にも[deployment script](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/deployment-script-bicep?tabs=PowerShell)というものがあり、似たようなことが出来る。説明を読んでいると、どうやらローカルで動いているわけではなく、デプロイの時に ACI (Azure Container Instances) を使って、コンテナをAPI側で起動して、クレデンシャルを渡して、スクリプトをそこで実行しているのであろう。

 # Bicep の弱み

 使ってみて思ったのは、さすがに Terraform 程便利なツール（テンプレート等）がそろっていない事、モジュール化は Terraform の方が強力っぽい。例えば、Bicep だと、おそらくこれはコンパイルの時にモジュールがインライン展開されることに関係していると思われる。将来最適化が進むといいなと思うことで、例えば、Storage Account 
  を作って、そのリソースのオブジェクトをそのまま渡したいと思っても渡せない。（いや、渡せるのだが、キャストが出来ないので、結局リソースとして使えない）だから、名前を渡して、あとで、使うところで名前からリソースを読み込む必要がある。

ちなみに、Bicep の場合、Azure CLI もしくは、Bicep のクライアントがあれば、スクリプトのビルド結果を見ることが出来る。私が試したところ、実際のデプロイで使ったら動くのに、ローカルのビルドは動かないこともあった。これは、多分パラメータを渡すことで最適化がおそらく行われているのだと思う。

次のようなイメージで実行できる。

* [bicep-cli](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-cli)

これは、単純な Storage Account をモジュールとして定義し、`deployment script` を組み込んだ例だ。コンパイル後の、ファイルを見るとどのように展開されるのかが見れて興味深い。

```PowerShell:Command
az bicep build --file .\main.bicep --outdir .
```

```main.bicep:Bicep
param storage_account_name string
param location string

module storageAccount 'storage.bicep' = { 
  name: storage_account_name
  params: {
    location: location
    storage_account_name: storage_account_name
  }
}

resource deploymentScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = {
  name: 'inlinePS'
  location: location
  kind: 'AzurePowerShell'
  properties: {
    azPowerShellVersion: '10.0'
    scriptContent: '''
    echo "hello world"
    '''
    retentionInterval: 'PT1H'
  }
}

```

```storage.bicep:Bicep
param storage_account_name string
param location string

resource storage_account 'Microsoft.Storage/storageAccounts@2022-09-01' = {
  name: storage_account_name
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  properties: {
    accessTier: 'Hot'
    allowBlobPublicAccess: true
    allowCrossTenantReplication: true
    allowSharedKeyAccess: true
    defaultToOAuthAuthentication: false
    dnsEndpointType: 'Standard'
    minimumTlsVersion: 'TLS1_2'
    networkAcls: {
      bypass: 'AzureServices'
      defaultAction: 'Allow'
    }
    publicNetworkAccess: 'Enabled'
  }
}
```

展開されたものを見ると、モジュール化された`Storage Account` のリソースは、`Deployment`の一部としてデプロイされている。また、`Deployment Script` はローカルではなく、サーバー側で準備されて実行されているのだということが分かる。

```main.json:json
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "metadata": {
    "_generator": {
      "name": "bicep",
      "version": "0.10.61.36676",
      "templateHash": "8584858534606995420"
    }
  },
  "parameters": {
    "storage_account_name": {
      "type": "string"
    },
    "location": {
      "type": "string"
    }
  },
  "resources": [
    {
      "type": "Microsoft.Resources/deploymentScripts",
      "apiVersion": "2023-08-01",
      "name": "inlinePS",
      "location": "[parameters('location')]",
      "kind": "AzurePowerShell",
      "properties": {
        "azPowerShellVersion": "10.0",
        "scriptContent": "    echo \"hello world\"\r\n    ",
        "retentionInterval": "PT1H"
      }
    },
    {
      "type": "Microsoft.Resources/deployments",
      "apiVersion": "2020-10-01",
      "name": "[parameters('storage_account_name')]",
      "properties": {
        "expressionEvaluationOptions": {
          "scope": "inner"
        },
        "mode": "Incremental",
        "parameters": {
          "location": {
            "value": "[parameters('location')]"
          },
          "storage_account_name": {
            "value": "[parameters('storage_account_name')]"
          }
        },
        "template": {
          "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
          "contentVersion": "1.0.0.0",
          "metadata": {
            "_generator": {
              "name": "bicep",
              "version": "0.10.61.36676",
              "templateHash": "10986875920963787883"
            }
          },
          "parameters": {
            "storage_account_name": {
              "type": "string"
            },
            "location": {
              "type": "string"
            }
          },
          "resources": [
            {
              "type": "Microsoft.Storage/storageAccounts",
              "apiVersion": "2022-09-01",
              "name": "[parameters('storage_account_name')]",
              "location": "[parameters('location')]",
              "sku": {
                "name": "Standard_LRS"
              },
              "kind": "StorageV2",
              "properties": {
                "accessTier": "Hot",
                "allowBlobPublicAccess": true,
                "allowCrossTenantReplication": true,
                "allowSharedKeyAccess": true,
                "defaultToOAuthAuthentication": false,
                "dnsEndpointType": "Standard",
                "minimumTlsVersion": "TLS1_2",
                "networkAcls": {
                  "bypass": "AzureServices",
                  "defaultAction": "Allow"
                },
                "publicNetworkAccess": "Enabled"
              }
            }
          ]
        }
      }
    }
  ]
}
```

コマンド実行に

# Bicep のでバッギング

IaC の一番の悩みはおそらくデバッグだろう。どんな値がセットされているかわかりにくいので、実際にデプロイをしてスクリプトが正しいか確かめるしかない。terraform だと terratest とかあるけど、Bicepでデバッグをするためにはいくつかの方法がある。

## 詳細なログを出力する

PowerShell の場合、[Enable Debug logging](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/enable-debug-logging?tabs=azure-powershell)のページに書かれているオプション `-DeploymentDebugLogLevel All`を使う。同じページに Azure CLI の場合も記載されている。

```Enable Debug:powershell
New-AzResourceGroupDeployment -Name exampledeployment -ResourceGroupName examplegroup -TemplateFile main.bicep -DeploymentDebugLogLevel All
```

詳細なログは、Portal でも観れる。デプロイに使用した Resource Group に行き、`Activity log` をクリックすると、詳細のデプロイのログが出てくるのでここで、原因調査も可能である。

## VSCode Extension

今 Bicep を書くのは VSCode が良さそうだ。[extension がサポート](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/quickstart-create-bicep-use-visual-studio-code?tabs=CLI)されている。インテリセンスに Lint そして、バリデーションがある。コードナビゲーションやリファクタリングもサポートされている。Rename の `F2`や、フォーマットの `alt+shift+f` は覚えておきたい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/8ec5b497-1304-7837-b895-f21d8a532273.png)

Visualizer もある。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/59d01a3a-d431-b957-cc48-93e2bb5d56a7.png)


## Linter 

標準で Linter が存在する。VSCode の Extension で Lint はサポートされているが、CIでデプロイメントの確認とまでいかなくてもLintを実行したいだろう。下記のコマンドがサポートされている。

```lint:PowerShell
az bicep upgrade
az bicep lint --file main.bicep
```

# 感想

Bicep 思ったよりずっと良いという印象。最初にオーバービューと感想だけ書いて結構時間かかったので、次は、実際にテンプレート作ってつまずいて、解決した Tips などを紹介したい。