Azure Functions の様々な種類のトリガーを持つアプリとイベントソースのデプロイメントを以前は Terraform で書いていたが、故あって Bicep に移行する必要性が生じた。Bicep は少ししか触ったことが無いためほとんどど素人なので学んでいることを記録して記憶に残したい。私はまだ初心者なので、もし間違っていたらコメントをいただけるとありがたいです。
Bicep と Terraform の使用感
Terraform の良さ
まず使ってみた感想だが、長年 Terraform 気に入って使っているが、Bicep に移行しても思ったより悪くない。仕組み的に考えて Bicep は ARM Template を単にプリコンパイルするシンタックスシュガーと思っていたのだが、単にそのことが十分良い効果をもたらしているようだ。
私の理解が浅いかもしれないが自分が Terraform を好んでいたのは次のためだ
- シンプルで分かりやすいシンタックス
- モジュール化のしやすさ
- ローカル実行
- 関数
- 豊富な機能(テンプレートなど)
- 3rd パーティのツール terratestなど
Bicepで出来ること
Azure の REST API で使われるのは、ARM Template のリソースのフォーマットで、純粋な Json で正直書くのめんどくさい。だから、Terraform の方がずっと好みだった。Bicep は、ARM Template が上記っでいう「シンプルで分かりやすいシンタックス」になっただけならば、あまり興味がないと思っていた。実際 Bicep を触ってみると
- シンプルで分かりやすいシンタックス
- モジュール化のしやすさ
- ローカル実行は無理っぽいが、デプロイメントスクリプトを実行できる
- 関数
- 筋肉に関する名前(上腕二頭筋)筆者は筋トレ好き
さすがに豊富な機能は手元のスクリプトで作るしかないのだが、結構上記の問題だけでも解決できたのは非常に大きい。Terraform が好きとは言え Azure で使う時に Terraform に全く弱点がないわけではない。
Terraform の弱点
他のプラットフォームで使う場合は良く知らないが、ARM/Bicep と比べた時に Terraform に弱点がないわけではない
- 最新APIがサポートされるまでのタイムラグがある 注:未サポートAPIバージョンはazapiで解決する(最高)
- アーキテクチャの違いによる、VNET/AccessControl設定等のデプロイの制約
- ステートの管理が若干弱く感じる(私のやり方がまずいだけかも)
Azureの開発チームは、新しい機能を実装すると、REST API で公開されることが多いが、そのAPIが固有のバージョンを持っていて、随時アップデートされる。アップデートされても、古いバージョンを指定できるのでブレーキングチェンジにはならないが、場合によっては新しいバージョンのAPIを使って新しく実装された機能を使いたいケースもあるだろう。そういった時も単なるシンタックスシュガーの、Bicep は有利だ。ただ、Terraform も上記でも書いているazapiがあり、ARMテンプレートのリソースを Terraform からデプロイできるようになっているので、ほぼ同じと言えよう。
ただ、どちらかというと 2点目が少しつらくなることが多い。下記の図は、Bicep でデプロイする場合と、Terraform でデプロイする場合のイメージの違いを示している。
どういうことかというと、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 の場合は、1回でデプロイ・設定したい情報が1回で送付されるため、そういった制限には引っかからないことになる。
Note: @shybayan さんがXでコメントをくれました。このふるまいに関しては、StorageAccountが ResourceManager でないAPIを使っているせいなので、azapiを使ってあげれば問題は解決しそうです!
詳しく説明すると Storage SDK を使って Container / Queue などを作成する場合は Storage の Data plane API を使うのに対し、ARM Template / Bicep の場合は Management API (Resource Manager) を使うといった具合です https://t.co/QZBiLBSIXg
— Tatsuro Shibamura (@shibayan) April 28, 2024
ローカル実行の代替え方法
Terraformであった機能で最高に便利だったのが、Local Execという機能で、手元のスクリプトを実行できる機能だ。これは何が便利なのか?というと、Azure Functions でいうと、Azure REST API ではないものを取得したいというケースがある。例えば、Azure Functions Host のキーを取得したい場合などは、Azure REST API ではない。他にも、Event Grid Trigger とかだと、Event Grid のリソースを設定するためには、一旦 Function App をデプロイして、アプリをデプロイした後に、キーを取得する必要があり、そのキーのAPI は Azure REST API ではない。
こういった時でも、local exec があれば対応できた。これ最高!知らなかったのだが、bicep にもdeployment scriptというものがあり、似たようなことが出来る。説明を読んでいると、どうやらローカルで動いているわけではなく、デプロイの時に ACI (Azure Container Instances) を使って、コンテナをAPI側で起動して、クレデンシャルを渡して、スクリプトをそこで実行しているのであろう。
Bicep の弱み
使ってみて思ったのは、さすがに Terraform 程便利なツール(テンプレート等)がそろっていない事、モジュール化は Terraform の方が強力っぽい。例えば、Bicep だと、おそらくこれはコンパイルの時にモジュールがインライン展開されることに関係していると思われる。将来最適化が進むといいなと思うことで、例えば、Storage Account
を作って、そのリソースのオブジェクトをそのまま渡したいと思っても渡せない。(いや、渡せるのだが、キャストが出来ないので、結局リソースとして使えない)だから、名前を渡して、あとで、使うところで名前からリソースを読み込む必要がある。
ちなみに、Bicep の場合、Azure CLI もしくは、Bicep のクライアントがあれば、スクリプトのビルド結果を見ることが出来る。私が試したところ、実際のデプロイで使ったら動くのに、ローカルのビルドは動かないこともあった。これは、多分パラメータを渡すことで最適化がおそらく行われているのだと思う。
次のようなイメージで実行できる。
これは、単純な Storage Account をモジュールとして定義し、deployment script
を組み込んだ例だ。コンパイル後の、ファイルを見るとどのように展開されるのかが見れて興味深い。
az bicep build --file .\main.bicep --outdir .
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'
}
}
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
はローカルではなく、サーバー側で準備されて実行されているのだということが分かる。
{
"$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のページに書かれているオプション -DeploymentDebugLogLevel All
を使う。同じページに Azure CLI の場合も記載されている。
New-AzResourceGroupDeployment -Name exampledeployment -ResourceGroupName examplegroup -TemplateFile main.bicep -DeploymentDebugLogLevel All
詳細なログは、Portal でも観れる。デプロイに使用した Resource Group に行き、Activity log
をクリックすると、詳細のデプロイのログが出てくるのでここで、原因調査も可能である。
VSCode Extension
今 Bicep を書くのは VSCode が良さそうだ。extension がサポートされている。インテリセンスに Lint そして、バリデーションがある。コードナビゲーションやリファクタリングもサポートされている。Rename の F2
や、フォーマットの alt+shift+f
は覚えておきたい。
Visualizer もある。
Linter
標準で Linter が存在する。VSCode の Extension で Lint はサポートされているが、CIでデプロイメントの確認とまでいかなくてもLintを実行したいだろう。下記のコマンドがサポートされている。
az bicep upgrade
az bicep lint --file main.bicep
感想
Bicep 思ったよりずっと良いという印象。最初にオーバービューと感想だけ書いて結構時間かかったので、次は、実際にテンプレート作ってつまずいて、解決した Tips などを紹介したい。