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と付き合っていくうえでめんどくさい側面の一つだ。

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 だろうとどこだろうとAPIからドキュメントに至るまで何かと手薄だが、Azure も例外ではない。

この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 ファイルに分ける (当然bicepごとにparamはそれぞれ違う)、という使い方には向いていないのでなんとももどかしいものがある。

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')
}

来てほしい機能

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