前回の投稿で、Terraform を使って AKS で AGIC を動かす際のハマりポイントを紹介しました。

その際はマネージド ID の権限周りの設定がうまくできていなかったのですが、解決方法がわかったので改めて Terrafrom で AKS と AGW をデプロイする書き方を整理します。

デプロイパターン

まず、AGIC から使う前提の AGW を Terraform でデプロイするパターンとしては以下の 2 つが考えられます。

  1. AKS と AGW を個別にデプロイ
  2. AKS デプロイ時に AGW も作成

それぞれについて、Terraform の書き方とデメリットについて説明します。

前提条件として、以下のリソースは既に存在しているものとします。

  • リソースグループ
  • VNet
  • AKS 用 サブネット
  • AGW 用 サブネット

AKS と AGW を個別にデプロイ

PIP

AKS と AGW を個別にデプロイするパターンでは、Public IP も作成する必要があります。

resource "azurerm_public_ip" "pip" {
  name                = local.pip_name
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  allocation_method   = "Static"
  sku = "Standard"
}

AGW

次に、PIP に依存する形で AGW を作成します。

今回作成する AGW では、HTTP で受けたリクエストを HTTPS へリダイレクトさせます。 redirect_configuration ブロックでリダイレクト先の HTTPS リスナーを指定し、request_routing_rule ブロックの HTTP ルーティング規則 でリダイレクト設定を指定します。

リダイレクトを設定する引数 redirect_configuration_name は、引数 backend_address_pool_namebackend_http_settings_name とは競合するので記載しません。

resource "azurerm_application_gateway" "agw" {
  ...

  # HTTPS リスナー
  http_listener {
    name                           = "fl-https"
    frontend_ip_configuration_name = "appGatewayFrontendIP"
    frontend_port_name             = "fp-443"
    protocol                       = "Https"
    ssl_certificate_name           = "cert"
  }

 # HTTP リスナー
  http_listener {
    name                           = "fl-http"
    frontend_ip_configuration_name = "appGatewayFrontendIP"
    frontend_port_name             = "fp-80"
    protocol                       = "Http"
  }

  # HTTPS ルーティング規則
  request_routing_rule {
    name                       = "rr-https"
    rule_type                  = "Basic"
    http_listener_name         = "fl-https"
    backend_address_pool_name  = "defaultaddresspool"
    backend_http_settings_name = "defaulthttpsetting"
  }

  # HTTP ルーティング規則
  request_routing_rule {
    name                        = "rr-http"
    rule_type                   = "Basic"
    http_listener_name          = "fl-http"
    redirect_configuration_name = "sslr-https"
  }

  # リダイレクト設定
  redirect_configuration {
    name                 = "sslr-https"
    redirect_type        = "Permanent"
    target_listener_name = "fl-https"
    include_path         = true
    include_query_string = true
  }

  ...
}

また、ssl_certificate ブロックを使って HTTPS で利用する証明書もデプロイと同時に AGW へインポートします。

このとき PFX ファイルを指定するのですが、filebase64() という関数を使うことで tf ファイルから見たパスで PFX ファイルを指定することができます。 あわせてパスワードも sensitive で設定しておきます。

証明書は HTTPS リスナーで指定します。

resource "azurerm_application_gateway" "agw" {
  ...

  # HTTPS リスナー
  http_listener {
    name                           = "fl-https"
    frontend_ip_configuration_name = "appGatewayFrontendIP"
    frontend_port_name             = "fp-443"
    protocol                       = "Https"
    ssl_certificate_name           = "cert"
  }

  # SSL 証明書
  ssl_certificate {
    name     = "mysslcert"
    data     = filebase64("test-cert.pfx")
    password = var.ssl_certificate_password
  }

  ...
}

variable "ssl_certificate_password" {
  sensitive = true
}
【 AGW 用 tf ファイルは折りたたんでいます】

resource "azurerm_application_gateway" "agw" {
  name                = local.agw_name
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location

  sku {
    name     = "Standard_v2"
    tier     = "Standard_v2"
    capacity = 2
  }

  gateway_ip_configuration {
    name      = "appGatewayFrontendIP"
    subnet_id = azurerm_subnet.subnet_agw.id
  }

  frontend_port {
    name = "fp-80"
    port = 80
  }

  frontend_port {
    name = "fp-443"
    port = 443
  }

  frontend_ip_configuration {
    name                 = "appGatewayFrontendIP"
    public_ip_address_id = azurerm_public_ip.pip.id
  }

  backend_address_pool {
    name = "defaultaddresspool"
  }

  backend_http_settings {
    name                  = "defaulthttpsetting"
    cookie_based_affinity = "Disabled"
    port                  = 80
    protocol              = "Http"
    request_timeout       = 30
  }

  http_listener {
    name                           = "fl-http"
    frontend_ip_configuration_name = "appGatewayFrontendIP"
    frontend_port_name             = "fp-80"
    protocol                       = "Http"
  }

  http_listener {
    name                           = "fl-https"
    frontend_ip_configuration_name = "appGatewayFrontendIP"
    frontend_port_name             = "fp-443"
    protocol                       = "Https"
    ssl_certificate_name           = "mysslcert"
  }

  request_routing_rule {
    name                        = "rr-http"
    rule_type                   = "Basic"
    http_listener_name          = "fl-http"
    redirect_configuration_name = "sslr-https"
  }

  request_routing_rule {
    name                       = "rr-https"
    rule_type                  = "Basic"
    http_listener_name         = "fl-https"
    backend_address_pool_name  = "defaultaddresspool"
    backend_http_settings_name = "defaulthttpsetting"
  }

  redirect_configuration {
    name                 = "sslr-https"
    redirect_type        = "Permanent"
    target_listener_name = "fl-https"
    include_path         = true
    include_query_string = true
  }

  ssl_certificate {
    name     = "cert"
    data     = filebase64("cert.pfx")
    password = var.ssl_certificate_password
  }

  depends_on = [azurerm_public_ip.pip]
}


AKS

次に、AKS をデプロイします。

AKS と AGW を個別にデプロイする場合でも、AGIC のアドオンを有効化するために azurerm_kubernetes_cluster モジュール内で AGW のリソースを指定します。

このとき、引数 gateway_id のみを指定してください。 引数 subnet_id などを記載すると、新規に AGW をデプロイしようとしてしまい、前述した AGW とリソースが重複してエラーとなります。

resource "azurerm_kubernetes_cluster" "aks" {
  ...

  ingress_application_gateway {
    gateway_id = azurerm_application_gateway.agw.id
  }

  ...
}
【AKS 用ファイルは折りたたんでいます】

resource "azurerm_kubernetes_cluster" "aks" {
  name                       = local.aks_name
  resource_group_name        = azurerm_resource_group.rg.name
  location                   = azurerm_resource_group.rg.location

  default_node_pool {
    name           = "nodepool1"
    node_count     = 1
    vm_size        = "Standard_B2ms"
    vnet_subnet_id = azurerm_subnet.subnet_aks.id
  }

  network_profile {
    network_plugin     = "azure"
    service_cidr       = "10.0.0.0/16"
    docker_bridge_cidr = "172.17.0.1/16"
    dns_service_ip     = "10.0.0.10"
  }

  identity {
    type = "SystemAssigned"
  }

  linux_profile {
    admin_username = "azureuser"
    ssh_key {
      key_data = file(var.ssh_key_path)
    }
  }

  ingress_application_gateway {
    gateway_id = azurerm_application_gateway.agw.id
  }

  azure_active_directory_role_based_access_control {
    managed                = true
    admin_group_object_ids = var.admin_group_object_ids
    azure_rbac_enabled     = true
  }

  depends_on = [azurerm_application_gateway.agw]
}


ロール割り当て

ここまでで AGW と AGIC アドオンが有効化された AKS がデプロイされるのですが、このままでは前回の投稿で述べた権限エラーが発生します。

具体的には、AKS デプロイ時に AKS 管理用のリソースグループ (MC_rg0101_aks01_japaneast といった名前) が作成されて、その中に AGIC 用マネージド ID も作成されるのですが、このマネージド ID が AGW に対して編集権限を持たないためエラーが発生します。

ingress-appgw-deployment Pod に以下のようなエラーが出力されていました(長いので抜粋しています)。

E1031 19:26:54.236135       1 client.go:170] Code="ErrorApplicationGatewayForbidden" Message="Unexpected status code '403' while performing a GET on Application Gateway. 
	You can use '
		az role assignment create --role Reader --scope /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg01 --assignee 00000000-0000-0000-0000-000000000000; 
		az role assignment create --role Contributor --scope /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg01/providers/Microsoft.Network/applicationGateways/agw01 --assignee 00000000-0000-0000-0000-000000000000
	' to assign permissions. 
	AGIC Identity needs atleast has 'Contributor' access to Application Gateway 'agw01' and 'Reader' access to Application Gateway's Resource Group 'rg01'." 

エラーには「AGIC ID には、少なくとも Application Gateway ‘agw01’ への ‘Contributor’ アクセスと、Application Gateway のリソース グループ ‘rg01’ への ‘Reader’ アクセスが必要です。 (Google 翻訳)」とあります。

ありがたいことに、必要な権限を付与するコマンドまで教えてくれました。 このコマンドを直接実行すると AGIC のエラーは解消したので、コマンドの内容を元に Terraform で実装します。

マネージド ID に権限を付与するには azurerm_role_assignment モジュールを利用します。

引数 scoperole_definition_name にはそれぞれ対象のリソースと必要な権限を指定します。 principal_id は AGIC 用のマネージド ID のオブジェクト ID (プリンシパル ID) を指定します(ID 周りの呼び名統一してくれ)。

このマネージド ID は AKS デプロイ時に動的に生成されるため、クラスタ作成の度にオブジェクト ID が変わってしまいます。 マネージド ID の名前は固定のようですが、azurerm_role_assignment では ID ベースでの指定が必要なので、名前での指定ができません。

では「AGIC 用のマネージド ID のオブジェクト ID をどうやって指定するか」ですが、AKS デプロイ後の terraform.tfstate ファイルに答えがありました。

{
  "mode": "managed",
  "type": "azurerm_kubernetes_cluster",
  "name": "aks",
  "provider": "provider[\"registry.terraform.io/hashicorp/azurerm\"]",
  "instances": [
    {
      "attributes": {
        "identity": [
          {
            "principal_id": "00000000-0000-0000-0000-000000000000",
            "tenant_id": "00000000-0000-0000-0000-000000000000",
            "type": "SystemAssigned",
            "user_assigned_identity_id": ""
          }
        ],
        "ingress_application_gateway": [
          {
            "effective_gateway_id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg01/providers/Microsoft.Network/applicationGateways/agw01",
            "gateway_id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg01/providers/Microsoft.Network/applicationGateways/agw01",
            "ingress_application_gateway_identity": [
              {
                "client_id": "00000000-0000-0000-0000-000000000000",
                "object_id": "00000000-0000-0000-0000-000000000000",
                "user_assigned_identity_id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/MC_rg01_aks01_japaneast/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ingressapplicationgateway-aks01"
              }
            ],
          }
        ],
      }
    }
  ]
}

type: "azurerm_kubernetes_cluster"instances[].attributes.ingress_application_gateway というオブジェクトがあり、その中の object_id が AGIC 用のマネージド ID のオブジェクト ID になります。 このオブジェクト ID を Terraform で指定してあげることで、AKS クラスタの作り直してもマネージド ID に権限を割り当てることができます。

resource "azurerm_role_assignment" "agic_rg_role" {
  scope                = azurerm_resource_group.rg.id
  role_definition_name = "Reader"
  principal_id         = azurerm_kubernetes_cluster.aks.ingress_application_gateway[0].ingress_application_gateway_identity[0].object_id

  depends_on = [azurerm_kubernetes_cluster.aks]
}

resource "azurerm_role_assignment" "agic_agw_role" {
  scope                = azurerm_application_gateway.agw.id
  role_definition_name = "Contributor"
  principal_id         = azurerm_kubernetes_cluster.aks.ingress_application_gateway[0].ingress_application_gateway_identity[0].object_id

  depends_on = [azurerm_application_gateway.agw, azurerm_kubernetes_cluster.aks]
}

デメリット

AKS と AGW を個別にデプロイするパターンのデメリットですが、「AGIC で AGW が変更されると Terraform の state ファイルと差分が発生してしまう」という点があります。

AGIC を使うように定義した Ingress リソースを AKS にデプロイすると、トラフィックを AGW から AKS 内に流すために Ingress コントローラが AGW の設定を変更します。 このように、AGIC は Terraform で管理する AGW リソースに対して変更を加えてしまうため、Ingress デプロイ後に terraform plan を実行すると差分が生じてしまいます。

これは Infrastructure as Code を実現する上で致命的なデメリットだと考えます。

クラスタをゼロから構築する場合は Terraform と Kubernetes のマニフェストでインフラ環境の再現性はありますが、Azure リソースを変更しようと Terraform を弄ったら上記の差分が出てきてしまうため運用に支障をきたします。

そのため、Terraform で AGIC を使う AKS を管理したい場合は、次に述べる「AKS デプロイ時に AGW も作成」するパターンのほうが良いと考えます。

追記

Terraform の lifecycle ブロックを使うことで、指定リソースに変更がある場合の挙動を制御できるようです。 デメリットで述べたような Terraform 外でリソースが更新される場合は ignore_changes を指定することで、対象リソースの変更が無視されるようになります。

「致命的なデメリット」というコメントは完全に知識不足によるものでした…。

AKS デプロイ時に AGW も作成

では、その「AKS デプロイ時に AGW も作成」するパターンですが、こちらは PIP と AGW の定義が不要なためシンプルです。

AKS

azurerm_kubernetes_cluster モジュールの ingress_application_gateway を使うところまでは一緒ですが、引数は gateway_namesubnet_id を指定します(subnet_cidr という引数もありますが、今回は説明対象外とします)。

resource "azurerm_kubernetes_cluster" "aks" {
  ...

  ingress_application_gateway {
    gateway_name = azurerm_application_gateway.agw.name
    subnet_id    = azurerm_subnet.subnet_agw.id
  }

  ...
}

AGW 用のサブネットを用意しておくだけで、あとは AKS が自動的に AGW と PIP を作成してくれます。

また、AKS 管理用リソースグループに AGW が作られます。 AGIC 用のマネージド ID と AGW が同じリソースグループに存在するため、マネージド ID への権限割り当ても必要ありません。

デメリット

AGW を自動的に作成するため、「デプロイ時に証明書を AGW へインポート」などの細かい設定ができません。

この場合に Terraform からイイカンジに管理する方法があるか考えたのですが、良い案が見つかりませんでした。 もしご存知の方いたら Twitter などでコメントいただけると嬉しいです。

あとがき

前回につづき、Terraform で Application Gateway (AGW) を管理する方法について書いてみました。

前回は「AKS と AGW を個別にデプロイ」するパターンをうまく構築できませんでしたが、なんとか理解が進んでデプロイできるようになりました。 ただ、デメリットでも記載したように「AGIC で利用する場合は運用するのが難しいのでは」と思います。

普段の投稿ではあまり断定するような表現はしないのですが(否定的コメントが怖いので)、Ansible を使った IaC を進めていって自分の中で IaC に対する考えが固まってきたこともあって、今回はデメリットにこのような書き方をしてみました。

とは言え、IaC はまだまだ奥が深いので精進あるのみです。