Azure Kubernetes Service(AKS)では、イングレスコントローラとしてNGINXイングレスコントローラベースのアプリケーションルーティングとAzure Application Gateway(AppGW)をインターフェースに使用できるApplication Gateway Ingress Controller(AGIC)が利用できます。
AppGWといえばL7ロードバランサであるとともにWeb Application Firewall(WAF)機能を搭載することができます。
セキュリティめちゃツヨAGICにするならAppGWにWAFを乗せたいところ。
普通にAKSをセットアップするとAppGWはStandard V2のSKUでセットアップされてしまいます。そこで、WAF v2のSKUでAppGWを構築しつつAKSと接続してセキュリティめちゃ強いAGICを利用できるようにします。
楽ちんに構築したいので、IaCツール OpenTofu を利用してセットアップしてみます。
OpenTofuはTerraformのOSSフォークです。今のところ大きな差はないのでTerraformでも同様のコードで動作できると思います。
TL;DR
- AppGWとAKSは別々に作成して、紐づける形で連携させる
- AGICのためのマネージドIDが生成されるので、ロール紐づけが必要
- AppGWが属するリソースグループに対する
Reader
ロール - AppGWに対する
Contributer
ロール - AppGWが使用するサブネットに対する
Network Contributer
ロール - AppGWに設定しうるWAFポリシーに対する
Network Contributer
ロール
- AppGWが属するリソースグループに対する
バージョン情報
ツール・プロバイダ | バージョン |
---|---|
OpenTofu | 1.9.0 |
hashicorp/azurerm | 4.25.0 |
構築コード
早速コードです。ちょっと長いので折りたたみで。
色々と誤魔化したり調整しているので、サンプルコードの動作保証はしていないです。
サンプルコード
terraform {
required_version = ">=1.9"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "4.25.0"
}
}
}
# 引数定義
variable "tenant_id" {
type = string
description = "テナントID"
}
variable "identity" {
type = string
description = "リソースのID"
}
variable "location" {
type = string
description = "リソースのロケーション"
}
variable "domain" {
type = string
description = "使用するドメイン"
}
variable "azrbac_admin_group_object_ids" {
type = list(string)
description = "AKS RBACで管理ロールとするEntra IDのオブジェクトIDリスト"
}
# リソース定義
## リソースグループ
resource "azurerm_resource_group" "rg" {
name = "rg-${var.identity}"
location = var.location
}
## 仮想ネットワーク
locals {
address_space_ipv4 = "172.16.0.0/12"
}
resource "azurerm_virtual_network" "vn" {
name = "vn-${var.identity}"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
address_space = [
local.address_space_ipv4,
]
}
resource "azurerm_subnet" "sub_default" {
name = "vn-${var.identity}-default"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vn.name
address_prefixes = [cidrsubnet(local.address_space_ipv4, 3, 0)]
service_endpoints = ["Microsoft.Storage"]
private_endpoint_network_policies = "Enabled"
depends_on = [azurerm_virtual_network.vn]
}
resource "azurerm_subnet" "sub_appgw" {
name = "vn-${var.identity}-appgw"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vn.name
address_prefixes = [
cidrsubnet(local.address_space_ipv4, 3, 1),
]
service_endpoints = ["Microsoft.Storage"]
private_endpoint_network_policies = "Enabled"
depends_on = [azurerm_virtual_network.vn]
}
resource "azurerm_subnet" "sub_aks_cls" {
name = "vn-${var.identity}-aks-cls"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vn.name
address_prefixes = [
cidrsubnet(local.address_space_ipv4, 3, 2),
]
service_endpoints = ["Microsoft.Storage"]
private_endpoint_network_policies = "Enabled"
depends_on = [azurerm_virtual_network.vn]
}
## IPアドレス
resource "azurerm_public_ip" "ipv4" {
name = "publicip-v4-${var.identity}"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
sku = "Standard"
sku_tier = "Regional"
allocation_method = "Static"
ip_version = "IPv4"
depends_on = [azurerm_virtual_network.vn]
}
resource "azurerm_dns_zone" "public_dns" {
name = var.domain
resource_group_name = azurerm_resource_group.rg.name
}
resource "azurerm_dns_a_record" "default" {
name = "@"
resource_group_name = azurerm_resource_group.rg.name
zone_name = azurerm_dns_zone.public_dns.name
ttl = 600
records = [azurerm_public_ip.ipv4.ip_address]
depends_on = [
azurerm_dns_zone.public_dns,
azurerm_public_ip.ipv4,
]
}
## WAFポリシー
resource "azurerm_web_application_firewall_policy" "default" {
name = "waf-custom-rules-${var.identity}-default"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
policy_settings {
enabled = true
# mode = "Prevention"
mode = "Detection"
}
managed_rules {
managed_rule_set {
type = "Microsoft_DefaultRuleSet"
version = "2.1"
}
}
custom_rules {
name = "AcmeHttp01Challenge"
priority = 10
action = "Allow"
rule_type = "MatchRule"
match_conditions {
operator = "BeginsWith"
transforms = ["Trim", "Lowercase", "RemoveNulls", "UrlDecode"]
match_variables {
variable_name = "RequestUri"
}
match_values = [
"/.well-known/acme-challenge/",
]
}
}
}
## Application Gateway
locals {
backend_address_pool_name = "${var.identity}-beap"
frontend_port_name_http = "${var.identity}-feport-http"
frontend_port_name_https = "${var.identity}-feport-https"
frontend_ip_configuration_name = "${var.identity}-feip"
http_setting_name = "${var.identity}-be-htst"
listener_name = "${var.identity}-httplstn"
request_routing_rule_name = "${var.identity}-rqrt"
}
resource "azurerm_application_gateway" "appgw_waf" {
name = "appgw-${var.identity}"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
sku {
name = "WAF_v2"
tier = "WAF_v2"
capacity = 1
}
enable_http2 = false
fips_enabled = false
force_firewall_policy_association = false
waf_configuration {
enabled = true
firewall_mode = "Prevention"
# 本当はMicrosoft_DefaultRuleSetにしたいがAPIがエラーするのでOWASPを使用
rule_set_type = "OWASP"
rule_set_version = "3.2"
file_upload_limit_mb = "500"
request_body_check = false
}
gateway_ip_configuration {
name = "appgw-ip-conf"
subnet_id = azurerm_subnet.sub_appgw.id
}
frontend_port {
name = local.frontend_port_name_http
port = 80
}
frontend_port {
name = local.frontend_port_name_https
port = 443
}
frontend_ip_configuration {
name = local.frontend_ip_configuration_name
public_ip_address_id = azurerm_public_ip.ipv4.id
}
backend_address_pool {
name = local.backend_address_pool_name
}
backend_http_settings {
name = local.http_setting_name
cookie_based_affinity = "Disabled"
port = 80
protocol = "Http"
request_timeout = 30
pick_host_name_from_backend_address = false
}
http_listener {
name = local.listener_name
frontend_ip_configuration_name = local.frontend_ip_configuration_name
frontend_port_name = local.frontend_port_name_http
protocol = "Http"
require_sni = false
firewall_policy_id = azurerm_web_application_firewall_policy.default.id
}
request_routing_rule {
name = local.request_routing_rule_name
rule_type = "Basic"
http_listener_name = local.listener_name
backend_address_pool_name = local.backend_address_pool_name
backend_http_settings_name = local.http_setting_name
priority = 10010
}
lifecycle {
# デプロイ後はAKS AGICで変更されるのですべて無視
ignore_changes = all
}
}
resource "azurerm_kubernetes_cluster" "aks" {
name = "aks-${var.identity}"
resource_group_name = azurerm_resource_group.name
location = azurerm_resource_group.rg.location
sku_tier = "Standard"
dns_prefix = "aks-${var.identity}"
automatic_upgrade_channel = "patch"
kubernetes_version = "1.30"
private_cluster_enabled = false
cost_analysis_enabled = "true"
oidc_issuer_enabled = true
workload_identity_enabled = true
azure_active_directory_role_based_access_control {
azure_rbac_enabled = true
tenant_id = var.tenant_id
admin_group_object_ids = var.azrbac_admin_group_object_ids
}
ingress_application_gateway {
gateway_id = azurerm_application_gateway.appgw_waf.id
}
network_profile {
network_plugin = "azure"
network_policy = "calico"
outbound_type = "loadBalancer"
load_balancer_sku = "standard"
}
workload_autoscaler_profile {
keda_enabled = true
}
default_node_pool {
name = "default"
vm_size = "Standard_D4as_v5"
os_sku = "AzureLinux"
temporary_name_for_rotation = "deftmp"
type = "VirtualMachineScaleSets"
auto_scaling_enabled = true
node_count = 1
min_count = 1
max_count = 10
max_pods = 100
vnet_subnet_id = azurerm_subnet.sub_aks_cls.id
zones = [
"1",
"2",
"3",
]
}
lifecycle {
ignore_changes = [
default_node_pool[0].node_count
]
}
}
resource "azurerm_role_assignment" "k8s_rg" {
scope = var.resource_group_id
principal_id = azurerm_kubernetes_cluster.aks.ingress_application_gateway[0].ingress_application_gateway_identity[0].object_id
role_definition_name = "Reader"
depends_on = [azurerm_kubernetes_cluster.aks]
}
resource "azurerm_role_assignment" "k8s_gateway" {
scope = var.appgw_id
principal_id = azurerm_kubernetes_cluster.aks.ingress_application_gateway[0].ingress_application_gateway_identity[0].object_id
role_definition_name = "Contributor"
depends_on = [azurerm_kubernetes_cluster.aks]
}
resource "azurerm_role_assignment" "k8s_gateway_network" {
scope = var.appgw_subnet_id
principal_id = azurerm_kubernetes_cluster.aks.ingress_application_gateway[0].ingress_application_gateway_identity[0].object_id
role_definition_name = "Network Contributor"
depends_on = [azurerm_kubernetes_cluster.aks]
}
resource "azurerm_role_assignment" "k8s_waf_policy_default" {
scope = var.waf_policy_default_id
principal_id = azurerm_kubernetes_cluster.aks.ingress_application_gateway[0].ingress_application_gateway_identity[0].object_id
role_definition_name = "Network Contributor"
depends_on = [azurerm_kubernetes_cluster.aks]
}
Azure Application Gatewayの構成
OpenTofuでAzure Application Gateway WAF v2を構成するためには2つのリソースが必要です。
- azurerm_web_application_firewall_policy
- azurerm_application_gateway
WAF v2でゲートウェイを構成する場合は1つ以上のhttp_listener
でfirewall_policy_id
の指定が必要なので何らかのWAFポリシーの作成が必要です。
また、AGICの制御によりリスナー〜バックエンドの設定はすべて書き換わってしまいます が、定義上必須なので、ダミーのbackend_address_pool
backend_http_settings
http_listener
request_routing_rule
を設定します。
当然、lifecycle.ignore_changes
の設定も必須です。
resource "azurerm_web_application_firewall_policy" "default" {
name = "waf-custom-rules-${var.identity}-default"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
policy_settings {
enabled = true
# mode = "Prevention"
mode = "Detection"
}
managed_rules {
managed_rule_set {
type = "Microsoft_DefaultRuleSet"
version = "2.1"
}
}
custom_rules {
name = "AcmeHttp01Challenge"
priority = 10
action = "Allow"
rule_type = "MatchRule"
match_conditions {
operator = "BeginsWith"
transforms = ["Trim", "Lowercase", "RemoveNulls", "UrlDecode"]
match_variables {
variable_name = "RequestUri"
}
match_values = [
"/.well-known/acme-challenge/",
]
}
}
}
サンプルコードではマネージドルールセットとしてMDR 2.1を指定し、カスタムルールとして、ACMEにおけるHTTP01方式のアクセス許可を設定しています。
locals {
backend_address_pool_name = "${var.identity}-beap"
frontend_port_name_http = "${var.identity}-feport-http"
frontend_port_name_https = "${var.identity}-feport-https"
frontend_ip_configuration_name = "${var.identity}-feip"
http_setting_name = "${var.identity}-be-htst"
listener_name = "${var.identity}-httplstn"
request_routing_rule_name = "${var.identity}-rqrt"
}
resource "azurerm_application_gateway" "appgw_waf" {
name = "appgw-${var.identity}"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
sku {
name = "WAF_v2"
tier = "WAF_v2"
capacity = 1
}
enable_http2 = false
fips_enabled = false
force_firewall_policy_association = false
waf_configuration {
enabled = true
firewall_mode = "Prevention"
# 本当はMicrosoft_DefaultRuleSetにしたいがAPIがエラーするのでOWASPを使用
rule_set_type = "OWASP"
rule_set_version = "3.2"
file_upload_limit_mb = "500"
request_body_check = false
}
gateway_ip_configuration {
name = "appgw-ip-conf"
subnet_id = azurerm_subnet.sub_appgw.id
}
frontend_port {
name = local.frontend_port_name_http
port = 80
}
frontend_port {
name = local.frontend_port_name_https
port = 443
}
frontend_ip_configuration {
name = local.frontend_ip_configuration_name
public_ip_address_id = azurerm_public_ip.ipv4.id
}
backend_address_pool {
name = local.backend_address_pool_name
}
backend_http_settings {
name = local.http_setting_name
cookie_based_affinity = "Disabled"
port = 80
protocol = "Http"
request_timeout = 30
pick_host_name_from_backend_address = false
}
http_listener {
name = local.listener_name
frontend_ip_configuration_name = local.frontend_ip_configuration_name
frontend_port_name = local.frontend_port_name_http
protocol = "Http"
require_sni = false
firewall_policy_id = azurerm_web_application_firewall_policy.default.id
}
request_routing_rule {
name = local.request_routing_rule_name
rule_type = "Basic"
http_listener_name = local.listener_name
backend_address_pool_name = local.backend_address_pool_name
backend_http_settings_name = local.http_setting_name
priority = 10010
}
lifecycle {
# デプロイ後はAKS AGICで変更されるのですべて無視
ignore_changes = all
}
}
SKUは当然WAF_v2
です。
全体のWAFルールはMDR 2.1にしたかったのですが、どうもARM APIがエラーしてしまうので、OWASP 3.2にしています。
gateway_ip_configuration
ではあらかじめ構成したAppGW向けサブネットを紐づけます。frontend_port
では80と443を紐づけます。frontend_ip_configuration
でパブリックIPを紐づけます。
backend_address_pool
は名前だけ定義しておきます。
backend_address_pool
backend_http_settings
http_listener
request_routing_rule
はどうせAGICによって書き換えが発生するのでそれっぽければOKです。ただし、http_listener
に限ってはfirewall_policy_id
の設定が必要です。これがないと「WAFv2のSKUにできないよ!」と怒られます。
そして、ignore_changes
で無視設定をちゃんと入れないと、AGICによる変更を壊してしまいますので設定必須です。どこが書き換わるかわからなければall
を設定しておきましょう。
Azure Kubernetes Serviceの構成
resource "azurerm_kubernetes_cluster" "aks" {
name = "aks-${var.identity}"
resource_group_name = azurerm_resource_group.name
location = azurerm_resource_group.rg.location
sku_tier = "Standard"
dns_prefix = "aks-${var.identity}"
automatic_upgrade_channel = "patch"
kubernetes_version = "1.30"
private_cluster_enabled = false
cost_analysis_enabled = "true"
oidc_issuer_enabled = true
workload_identity_enabled = true
azure_active_directory_role_based_access_control {
azure_rbac_enabled = true
tenant_id = var.tenant_id
admin_group_object_ids = var.azrbac_admin_group_object_ids
}
ingress_application_gateway {
gateway_id = azurerm_application_gateway.appgw_waf.id
}
network_profile {
network_plugin = "azure"
network_policy = "calico"
outbound_type = "loadBalancer"
load_balancer_sku = "standard"
}
workload_autoscaler_profile {
keda_enabled = true
}
default_node_pool {
name = "default"
vm_size = "Standard_D4as_v5"
os_sku = "AzureLinux"
temporary_name_for_rotation = "deftmp"
type = "VirtualMachineScaleSets"
auto_scaling_enabled = true
node_count = 1
min_count = 1
max_count = 10
max_pods = 100
vnet_subnet_id = azurerm_subnet.sub_aks_cls.id
zones = [
"1",
"2",
"3",
]
}
lifecycle {
ignore_changes = [
default_node_pool[0].node_count
]
}
}
resource "azurerm_role_assignment" "k8s_rg" {
scope = var.resource_group_id
principal_id = azurerm_kubernetes_cluster.aks.ingress_application_gateway[0].ingress_application_gateway_identity[0].object_id
role_definition_name = "Reader"
depends_on = [azurerm_kubernetes_cluster.aks]
}
resource "azurerm_role_assignment" "k8s_gateway" {
scope = var.appgw_id
principal_id = azurerm_kubernetes_cluster.aks.ingress_application_gateway[0].ingress_application_gateway_identity[0].object_id
role_definition_name = "Contributor"
depends_on = [azurerm_kubernetes_cluster.aks]
}
resource "azurerm_role_assignment" "k8s_gateway_network" {
scope = var.appgw_subnet_id
principal_id = azurerm_kubernetes_cluster.aks.ingress_application_gateway[0].ingress_application_gateway_identity[0].object_id
role_definition_name = "Network Contributor"
depends_on = [azurerm_kubernetes_cluster.aks]
}
resource "azurerm_role_assignment" "k8s_waf_policy_default" {
scope = var.waf_policy_default_id
principal_id = azurerm_kubernetes_cluster.aks.ingress_application_gateway[0].ingress_application_gateway_identity[0].object_id
role_definition_name = "Network Contributor"
depends_on = [azurerm_kubernetes_cluster.aks]
}
ingress_application_gateway.gateway_id
に構成したAppGWのIDを設定することで既存AppGWとの接続が可能です。
しかし!
紐づけただけでは全く動作しません。なぜかというと、権限が噛み合っていないからです。
AKSを構成したあとにAzure Portalを眺めるとわかるのですが、AKSのサービス自体は構成したリソースグループにいるのですが、MC_<aksリソースグループ名>_<AKS名>_<リージョン>
といった名前のリソースグループが生えているかと思います。これはノードリソースグループといい、AKSを構成するコンポーネント(Azure VM Scale Setなど)が入っています。
このリソースグループの中にマネージドIDが生成されているのですが、これがAGICの制御に使用するマネージドIDです。
このマネージドIDがどれなのかというと、azurerm_kubernetes_cluster.<リソース名>.ingress_application_gateway[0].ingress_application_gateway_identity[0]
で参照することができます。
このIDと各コンポーネントのロールを紐づける必要があります。
AGICを動作させるためには、以下のロール紐づけが必要です
対象リソース | 必要なロール | コード上での構成箇所 |
---|---|---|
AppGW所属リソースグループ |
Reader |
azurerm_role_assignment.k8s_rg |
AppGW | Contributor |
azurerm_role_assignment.k8s_gateway |
AppGW使用のサブネット | Network Contributor |
azurerm_role_assignment.k8s_gateway_network |
WAFポリシー | Network Contributor |
azurerm_role_assignment.k8s_waf_policy_default |
ノードリソースグループ所属のマネージドIDなので、AppGWが所属しているリソースグループに対する閲覧者ロールが必要です。
関連コンポーネント(AppGW, サブネット、WAFポリシー)についてはくっつけたり外したりをするためのロールが必要です。
最後に
オフィシャルのドキュメントにはAKSとAppGWを別に作成して紐づける手順は記載されているののですが、ロール紐づけに関するところは以下の文言だけ……
AKS クラスター リソース グループとは異なるリソース グループのアプリケーション ゲートウェイを使う場合は、作成されるマネージド ID ingressapplicationgateway-{AKSNAME} に、アプリケーション ゲートウェイ リソース グループでのネットワーク共同作成者と閲覧者ロールを設定する必要があります。
これはわからないべ……