tech.guitarrapc.cóm

Technical updates

Azure Bicep の設計 Resource編

前回は、Bicepの性質から、どのような基本設計でIaCを指向するか書きました。

tech.guitarrapc.com

今回は、実際にBicep Resourceを使って書くときに、どのような工夫が必要なのかメモしておきます。

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つの方法でパラメーターを渡せます。

  1. cli引数
  2. 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リソースのように、既存のリソースからリソース参照を拾ってくる使い方のために用意されています。

Referencing existing resources

たとえば、次のような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が固定です。

一覧: Azure built-in roles - Azure RBAC | Microsoft Docs

固定値なので何も考えずに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')
}

来てほしい機能

いくつか書いていてつらいのでサポートが欲しい機能。