💪

Bicepを使ってAKS+ACRを一瞬で構築/デプロイする

2021/12/14に公開

はじめに

先日Azureハッカソンというものに参加してきました。
https://hackathon.we-are-ma.jp/ms2021/

参加の動機は「いろいろな人と知り合いたいなー」という軽いものでしたが、なんと最優秀賞をいただくことができました。2日間Azureのリソースにたくさん触れ、大変良い経験になったのですが、特にBicepに興味を持ったのでいろいろ試してみることにしました。

ちなみにハッカソン当日の様子については、同じチームのメンバが記事を書いてくれました
https://qiita.com/arakirai1128/items/69f9efcc7e77f5d4c1c0

Bicep(AKS+ACR)のソースコード

さっそくBicepの説明に入っていきたいとこですが。。
説明なんかいらん!!ソースコードだけ見れればいいんだよ!って方のために。

az group create -n rg-XXXX -l japaneast
az deployment group create -f ./main.bicep -g rg-XXXX

上記コマンド実行後、paramで指定している項目を入力すると自動でデプロイされます。

main.bicep
@description('Resource Deployment Location.')
param location string = resourceGroup().location

@description('AKS Cluster Name')
param clusterName string

@description('AKS Cluster Managed Identity Name')
param managedIdName string = guid(clusterName)

@description('VNET Name Prefix')
param VNetAddressPrefix string = '10.10.0.0/16'

@description('SUBNET Name Prefix')
param SubnetAddressPrefix string = '10.10.1.0/24'

resource AKSVNet 'Microsoft.Network/virtualNetworks@2021-03-01' = {
  name: 'vn-${clusterName}'
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: [
        VNetAddressPrefix
      ]
    }
    subnets: [
      {
        name: 'sn-${clusterName}'
        properties: {
          addressPrefix: SubnetAddressPrefix
        }
      }
    ]
  }
}

resource AKSSubNet 'Microsoft.Network/virtualNetworks/subnets@2021-03-01' = {
  parent: AKSVNet // https://githubmemory.com/repo/Azure/bicep/issues/1972
  name: 'sn-${clusterName}'
}

// ユーザー割り当て Managed ID の作成
resource ManagedId 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
  name: managedIdName
  location: location
}

// ロールの作成と割り当て
@description('A new GUID used to identify the role assignment')
param roleNameGuid string = guid(managedIdName)

resource RoleAssignment 'Microsoft.Authorization/roleAssignments@2020-08-01-preview' = {
  name: roleNameGuid
  scope: AKSSubNet
  properties: {
    roleDefinitionId: '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c'
    principalId: ManagedId.properties.principalId
    principalType: 'ServicePrincipal'
    // https://githubmemory.com/repo/Azure/bicep/issues/3695
  }
  dependsOn: [
    ManagedId
  ]
}

// AKS Cluster の作成
resource aks 'Microsoft.ContainerService/managedClusters@2021-08-01' = {
  name: clusterName
  location: location
  identity: {
    type: 'UserAssigned'
    // userAssignedIdentities: ManagedIdと指定するとデプロイできない。
    // https://stackoverflow.com/questions/64877861/the-template-function-reference-is-not-expected-at-this-location
    userAssignedIdentities: {
      '${ManagedId.id}': {}
    }
  }
  properties: {
    dnsPrefix: clusterName
    enableRBAC: true
    agentPoolProfiles: [
      {
        name: 'agentpool1'
        count: 2
        vmSize: 'standard_d2s_v3'
        mode: 'System'
        vnetSubnetID: AKSSubNet.id
      }
    ]
  }
}

// ACRの作成
@description('Provide a globally unique name of your Azure Container Registry')
param acrName string

@description('Provide a tier of your Azure Container Registry.')
param acrSku string = 'Basic'

resource acr 'Microsoft.ContainerRegistry/registries@2021-06-01-preview' = {
  name: acrName
  location: location
  sku: {
    name: acrSku
  }
  properties: {
    adminUserEnabled: true
  }
}

//https://docs.microsoft.com/ja-jp/azure/role-based-access-control/built-in-roles
var roleAcrPull = '7f951dda-4ed3-4680-a7ca-43fe172d538d'

resource assignAcrPullToAks 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
  name: guid(resourceGroup().id, acrName, aks.id, 'AssignAcrPullToAks')
  scope: acr
  properties: {
    description: 'Assign AcrPull role to AKS'
    principalId: aks.properties.identityProfile.kubeletidentity.objectId //https://github.com/Azure/bicep/discussions/3181
    principalType: 'ServicePrincipal'
    roleDefinitionId: '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/${roleAcrPull}'
  }
  dependsOn: [
    aks
  ]
}

Bicepってなに?

Bicepは、いわゆるIaCを実現するためのツールの1つであり、宣言型の構文を使用してAzureリソースをデプロイすることができます。AzureにはARMテンプレートというものがありますが、JSONなので人間が理解するには少しわかりづらいです。そこで簡潔にかけるBicepが登場したようです。

他のツールと比較した時のBicepの利点は以下の7つがあります。(公式掲載)

  • すべてAzureのリソースの種類と API バージョンのサポート
  • 単純な構文
  • 作成エクスペリエンス
  • モジュール性
  • Azure サービスとの統合
  • 管理する状態または状態ファイルがない
  • コストがかからないオープンソース

すべてのAzureリソースに対応しているはMicrosoftが提供しているので、当たり前といったところでしょうか笑。構文の単純さやコスト面に関しては、他のツールとあまり大差ない気がします。唯一の違いは「ステートレス」である部分です。利用場面や開発規模などにもよると思うので、一概に良い悪いは判断できないですね。

BicepでAKSとACRを構築する

最近CKSの勉強も始めたので、Kuberntes環境を簡単に構築できると便利だなと思い、AKSを対象とすることにしました。ACRはおまけです笑

まずはAKSで使用するパラメータを宣言します。
Bicepのデプロイ時のスコープはデフォルトでresourceGroupとなっていますが、明示的に指定することで、subscriptionをスコープとすることもできます。

@description('Resource Deployment Location.')
param location string = resourceGroup().location

@description('AKS Cluster Name')
param clusterName string

@description('AKS Cluster Managed Identity Name')
param managedIdName string = guid(clusterName)

@description('VNET Name Prefix')
param VNetAddressPrefix string = '10.10.0.0/16'

@description('SUBNET Name Prefix')
param SubnetAddressPrefix string = '10.10.1.0/24'

次にVNetを作成します。
変数の埋め込みは${hogehoge}のように記述できます。

resource AKSVNet 'Microsoft.Network/virtualNetworks@2021-03-01' = {
  name: 'vn-${clusterName}'
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: [
        VNetAddressPrefix
      ]
    }
    subnets: [
      {
        name: 'sn-${clusterName}'
        properties: {
          addressPrefix: SubnetAddressPrefix
        }
      }
    ]
  }
}

後述するRoleAssignmentのところでSubNetのオブジェクトでないと指定ができなかったので作成しています。

resource AKSSubNet 'Microsoft.Network/virtualNetworks/subnets@2021-03-01' existing = {
  parent: AKSVNet 
  name: 'sn-${clusterName}'
}

ユーザー割り当てマネージドIDとは、複数のリソースに対して割り当て可能なマネージドIDになります。 AKSを構築する上でシステム割り当てのマネージドIDでも問題ありませんが、今回は勉強も兼ねてユーザー割り当てのマネージドIDを作成します。

resource ManagedId 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
  name: managedIdName
  location: location
}

ロールの作成と割り当てを行います。組み込みロールについては以下のページをご確認ください。idの記載もあります。
https://docs.microsoft.com/ja-jp/azure/role-based-access-control/built-in-roles

idで指定できるといってもぱっと見なんのロールなのかわからない。。
ロール名からのid逆引きができると便利そう(もしかして私が知らないだけで、何か方法があるのだろうか)

ちなみにguid()とは、パラメーターに対するハッシュ関数の結果を36文字の文字列として返してくれる関数です。返される値は必ずしもグローバルに一意となるわけではありません。

例えば、サブスクリプションのスコープで一意の場合は①、リソース グループのスコープで一意の場合は②のような記述になります

guid(subscription().subscriptionId) #①
guid(resourceGroup().id) #②

ロールの作成と割り当てのコードはこちらです。
先程作成したマネージドIDにロールを割り当てます。

@description('Role assignment')
param roleNameGuid string = guid(managedIdName)

resource RoleAssignment 'Microsoft.Authorization/roleAssignments@2020-08-01-preview' = {
  name: roleNameGuid
  scope: AKSSubNet
  properties: {
    roleDefinitionId: '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c'
    principalId: ManagedId.properties.principalId
    principalType: 'ServicePrincipal'
    // https://githubmemory.com/repo/Azure/bicep/issues/3695
  }
  dependsOn: [
    ManagedId
  ]
}

やっとAKSの作成まで辿り着きました。シンプルに見えますが、AKSの設定項目は実はめちゃくちゃ多いです。細かい設定をしたい方は覗いてみてください。
https://docs.microsoft.com/ja-jp/azure/templates/microsoft.containerservice/managedclusters?tabs=bicep

userAssignedIdentitiesの部分ですが、ManagedIdオブジェクトを指定するとBicepからARMに変換した際にエラーを吐いてしまうため、無理やり指定しています。

resource aks 'Microsoft.ContainerService/managedClusters@2021-08-01' = {
  name: clusterName
  location: location
  identity: {
    type: 'UserAssigned'
    // userAssignedIdentities: ManagedIdと指定するとデプロイできない
    // https://stackoverflow.com/questions/64877861/the-template-function-reference-is-not-expected-at-this-location
    userAssignedIdentities: {
      '${ManagedId.id}': {}
    }
  }
  properties: {
    dnsPrefix: clusterName
    enableRBAC: true
    agentPoolProfiles: [
      {
        name: 'agentpool1'
        count: 2
        vmSize: 'standard_d2s_v3'
        mode: 'System'
        vnetSubnetID: AKSSubNet.id
      }
    ]
  }
}

ACRはこんな感じです。

@description('Azure Container Registry')
param acrName string

@description('Provide a tier of your Azure Container Registry.')
param acrSku string = 'Basic'

resource acr 'Microsoft.ContainerRegistry/registries@2021-06-01-preview' = {
  name: acrName
  location: location
  sku: {
    name: acrSku
  }
  properties: {
    adminUserEnabled: true
  }
}

最後にロール割り当てを行います。
コンテナーレジストリからImage PullするためのロールはAcrPullといいidが7f951dda-4ed3-4680-a7ca-43fe172d538dになります。

ここでハマったのがprincipalIdのところです。
勝手にAKSのマネージドIDと紐付けてやればいいと思っていたのですが、違いました。
デプロイしたPodがImagePullBackoff だったのを、ずっとロールが足りていないのだろうと思って無駄な時間を過ごしてしまいました。。

以下のページに

AKS クラスター ノード プールのマネージド ID を使用

と記載ありました。Kubelet Identity = Nodepool MSIらしいです。
ドキュメントを読み飛ばすクセはなんとかしなきゃですね。

https://docs.microsoft.com/ja-jp/azure/architecture/operator-guides/aks/aks-triage-container-registry

var roleAcrPull = '7f951dda-4ed3-4680-a7ca-43fe172d538d'

resource assignAcrPullToAks 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
  name: guid(resourceGroup().id, acrName, aks.id, 'AssignAcrPullToAks')
  scope: acr
  properties: {
    description: 'Assign AcrPull role to AKS'
    principalId: aks.properties.identityProfile.kubeletidentity.objectId //https://github.com/Azure/bicep/discussions/3181
    principalType: 'ServicePrincipal'
    roleDefinitionId: '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/${roleAcrPull}'
  }
  dependsOn: [
    aks
  ]
}

デプロイ

リソースグループを作成してaz deployment group createコマンドで、ファイルとリソースグループを指定するだけで簡単にデプロイできちゃいます。

az group create -n rg-XXXX -l japaneast
az deployment group create -f ./main.bicep -g rg-XXXX

おまけ(リソースの視覚化)

Visual Studio CodeのBicep拡張機能を使うと開発がとても捗るのでおすすめです。その中に面白い機能があります。それがVisualizerという、ファイル内のリソースの表現を視覚的に確認できる機能です。VSCode左上にあるビジュアライザーボタンを選択するとBicep Visualizerが開きます。

今回のAKS+ACRだとこのような図が作成されます。
ロールの割り当てとかは慣れるまで分かり難かったりするので初心者の方に対しては理解の助けになるかもしれませんね。

まとめ

一瞬でAKS+ACRが構築できるようになりました。これで突然K8sクラスタを使いたい!と思った時も対応できますね。
Bicepは簡単で理解しやすかったですが、Terraformのように状態管理をしているわけではないので、たまにコードとAzureの実態が整合性がとれなくなってリソースグループごと削除してしまうこともありました。ハッカソンや個人での検証といった用途には向いているかもしれませんが、実際に運用していくとなると大変かもしれません。
あとエラーが分かりにくかった笑

今回は触れませんでしたが、Bicepのモジュール化もやってみたいです。おそらくTerraformと同じようにモジュールを作れそうだなと思っています。

参考

https://docs.microsoft.com/ja-jp/azure/azure-resource-manager/bicep/overview
https://techblog.ap-com.co.jp/entry/2021/06/09/181340?utm_source=feed
https://zenn.dev/08thse/articles/55-bicep-for-aks

Discussion