TerraformでAzureのContainer GroupをVirtual Networkに作成するときの罠
devops terraform
Lastmod: 2023-06-23

3行まとめ

  • AzureのContainer InstanceをVirtual Networkの中にTerraformで作成したいけどなぜかうまくいかない。
  • Container InstanceとVirtual Networkの依存関係がTerraformに認識されていなかった。
  • Terraformのdepends_onを使えば解決!

背景

Azure上でウェブアプリを走らせたいときには

  • App service ( + Blob)
  • Functions + Blob

などの技術スタックを用いると、かなりあっさりアプリケーションをデプロイすることができる。まさしく王道。

一方で、containerizeされたアプリケーションをデプロイしたいけど、App serviceでは要件を満たせない場合などがあると思う。実際に僕の場合だと、ネットワークの設定やコンテナの構成で少しこだわりたいところが出てきて、その要件を満たすためにはApp serviceでは間に合わないことがわかった。

そこで次に検討されるのが、以下のサービスだと思う

  • Azure Kubernetes Service
  • Azure Container Instance

Azure Kubernetes Serviceはご存じKubernetesのマネージドサービス。Azure Container InstanceはAWSのECSに該当するサービスで、作成したContainerを渡すとあとはいい感じに運用してくれるサービス。

ここで、個人開発ではAKSが割に合わないなあとなると、必然的にAzure Container Instanceを選ぶことになる。

この記事では、この構成をTerraformで作ろうとするとなぜかうまくいかないことを解消する。

発生する問題

Azure Container InstanceをVirtual Networkの中に建てたい、しかもそれをTerraformで構成管理したい!というときに、どうもうまくいかない。

実際のmain.tfは長いので、必要なところを抜粋。ざっくり説明すると

  • subnet
  • Container Group(Container Instanceだと思ってもらえればOK)
  • Network Profile
    • Container Groupのネットワーク設定

を作成している。

resource "azurerm_subnet" "subnet" {
  name                 = "application-subnet"
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = ["10.0.1.0/24"]
  resource_group_name  = azurerm_resource_group.rg.name
    delegation {
      name = "delegation"

      service_delegation {
        name    = "Microsoft.ContainerInstance/containerGroups"
        actions = ["Microsoft.Network/networkinterfaces/*", "Microsoft.Network/virtualNetworks/subnets/action", "Microsoft.Network/virtualNetworks/subnets/join/action", "Microsoft.Network/virtualNetworks/subnets/prepareNetworkPolicies/action", "Microsoft.Network/virtualNetworks/subnets/unprepareNetworkPolicies/action"]
      }
    }
}

resource "azurerm_container_group" "acg" {
  name                = "container-group"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  ip_address_type     = "Private"
  network_profile_id  = azurerm_network_profile.app-netprofile.id
  os_type = "Linux"

  container {
    name   = "hello-world"
    image  = "mcr.microsoft.com/azuredocs/aci-helloworld:latest"
    cpu    = "0.5"
    memory = "1.5"

    ports {
      port     = 80
      protocol = "TCP"
    }
  }
}

resource "azurerm_network_profile" "app-netprofile" {
  name                = "app-netprofile"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name

  container_network_interface {
    name = "app-enic"
    ip_configuration {
      name      = "app-ipconfig"
      subnet_id = azurerm_subnet.subnet.id
    }
  }
}

で、これを走らせると、以下のエラーが出る。

# TODO: Error logをここに貼る。

エラーログを素直に読めば、Virtual Network SubnetからContainerInstancesへのdelegation(日本語訳は「委譲」)がうまくいっていないということになる。

しかし、subnetの設定を抜粋すると、

resource "azurerm_subnet" "subnet" {
  name                 = "application-subnet"
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = ["10.0.1.0/24"]
  resource_group_name  = azurerm_resource_group.rg.name
    delegation {
      name = "delegation"

      service_delegation {
        name    = "Microsoft.ContainerInstance/containerGroups"
        actions = ["Microsoft.Network/networkinterfaces/*", "Microsoft.Network/virtualNetworks/subnets/action", "Microsoft.Network/virtualNetworks/subnets/join/action", "Microsoft.Network/virtualNetworks/subnets/prepareNetworkPolicies/action", "Microsoft.Network/virtualNetworks/subnets/unprepareNetworkPolicies/action"]
      }
    }
}

最後の部分、service_delegationで問題なく"Microsoft.ContainerInstance/containerGroups"へ委譲できているように見える。

付与する権限が足りてないのかなと思って、Terraformのドキュメントに乗っている、actionsの配列をすべて突っ込んでみたりしたが、うまく動かなかった。

解決の糸口とその方法

ふとした拍子に、

Terraformはリソースを並列に生成すること

を思い出して(公式ドキュメント)、「もしかしてリソース作成の順番が影響するのでは?subnetがない状態でそのsubnetへの権限をリクエストしてしまっているのではないか」と思って検証したのが功を奏した。

  1. はじめにVirtual NetworkとSubnetを生成するmain.tfterraform applyする。
    1. 成功
  2. 次に、Subnetにdelegation blockを追加して、再度terraform applyする。
    1. 成功
  3. 最後に、Container GroupとNetwork Profileを追加して、terraform applyする。
    1. 成功!!

一件落着?

Terraformは状態を記述すれば、何度やっても同じ結果が得られることが売りのはず。こんな原始的な方法でしか生成できないだなんてありえない!なにか公式がいい手立てを用意しているはず!

とおもって調べたら、depends_onというまさしくな設定があることを初めて知った。

これは

resource "azurerm_subnet" "subnet" {
  name                 = "application-subnet"
  virtual_network_name = azurerm_virtual_network.vnet.name
  ... # 省略
}

resource "azurerm_container_group" "acg" {
  name                = "container-group"
  ip_address_type     = "Private"
  network_profile_id  = azurerm_network_profile.app-netprofile.id
  ... # 省略
  depends_on = [
    azurerm_subnet.subnet # resourceのtypeとlocal nameで指定
  ]
}

のように記述すると、Terraformが自動的に認識することができない、隠れた(implicit)依存性を明記することができ、Terraformが上手にリソース作成することができるようになる。(Resourceの依存性解決の話はここdepends_onの話はここのそれぞれ公式ドキュメントが詳しい)

今回のリソース作成で自動的な依存性解決が行われなかったのは、おそらくAzure Container Groupのネットワーク設定を直接リソースに記述するのではなく、Network Profileという別リソースに記述しなければならなかったことが絡んでいると思われる。

まとめ

  • Terraformにとってimplicitなdependencyが存在するとき、そのリソース作成がうまくいかない場合がある
  • 今回はこれを、Subnet + ACG + Network Profileの組み合わせで踏んでしまった
  • 段階的に生成すればOK
    • 原始的に、手作業で段階的に生成することでも回避できる。
      • 実際の開発現場では、段階的にコードが継ぎ足されるはず。
      • 「本番環境では動くのに、0から開発環境を作成しようとするとなぜか失敗する」悲劇、頻発してそう。
    • depends_onを書けば、いつでもちゃんと回避できる。

depends_onで、よきTerraformライフを!

後日談

この記事に記載した問題は、Ubuntu 22.04の上で実行していた際に遭遇したが、なぜかWindows 10の上でまったく同じコードを使用すると問題なくリソース作成できてしまった。Terraform CLIのバージョン違いとかかな。悲しい。