前回は、Bicepの性質から、どのような基本設計でIaCを指向するか書きました。
今回は、実際にBicep Resourceを使って書くときに、どのような工夫が必要なのかメモしておきます。
- tl;dr;
- Bicep Resource
- Preview リソースと ARM Template
- Bicep Module 粒度
- Bicep Parameter
- 実行時Parameter の渡し方
- existing と リソースの存在保障
- Role Name の取得
- 来てほしい機能
tl;dr;
- PreviewリソースはARM Templateを見つけるところから気を付けよう
- Bicepモジュール粒度はTerraformのモジュール粒度と同じコンセプトでよく機能する
- paramでobjectを使うときにはデフォルト値とaray of objectが使いにくい
- RoleのようなGUIDがnameのリソースでは、逆引きできるように設計が必要
Bicep Resource
IaCで一番重要なのが、Resource Referenceはどこを探せばいいのかの確認です。
Bicep ResourceはARM Temaplteと相互に変換ができます。 ということで、MirosoftはARM TemplateのReferenceにBicepの定義も配置しています。
Azure Resource Manager template reference - ARM template reference | Microsoft Docs
型定義は、次の通り。改行に意味がある構文なので、慣れてない内は、ふとした変数定義でエラーになります。
Bicep functions - objects - Azure Resource Manager | Microsoft Docs
Preview リソースと ARM Template
Previewは、Azureと付き合っていくうえでめんどくさい側面の1つです。
AzureはPreviewじゃないと使いたい機能がない、というケースが多い。(それ自体はいいが、プレビューが長いのはAzureを使っていてつらいところ) ということでPreviewも扱えないか考えましょう。
AzureのARM Templateページには「Previewを除くAzureリソース」は記載されているが、Previewリソースはここにありません。 Previewリソースは、それぞれのPreviewリソースの説明ページに存在します。
例えば、PostgreSQL Flexible ServerはPreviewなので、こっちを見ることになります。
クイック スタート:Azure DB for PostgresSQL フレキシブル サーバーを作成する - ARM テンプレート | Microsoft Docs
Previewページは手薄
Previewは、AWS・Azure問わずAPIからドキュメントに至るまで何かと手薄です。
このPreviewのページにはDatabaseなどの追加ARM Templateの記載はもちろん、言及すらない、探す難易度が高い。そしてConfigurationに至っては存在しません。 幸いにして、ドキュメントになくてもVS Codeのbicep補完でリソースが出る。インテリセンスに頼ってエスパーしましょう。
こういうところがPreviewを使う上で本当に苦しいでしょう。そしてPreviewは長い、先が見えない不安は付きまといます。
細かいように思えるが、このドキュメントの一貫性の欠如はAzureを学ぶ上で、探すコストが著しく高く厳しいです。Previewも同じARM template referenceに置いて、定義をみるべき場所を減らせばよさそうです。
Bicep Module 粒度
BicepのModuleは、Terraform同様にある程度の粒度で組むのがよさそう。 いわゆる1 Resourceで1 Moduleというのはなるべく避けるべきでしょう。(拡張性が事実上ない) ただ、隠蔽するという意味では十分拡張性があってメンテコストが低いならあり。(resourceを露出させたくないのもわかる)
ダメな例
Subnetがarray of objectを受け付けるが、1subnet固定 + vnetが同時に作成される前提になっています。 これでは利用者は1 vnetにn subnetはできず、かならずvnetに1 subnetが強制されるでしょう。
@description('Specifies the Azure location where the key vault should be created.') param location string =resourceGroup().location @description('Tag information for vnet') param tags object = {} @description('Virtual network name') param virtualNetworkName string @description('Address prefix for virtual network') param addressPrefix string = '10.0.0.0/8' @description('Subnet name') param subnetName string @description('Subnet prefix for virtual network') param subnetPrefix string = '10.1.0.0/16' resource vn 'Microsoft.Network/virtualNetworks@2020-06-01' = { name: virtualNetworkName location: location tags: tags properties: { addressSpace: { addressPrefixes: [ addressPrefix ] } subnets: [ { name: subnetName properties: { addressPrefix: subnetPrefix privateEndpointNetworkPolicies: 'Disabled' } } ] } } output id string = vn.id output name string = vn.name output subnetIds array = [ { id: vn.properties.subnets[0].id name: vn.properties.subnets[0].name } ]
複数のAKSを構成する必要がないなら、ACRやACR Role Assignmentなど、関連するリソースをまとめてしまうほうがいいでしょう。
// パラメーター // リソース resource vn 'Microsoft.Network/virtualNetworks@2020-06-01' = { } resource symbolicname 'Microsoft.ContainerRegistry/registries@2020-11-01-preview' = { } // 他隠蔽できるリソース... // アウトぷっと output id string = vn.id
Terraformなどを使っている人にとっては、Terraformモジュールと同じコンセプトで分離すればいい、といえば伝わるでしょうか。
Bicep Parameter
Parameterで活躍するのが型システムです。 型が強く機能すれば、どのパラメーターに何をいれればいいのか、インテリセンスがドキュメントとして機能します。 Bicepの型定義システム自体は決して強くない。だが、VS CodeのLanguage Serverが強力に機能しているので、インテリセンスだけを見るとTerraformよりも書きやすい。
Data types in Bicep - Azure Resource Manager | Microsoft Docs
string, int, bool の扱いやすさ
型を指定すれば、パラメーターを渡すとき、使うときに型チェックされて入力している値の型と合致しているか見てくれる。 terraformと同程度には扱えるし、便利です。
param strParam string param enable bool
また、attirbuteで@allowed
などをparamの上の行の書けば入力をenum値で制限もできて便利です。
@allowed([ 'apple' 'orange' ]) param fruit string
パスワードのようなセキュアな値は、@secured()
を付ければSecureStringとして扱われてDeploy Historyなどに乗らないのでこれも便利。
@secure() param password string
object型の型宣言が弱い
Bicepのobject型は、型宣言時にプロパティを宣言できないため使いにくいという印象がぬぐえない。
// 宣言時にデフォルト値をもってプロパティが決まる param foo object = { str_prop = '' num_prop = 111 bool_prop = true array_prop = [] }
なぜ、型宣言時にプロパティを宣言できないのが使いにくいのでしょうか。
IaCで避けられるなら避けたほうがいいのは、デフォルト値の設定です。 デフォルト値が、オフィシャルのARM Templateのbicep Resourceのような本体ならいいのだが、Moduleとして提供する場合はデフォルト値を入れた/入れてないで事故が起こりやすい。
そのため、基本的にパラメーターで与えたいものはデフォルト値なしで、型宣言だけして与えるのがよいと、私が見てきた多くの現場ではプラクティスとして得ています。
例えばterraformでは、変数の型宣言は次のようにデフォルト値なしで行えます。
variable "foo" { type = set(object({ str_prop = string num_prop = number list_prop = list(string) set_prop = set(string) map_prop = map(string) })) }
bicepも、object型宣言時にプロパティと型を指定できれば事故を防げてうれしいのだが、できないので諦めよう。
array 型の型宣言が弱い
同じことはarray型にも言えるが、stringやintなどの単一の型なら推論が効くので何も問題がない。 だが、objectのarrayとなると完全に無力です。parameterに渡すとき、parameterを使うときの両方でインテリセンスは沈黙します。
そもそもの型宣言がarrayでしかないので無力としか言えない、ここからプロパティを推論できるようになるといいのだが。
param foo array = [ { str_prop = '' num_prop = 111 bool_prop = true array_prop = [] } ]
terraformのlist(map(string))
型のインテリセンスの利かなさと同じといえばイメージしやすいでしょうか。
実行時Parameter の渡し方
bicepは、実行時に2つの方法でパラメーターを渡せます。
- cli引数
- jsonファイル参照
cli引数は-p key=value
で指定できるので使いやすくはじめのうちはこれが多い。
az deployment group create --resource-group dev-foo -f foo.bicep -p key=value -p key2=value2 --mode Complete
ただ、実際にCIで回し始めるとdevやstgなど、決まった環境に決まった実行を毎度行うことが多くなります。 ということで、いちいち引数設定せずjson にしておいて実行引数はいつも同じになっていくでしょう。
az deployment group create --resource-group dev-foo -f foo.bicep -p @param.json --mode Complete
json parameterがちょっと使いにくく、bicepで指定していないparameterがjsonに定義されていると引数が渡せずエラーになります。 設定ファイルを共通にして、いくつかのbicepファイルに分ける、という使い方には向いていないのでなんとももどかしいものがある。
az deployment group create --resource-group dev-foo -f foo.bicep -p @param.json --mode Complete # foo とbar で同じ param じゃないとパラメーター渡しでエラー az deployment group create --resource-group dev-bar -f foo.bicep -p @param.json --mode Complete
諦めて、それぞれのbicepごとにparamを用意することになったが微妙。
existing と リソースの存在保障
existingは、いわゆるterraformのdataリソースのように、既存のリソースからリソース参照を拾ってくる使い方のために用意されています。
たとえば、次のようなstorage accountリソースを拾ってくる書き方ができます。
resource stg 'Microsoft.Storage/storageAccounts@2019-06-01' existing = { name: 'myacc' }
では、subnetのように、他のリソース(subnetならvnet ) の中にあるリソースはどうやってとってくるかというと、vnetを拾ってからsubnetを拾うのがいいでしょう。 例えば次のようにします。
resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' existing = { name: 'vnet-name' } resource subnet 'Microsoft.Network/virtualNetworks/subnets@2021-02-01' existing = { name: '${vnet.name}/my-subnet' }
existing の実行成功は存在保障ではない
このexisting処理の問題点は、本当にそのリソースが取れたかの確証が取れないことです。 通常terraformやpulumiでは、data resourceで対象のリソースの取得に失敗した場合エラーで中断します。 だが、bicepでは中断処理が行われない。
たとえば、先ほどのvnetをnameではなくid参照にするとどうなるでしょう。
resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' existing = { name: 'vnet-name' } resource subnet 'Microsoft.Network/virtualNetworks/subnets@2021-02-01' existing = { name: '${vnet.id}/my-subnet' // vnet.name から vnet.id }
結果は、subnetが取れない、です。それにも関わらずARM Templateのデプロイ時にここはパスされて、後続の処理では「取れてないsubnet」を渡そうとします。結果、デプロイ自体はは、subnetを使うリソースで作成が失敗してエラーになります。
エラーメッセージもリソースが作れなかったことを示すのみで、subnetが取れなかった原因とは連想しにくい。 本来は、原因であるsubnetの取得で失敗してエラーになってほしいのは言うまでもない。
existingは、既存のリソースをとってくるが、とってきたことを保証しない。 これはIaCとしては厄介な挙動で、what-ifのような実行前の確認で検知できないことを示しています。 Terraformではdata sourceを使うことで確証を取れるのだが、Bicepでは実行前にazコマンドなどで取得してパラメーターに渡すぐらいしか確証とれなさそうです。
なお、こういったsubnet -> vnetという依存関係があるリソースは、id上で{parent_id}/subnets/{subnet_name}
のようなresource idルールが一般に存在するため、subnetをexistingで拾う必要がない。
existingの現状の挙動では、無理して使う理由が乏しいので回避できるならするといいでしょう。
Role Name の取得
Roleには、Build-in RoleとCustom Roleが存在します。 AzureのIAMはリソースごとに存在するので、RBAもリソースごとに他のリソースやRoleと関連づけることになります。 つまり、role assignmentは、リソースごとに行う。
参考: AWS の場合、IAM Role でリソースとアクションをポリシーとして集権して、IAM Role Arn をリソースに割り振る。
Roleの特徴は、resourceIdの名前部分がGUIDであることです。
コマンドならaz role definition list --name 'ROLE_NAME' | jq -r .[].id
のようにすることでRole名さえわかっていればRole Idを取得できます。
だがbicepでリソースをとってくるときは、Microsoft.Authorization/roleDefinitions
リソースでexistingを経由して取得しようと思っても、subscriptionResourceId
関数で取得しようと思っても、GUIDがわからないと使えないことに気づくでしょう。
// resourceSubscription関数で subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ここに入れるGUIDをどう導き出すか') // あるいは existing 使うなら resource aksAcrPermissions 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = { name: 'ここに入れるGUIDをどう導き出すか' }
RoleがGUIDであるため名前から推測できない。 ということで、Built-In Role、Custom Roleそれぞれで既存Roleを参照するときに工夫が必要となります。
Built-in Role
Azureが提供している組み込みRoleは、全アカウントでRole NameとなるGUIDが固定です。
固定値なので何も考えずにGUIDを必要に応じて渡すか、Role NameからGUIDを返すだけのModuleを用意すればいいでしょう。 現実的に考えると、bicepのモジュールは関数的に使うには無駄にしんどいので、GUIDをそのまま渡すのがいいでしょう。(terraformやPulumiを考えると、こういうAzureで決定しているものの取得はbicepが組み込み関数で用意するといいでしょう)
例えば、AKS ClusterからACRのイメージを取得するRole Assignmentを与えるRole Assignmentを行うことを考えてみましょう。
ACRからのPull権限は、Build-in Role AcrPull
で提供されており、GUIDは7f951dda-4ed3-4680-a7ca-43fe172d538d
とわかっているので次のように書くことになるでしょう。
resource aks 'Microsoft.ContainerService/managedClusters@2021-03-01' = { // プロパティ } resource acr 'Microsoft.ContainerRegistry/registries@2020-11-01-preview' = { // プロパティ } resource aksAcrPermissions 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { name: guid(aks.name) scope: acr properties: { roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') principalId: aks.identity.principalId } }
Custom Role
Custom Roleを定義した場合、そのRoleが同じModuleやリソースから参照できるならそれを使えばいい。 そうでなく、先ほどのBuild-in Roleのように既存の取得をしたい場合、Role作成時のname時点で工夫するしかない。
RoleDefinitionsのnameは、GUIDです。このGUIDにbicepのGuid関数を利用し、引数にroleName
を指定すればいい。
こうすれば、参照する側はroleNameがわかっていれば、Guid関数で逆引きができます。
コードで見てみましょう。 ロールを作成するときに工夫するのがすべてです。
var role_name = 'my_awesome_role' resource hoge 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' = { name: guid(role_name) // ここで role_name を知っていればguid が算出できるようにします。 properties: { roleName: role_name // ほかのプロパティ } }
あとは、resourceが直接参照できなくても、次の方法で導き出すことができます。
// subscriptionResourceId 関数で取得 subscriptionResourceId('Microsoft.Authorization/roleDefinitions', guid('my_awesome_role')) // existing で取得 resource aksAcrPermissions 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = { name: guid('my_awesome_role') }
来てほしい機能
いくつか書いていてつらいのでサポートが欲しい機能。
- Shared Variables for Bicep Files · Issue #893 · Azure/bicep
- terraformのData Sourceのような、実リソースの存在を確定できる方法が欲しい。existingはリソースの存在を保証できず厳しい