在 Azure Kubernetes Service (AKS) 中使用 Azure 防火牆 限制網路流量

本文將教你如何利用 AKS 叢集的出站網路和完全限定網域名稱(FQDN)規則 ,透過 Azure 防火牆控制出口流量。 為了簡化此設定,Azure 防火牆 提供 Azure Kubernetes Service (AzureKubernetesService) FQDN 標籤,限制來自 AKS 叢集的外站流量。

先決條件

  • 一個有有效訂閱的 Azure 帳號。 如果您沒有帳戶,請在開始之前建立 免費帳戶 。
  • 安裝並設定 Terraform。
  • 已安裝 Azure CLI,並使用 az login 登入,以驗證部署。 如果你還沒有 Azure CLI,請安裝它。
  • kubectl 安裝以連接叢集。 如果您使用 Azure Cloud Shell,則 kubectl 已安裝。 要在本地安裝,請使用指令 az aks install-cli 。

防火牆前端 IP 需求

  • 生產環境起點:在 Azure 防火牆 上規劃至少 20 個前端 IP,並根據你的工作負載調整配置,以避免來源網路位址轉換(SNAT)埠口耗盡。
  • 高流量叢集:如果你的叢集會產生許多通往相同目的地的外發連線,可能需要更多前端 IP 或透過 Azure 防火牆 與 NAT 閘道整合來提升 SNAT 容量。
  • API 伺服器保護:將防火牆的公共前端 IP 加入 API 伺服器授權的 IP 範圍 ,以提升安全性
  • 開發者存取:使用授權 IP 範圍時,請使用防火牆虛擬網路(VNet)中的跳接盒,或將開發端點 IP 加入授權範圍

此指引適用於本文所述的整個配置流程。

附註

FQDN 標籤包含 AKS 叢集的出站網路及 FQDN 規則 中列出的所有 FQDN,並會自動更新。

架構概觀

下圖說明使用 Azure 防火牆限制出口流量的 AKS 叢集架構:

鎖定的拓撲

圖示顯示 AKS 代理節點在專用子網中使用 UDR,將出站流量路由至 Azure 防火牆。 對於入站流量,DNAT 規則會將防火牆的公共 IP 位址轉換成公共負載平衡器的前端 IP 位址。

此架構的關鍵組成包括:

  • 公開入侵必須通過防火牆過濾器:
    • AKS 代理節點被隔離在專用子網路中。
    • Azure 防火牆 部署在自己的子網路中。
    • DNAT 規則會將防火牆的公有 IP 轉換成負載平衡器的前端 IP。
  • 外發請求從代理節點開始,透過使用者定義路由(UDR)傳送至 Azure 防火牆 私有 IP:
    • 來自 AKS Agent 節點的要求會遵循放置於 AKS 叢集部署所在子網路上的 UDR。
    • Azure 防火牆 會透過公用 IP 前端從 VNet 傳出。
    • 對公用網際網路或其他 Azure 服務的存取會透過防火牆前端 IP 位址進出。
    • 你可以利用 API 伺服器授權的 IP 範圍來保護對 AKS 控制平面的存取,包括防火牆的公共前端 IP 位址。
  • 內部流量:

設定環境變數

下表列出本文所使用的環境變數。 在繼續前,先在你的 shell 裡設定這些變數,或修改指令讓它使用你自己的數值。

Variable Description 範例值
RESOURCE_GROUP 資源群組的名稱 aks-egress-rg
LOCATION 資源的 Azure 區域 eastus
CLUSTER_NAME AKS 叢集名稱 aks-egress
VNET_NAME 虛擬網路名稱 aks-egress-vnet
AKS_SUBNET_NAME AKS 子網名稱 aks-subnet
FW_SUBNET_NAME Azure 防火牆 子網名稱 AzureFirewallSubnet
FW_NAME Azure 防火牆的名稱 aks-egress-fw
FW_PUBLICIP_NAME Azure 防火牆的公用 IP 名稱 aks-egress-fwpublicip
FW_IPCONFIG_NAME Azure 防火牆的 IP 設定名稱 aks-egress-fwconfig
FW_ROUTE_TABLE_NAME Azure 防火牆 路由表名稱 aks-egress-fwrt
FW_ROUTE_NAME_1 Azure 防火牆 路由名稱 aks-egress-fwrn
FW_ROUTE_NAME_2 Azure 防火牆 的 internet 路由名稱 aks-egress-fwrn-internet

建立資源群組

  • 使用 az group create (部分機器翻譯) 命令建立資源群組。

    az group create --name $RESOURCE_GROUP --location $LOCATION
    

建立具有多個子網路的虛擬網路

將 VNet 配置為兩個獨立子網:一個給叢集,一個給防火牆。 您也可以選擇性建立一個用於內部服務輸入。 下圖說明了部署任何資源前的空網路拓撲:

空白的網路拓撲

圖表顯示在部署資源之前,VNet 已為 AKS 和 Azure 防火牆 分別設有獨立的子網路。

  1. 使用命令 az network vnet create 建立 VNet。

    az network vnet create \
        --resource-group $RESOURCE_GROUP \
        --name $VNET_NAME \
        --location $LOCATION \
        --address-prefixes 10.42.0.0/16 \
        --subnet-name $AKS_SUBNET_NAME \
        --subnet-prefix 10.42.1.0/24
    
  2. 請用指令 az network vnet subnet create 建立 Azure 防火牆的子網路。

    # Dedicated subnet for Azure Firewall (subnet must be named "AzureFirewallSubnet")
    az network vnet subnet create \
        --resource-group $RESOURCE_GROUP \
        --vnet-name $VNET_NAME \
        --name $FW_SUBNET_NAME \
        --address-prefix 10.42.2.0/24
    

為 Azure 防火牆建立公共 IP

  • 使用 az network public-ip create 命令建立 Standard SKU 公用 IP 資源。 將此資源用作 Azure 防火牆 的前端 IP 位址。 本文中的單一 IP 配置僅供測試使用。 在生產環境中,利用 防火牆前端的 IP 需求 來規劃你的工作負載 SNAT 容量。

    az network public-ip create --resource-group $RESOURCE_GROUP --name $FW_PUBLICIP_NAME --location $LOCATION --sku "Standard"
    

安裝 Azure 防火牆 CLI 擴充

建立 Azure 防火牆並啟用 DNS 代理

附註

對於高流量情境,請參見 防火牆前端 IP 需求 章節。

如需了解如何使用多個 IP 建立 Azure 防火牆 的詳細資訊,請參閱使用 Bicep 建立具有多個公用 IP 位址的 Azure 防火牆。

防火牆與 UDR

圖示顯示 Azure 防火牆 部署在其子網路中,並有 UDR 將 AKS 子網路的流量導向防火牆的私人 IP 位址。

  • 建立 Azure 防火牆,並用 az network firewall create 設定 --enable-dns-proxy 為 true的指令啟用 DNS 代理。

    az network firewall create --resource-group $RESOURCE_GROUP --name $FW_NAME --location $LOCATION --enable-dns-proxy true
    

    設定 Azure 防火牆的公開 IP 位址可能需要幾分鐘。 一旦準備好,你可以把 IP 位址指派到防火牆前端。

    附註

    要在網路規則上使用 FQDN,你需要啟用 DNS 代理。 啟用 DNS 代理時,防火牆會在 53 埠監聽,並將 DNS 請求轉發到你指定的 DNS 伺服器。 此設定允許防火牆自動轉譯 FQDN。

為 Azure 防火牆建立 IP 配置

  • 使用 az network firewall ip-config create 命令建立 Azure 防火牆 IP 組態。

    az network firewall ip-config create --resource-group $RESOURCE_GROUP --firewall-name $FW_NAME --name $FW_IPCONFIG_NAME --public-ip-address $FW_PUBLICIP_NAME --vnet-name $VNET_NAME
    

取得 Azure 防火牆 的 IP 位址

  • 請儲存公用與私有防火牆前端 IP 位址,之後使用以下指令進行設定:

    export FW_PUBLIC_IP=$(az network public-ip show --resource-group $RESOURCE_GROUP --name $FW_PUBLICIP_NAME --query "ipAddress" -o tsv)
    export FW_PRIVATE_IP=$(az network firewall show --resource-group $RESOURCE_GROUP --name $FW_NAME --query "ipConfigurations[0].privateIPAddress" -o tsv)
    

    附註

    關於 API 伺服器的安全,請參見 防火牆前端 IP 要求 章節。

設定虛擬網路使用 Azure 防火牆 作為其 DNS 伺服器

  • 透過指令az network vnet update設定虛擬網路,讓其使用 Azure 防火牆 的私人 IP 位址作為 DNS 伺服器。 此配置確保 AKS 節點與 Azure 防火牆 能一致解析網路規則中的目的地 FQDN。

    az network vnet update \
        --resource-group $RESOURCE_GROUP \
        --name $VNET_NAME \
        --dns-servers $FW_PRIVATE_IP
    

AKS 透過 Azure 防火牆 輸出流量的 UDR 要求

Azure 會自動在 Azure 子網、VNet 與本地網路間路由流量。 要修改預設路由,請建立包含以下需求的路由表:

類別 Requirement 值或描述
路由參數 目的地 0.0.0.0/0 (所有流量)
路由參數 下一個躍點類型 網路虛擬設備 (NVA)
路由參數 下一個躍點 IP Azure 防火牆 私有 IP 位址
路由參數 關聯 每個子網路一個路由表(允許 零 個或 一 個)
UDR 限制 預設網際網路路由 0.0.0.0/0 已經存在,但 SNAT 需要公共 IP 位址。
UDR 限制 下一個中繼點 路由必須指向閘道器或 NVA,而非直接指向網際網路。
UDR 限制 AKS 驗證 AKS 會驗證路由設定並防止直接的網際網路路由。
UDR 限制 路由表限制 每個子網最多支援 一個 相關的路由表。
出站類型影響 UDRuserDefinedRouting() 不會為外撥請求建立負載平衡器的公共 IP 位址。
輸出類型影響 負載平衡器公共 IP 位址 僅為使用 LoadBalancer 服務的入站請求建立。
輸出類型影響 SNAT 配置 需要正確設定的公共 IP 位址才能進行出站連線。

如需詳細資訊,請參閱 Azure Load Balancer 的輸出規則。

建立具有躍點至 Azure 防火牆 的路由

  1. 請使用 該 az network route-table create 指令建立一個空路由表。 路由表將 Azure 防火牆定義為下一跳。 每個子網路可以關聯零個或一個路由資料表。

    az network route-table create --resource-group $RESOURCE_GROUP --location $LOCATION --name $FW_ROUTE_TABLE_NAME
    
  2. 使用 az network route-table route create 命令在路由資料表中建立子網路的路由。

    az network route-table route create --resource-group $RESOURCE_GROUP --name $FW_ROUTE_NAME_1 --route-table-name $FW_ROUTE_TABLE_NAME --address-prefix 0.0.0.0/0 --next-hop-type VirtualAppliance --next-hop-ip-address $FW_PRIVATE_IP
    
    az network route-table route create --resource-group $RESOURCE_GROUP --name $FW_ROUTE_NAME_2 --route-table-name $FW_ROUTE_TABLE_NAME --address-prefix $FW_PUBLIC_IP/32 --next-hop-type Internet
    

關於如何覆寫 Azure 預設系統路由或在子網路路由表中新增更多路由,請參閱 虛擬網路路由表文件。

AKS 經由 Azure 防火牆外連所需的必要輸出規則

附註

針對 kube-system 或 gatekeeper-system 命名空間以外、且需要與 API 伺服器通訊的應用程式,除了新增 fqdn-tag 的 AzureKubernetesService 應用程式規則之外,還需要額外的網路規則允許透過 TCP 與 API 伺服器 IP 的 443 連接埠通訊。

以下網路規則允許本文所使用的出口流量:

  • 第一和第二條網路規則允許存取 GitHub 容器登錄檔(ghcr.io)和 Docker Hub(docker.io)拉取容器。

使用 Konnectivity 代理的 AKS 叢集,包括本文中建立的叢集,不要求在 TCP 埠 9000 或 UDP 埠 1194 上進行出站存取。 2021 年 3 月之後配置的節點也不需要在 UDP 123 埠口進行出站 NTP 存取。 關於其他叢集配置及 Azure 雲端所需的規則,請參見 AKS 叢集的出站網路與 FQDN 規則。

在 Azure 防火牆上建立網路規則

  • 請使用以下 az network firewall network-rule create 指令建立網路規則。

    az network firewall network-rule create --resource-group $RESOURCE_GROUP --firewall-name $FW_NAME --collection-name 'aksfwnr' --name 'ghcr' --protocols 'TCP' --source-addresses '*' --destination-fqdns ghcr.io pkg-containers.githubusercontent.com --destination-ports '443' --action allow --priority 100
    
    az network firewall network-rule create --resource-group $RESOURCE_GROUP --firewall-name $FW_NAME --collection-name 'aksfwnr' --name 'docker' --protocols 'TCP' --source-addresses '*' --destination-fqdns docker.io registry-1.docker.io production.cloudflare.docker.com --destination-ports '443'
    

在 Azure 防火牆 建立應用程式規則

  • 使用 az network firewall application-rule create 命令建立應用程式規則。

    az network firewall application-rule create --resource-group $RESOURCE_GROUP --firewall-name $FW_NAME --collection-name 'aksfwar' --name 'fqdn' --source-addresses '*' --protocols 'http=80' 'https=443' --fqdn-tags "AzureKubernetesService" --action allow --priority 100
    

若要深入了解 Azure 防火牆,請參閱 Azure 防火牆 文件。

將路由資料表關聯至 AKS

為了將叢集與防火牆關聯,叢集子網的專用子網路必須參考路由表。

  • 使用 命令 az network vnet subnet update 將路由表關聯到 AKS。

    az network vnet subnet update --resource-group $RESOURCE_GROUP --vnet-name $VNET_NAME --name $AKS_SUBNET_NAME --route-table $FW_ROUTE_TABLE_NAME
    

部署符合輸出規則的 AKS 叢集

你現在可以在現有的 VNet 中部署 AKS 叢集。 你使用 userDefinedRouting 出站類型,確保所有出站流量都必須通過防火牆,且沒有其他出口路徑存在。 你也可以使用 loadBalancer 外部發送類型。

aks-deploy

圖示顯示 AKS 叢集部署於現有 VNet,出站流量透過 Azure 防火牆 透過 UDR 路由。

  • 使用以下指令設定目標子網的子網 ID 環境變數:

    SUBNET_ID=$(az network vnet subnet show --resource-group $RESOURCE_GROUP --vnet-name $VNET_NAME --name $AKS_SUBNET_NAME --query id -o tsv)
    

你定義出站類型,以使用次網中已存在的 UDR。 此配置使 AKS 能跳過負載平衡器的設定與 IP 配置。

秘訣

你可以在叢集部署中加入額外功能,例如 私有叢集。

關於 API 伺服器授權的 IP 範圍設定及開發者存取考量,請參閱 防火牆前端 IP 需求 章節。


建立一個由系統指派的控制平面識別碼的 AKS 叢集

附註

此叢集使用系統指派的控制平面身份。 如果您未指定您自己的 kubelet 受控識別,AKS 就會在節點資源群組中建立使用者指派的 kubelet 識別。

針對使用者定義路由,系統指派受控識別只支援 CNI 網路外掛程式。

  • 使用 az aks create 命令搭配 CNI 網路外掛程式建立使用系統指派受控識別的 AKS 叢集。

    az aks create --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME --location $LOCATION \
        --node-count 3 \
        --network-plugin azure \
        --outbound-type userDefinedRouting \
        --vnet-subnet-id $SUBNET_ID \
        --api-server-authorized-ip-ranges $FW_PUBLIC_IP \
        --generate-ssh-keys
    

建立使用者指派的受管理身份

如果您沒有使用者指派受控識別,請依照本節的步驟操作。 如果您已經有使用者指派受控識別,請略過並前往建立具有使用者指派受控識別的 AKS 叢集。

  1. 使用 az identity create 命令建立受控識別。

    az identity create --name myIdentity --resource-group $RESOURCE_GROUP
    

    輸出應類似下列範例輸出:

     {
       ...
       "id": "/subscriptions/<subscriptionid>/resourcegroups/aks-egress-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity",
       "location": "eastus",
       "name": "myIdentity",
       ...
       "type": "Microsoft.ManagedIdentity/userAssignedIdentities"
     }
    
  2. 使用 az identity create 命令建立 kubelet 受控識別。

    az identity create --name myKubeletIdentity --resource-group $RESOURCE_GROUP
    

    輸出應類似下列範例輸出:

    {
      ...
      "id": "/subscriptions/<subscriptionid>/resourcegroups/aks-egress-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myKubeletIdentity",
      "location": "eastus",
      "name": "myKubeletIdentity",
      ...
      "resourceGroup": "aks-egress-rg",
      ...
      "type": "Microsoft.ManagedIdentity/userAssignedIdentities"
    }
    

附註

如果你自己建立 VNet 和路由表,且資源位於工作節點資源群組之外,CLI 會自動新增角色指派。 如果您使用 ARM 範本或其他方法,您需要使用叢集受控識別的主體 ID 來執行角色指派。

建立一個由使用者指派身份的 AKS 叢集

  • 使用 az aks create 命令,在子網路中使用您現有的使用者指派受控識別建立 AKS 叢集。 提供控制平面的受控識別資源 ID 與 kubelet 身分識別的資源 ID。

    az aks create \
        --resource-group $RESOURCE_GROUP \
        --name $CLUSTER_NAME \
        --location $LOCATION \
        --node-count 3 \
        --network-plugin azure \
        --network-plugin-mode overlay \
        --outbound-type userDefinedRouting \
        --vnet-subnet-id $SUBNET_ID \
        --api-server-authorized-ip-ranges $FW_PUBLIC_IP \
        --assign-identity <identity-resource-id> \
        --assign-kubelet-identity <kubelet-identity-resource-id> \
        --generate-ssh-keys
    

檢視 Terraform 程式碼

Terraform 範例部署了以下資源:

  • 一個有兩個子網的 Azure 虛擬網路:一個給 AKS 叢集節點,另一個專門AzureFirewallSubnet給 Azure 防火牆。
  • 一個標準 SKU 公用 IP 位址,以及一個使用已啟用 DNS Proxy 的Standard的Azure 防火牆( SKU、AZFW_VNet 層)。
  • 一個防火牆政策規則收集群組,包含 AKS 出口所需的網路與應用規則:API 伺服器埠口(TCP/9000、UDP/1194)、NTP(ntp.ubuntu.comUDP/123)、GitHub 容器登錄檔及 Docker Hub 的網路規則,以及在 80 與 443 埠口使用 AzureKubernetesService FQDN 標籤的應用規則。
  • 具有預設路由(0.0.0.0/0)的路由表,其將防火牆的私有 IP 位址設為虛擬設備下一個躍點,並包含一條將防火牆本身的公有 IP 位址直接路由至網際網路的路由,以避免非對稱路由。 路由表僅與 AKS 子網相關聯。
  • 一個 AKS 叢集 ,其採用了系統指派的受控識別、azure 網路外掛程式與 userDefinedRouting 輸出類型,並且將 api_server_access_profile.authorized_ip_ranges 限制為防火牆的公用 IP 位址。
  • 一個 角色指派 ,用於授予 AKS 叢集的受控識別在資源群組層級的 Network Contributor 角色,使叢集能夠管理其所相依的網路資源。

範例不會建立示範工作負載、DNAT 規則或負載平衡服務。 驗證叢集後,使用 kubectl 部署您自己的應用程式,然後依照本文前面所述的指引,如需輸入連線,請在 Azure 防火牆 上新增 DNAT 規則。

  1. 建立一個目錄來測試範例 Terraform 程式碼,並將其設為目前的目錄。

  2. 建立名為 main.tf 的檔案,並插入下列程式碼:

    terraform {
      required_version = ">= 1.5.0"
    
      required_providers {
        azurerm = {
          source  = "hashicorp/azurerm"
          version = "~> 4.0"
        }
      }
    }
    
    provider "azurerm" {
      features {}
    }
    
    variable "prefix" {
      type    = string
      default = "aks-egress"
    }
    
    variable "location" {
      type    = string
      default = "eastus"
    }
    
    variable "node_count" {
      type    = number
      default = 3
    }
    
    variable "vnet_cidr" {
      type    = string
      default = "10.42.0.0/16"
    }
    
    variable "aks_subnet_cidr" {
      type    = string
      default = "10.42.1.0/24"
    }
    
    variable "firewall_subnet_cidr" {
      type    = string
      default = "10.42.2.0/24"
    }
    
    locals {
      resource_group_name    = "${var.prefix}-rg"
      vnet_name              = "${var.prefix}-vnet"
      aks_subnet_name        = "aks-subnet"
      firewall_subnet_name   = "AzureFirewallSubnet"
      firewall_name          = "${var.prefix}-fw"
      firewall_public_ip     = "${var.prefix}-fwpublicip"
      firewall_policy_name   = "${var.prefix}-fwpolicy"
      firewall_ipconfig_name = "${var.prefix}-fwconfig"
      route_table_name       = "${var.prefix}-fwrt"
      default_route_name     = "${var.prefix}-fwrn"
      firewall_public_route  = "${var.prefix}-fwrn-internet"
      aks_name               = var.prefix
    }
    
    resource "azurerm_resource_group" "this" {
      name     = local.resource_group_name
      location = var.location
    }
    
    resource "azurerm_virtual_network" "this" {
      name                = local.vnet_name
      location            = azurerm_resource_group.this.location
      resource_group_name = azurerm_resource_group.this.name
      address_space       = [var.vnet_cidr]
    }
    
    resource "azurerm_subnet" "aks" {
      name                 = local.aks_subnet_name
      resource_group_name  = azurerm_resource_group.this.name
      virtual_network_name = azurerm_virtual_network.this.name
      address_prefixes     = [var.aks_subnet_cidr]
    }
    
    resource "azurerm_subnet" "firewall" {
      name                 = local.firewall_subnet_name
      resource_group_name  = azurerm_resource_group.this.name
      virtual_network_name = azurerm_virtual_network.this.name
      address_prefixes     = [var.firewall_subnet_cidr]
    }
    
    resource "azurerm_public_ip" "firewall" {
      name                = local.firewall_public_ip
      location            = azurerm_resource_group.this.location
      resource_group_name = azurerm_resource_group.this.name
      allocation_method   = "Static"
      sku                 = "Standard"
    }
    
    resource "azurerm_firewall_policy" "this" {
      name                = local.firewall_policy_name
      location            = azurerm_resource_group.this.location
      resource_group_name = azurerm_resource_group.this.name
    
      dns {
        proxy_enabled = true
      }
    }
    
    resource "azurerm_firewall" "this" {
      name                = local.firewall_name
      location            = azurerm_resource_group.this.location
      resource_group_name = azurerm_resource_group.this.name
      sku_name            = "AZFW_VNet"
      sku_tier            = "Standard"
      firewall_policy_id  = azurerm_firewall_policy.this.id
    
      ip_configuration {
        name                 = local.firewall_ipconfig_name
        subnet_id            = azurerm_subnet.firewall.id
        public_ip_address_id = azurerm_public_ip.firewall.id
      }
    }
    
    resource "azurerm_firewall_policy_rule_collection_group" "aks_rules" {
      name               = "aks-egress-rules"
      firewall_policy_id = azurerm_firewall_policy.this.id
      priority           = 100
    
      network_rule_collection {
        name     = "aks-network-rules"
        priority = 100
        action   = "Allow"
    
        rule {
          name                  = "apitcp"
          protocols             = ["TCP"]
          source_addresses      = ["*"]
          destination_addresses = ["AzureCloud.${var.location}"]
          destination_ports     = ["9000"]
        }
    
        rule {
          name                  = "apiudp"
          protocols             = ["UDP"]
          source_addresses      = ["*"]
          destination_addresses = ["AzureCloud.${var.location}"]
          destination_ports     = ["1194"]
        }
    
        rule {
          name              = "time"
          protocols         = ["UDP"]
          source_addresses  = ["*"]
          destination_fqdns = ["ntp.ubuntu.com"]
          destination_ports = ["123"]
        }
    
        rule {
          name             = "ghcr"
          protocols        = ["TCP"]
          source_addresses = ["*"]
          destination_fqdns = [
            "ghcr.io",
            "pkg-containers.githubusercontent.com"
          ]
          destination_ports = ["443"]
        }
    
        rule {
          name             = "docker"
          protocols        = ["TCP"]
          source_addresses = ["*"]
          destination_fqdns = [
            "docker.io",
            "registry-1.docker.io",
            "production.cloudflare.docker.com"
          ]
          destination_ports = ["443"]
        }
      }
    
      application_rule_collection {
        name     = "aks-application-rules"
        priority = 200
        action   = "Allow"
    
        rule {
          name                  = "fqdn"
          source_addresses      = ["*"]
          destination_fqdn_tags = ["AzureKubernetesService"]
    
          protocols {
            type = "Http"
            port = 80
          }
    
          protocols {
            type = "Https"
            port = 443
          }
        }
      }
    }
    
    resource "azurerm_route_table" "this" {
      name                = local.route_table_name
      location            = azurerm_resource_group.this.location
      resource_group_name = azurerm_resource_group.this.name
    }
    
    resource "azurerm_route" "default_to_firewall" {
      name                   = local.default_route_name
      resource_group_name    = azurerm_resource_group.this.name
      route_table_name       = azurerm_route_table.this.name
      address_prefix         = "0.0.0.0/0"
      next_hop_type          = "VirtualAppliance"
      next_hop_in_ip_address = azurerm_firewall.this.ip_configuration[0].private_ip_address
    }
    
    resource "azurerm_route" "firewall_public_ip_to_internet" {
      name                = local.firewall_public_route
      resource_group_name = azurerm_resource_group.this.name
      route_table_name    = azurerm_route_table.this.name
      address_prefix      = "${azurerm_public_ip.firewall.ip_address}/32"
      next_hop_type       = "Internet"
    }
    
    resource "azurerm_subnet_route_table_association" "aks" {
      subnet_id      = azurerm_subnet.aks.id
      route_table_id = azurerm_route_table.this.id
    }
    
    resource "azurerm_kubernetes_cluster" "this" {
      name                = local.aks_name
      location            = azurerm_resource_group.this.location
      resource_group_name = azurerm_resource_group.this.name
      dns_prefix          = "${var.prefix}-dns"
    
      default_node_pool {
        name           = "system"
        vm_size        = "Standard_D2s_v3"
        node_count     = var.node_count
        vnet_subnet_id = azurerm_subnet.aks.id
    
        upgrade_settings {
          max_surge                     = "10%"
          drain_timeout_in_minutes      = 0
          node_soak_duration_in_minutes = 0
        }
      }
    
      identity {
        type = "SystemAssigned"
      }
    
      network_profile {
        network_plugin = "azure"
        outbound_type  = "userDefinedRouting"
      }
    
      api_server_access_profile {
        authorized_ip_ranges = [
          "${azurerm_public_ip.firewall.ip_address}/32"
        ]
      }
    
      depends_on = [
        azurerm_route.default_to_firewall,
        azurerm_subnet_route_table_association.aks,
        azurerm_firewall_policy_rule_collection_group.aks_rules
      ]
    }
    
    resource "azurerm_role_assignment" "network_contributor" {
      scope                = azurerm_resource_group.this.id
      role_definition_name = "Network Contributor"
      principal_id         = azurerm_kubernetes_cluster.this.identity[0].principal_id
    }
    
    output "resource_group_name" {
      value = azurerm_resource_group.this.name
    }
    
    output "aks_cluster_name" {
      value = azurerm_kubernetes_cluster.this.name
    }
    
    output "firewall_public_ip" {
      value = azurerm_public_ip.firewall.ip_address
    }
    
    output "firewall_private_ip" {
      value = azurerm_firewall.this.ip_configuration[0].private_ip_address
    }
    

初始化 Terraform

執行 terraform init 來初始化 Terraform 部署。 此指令會下載管理 Azure 資源所需的 azurerm 提供者。

terraform init -upgrade

格式化並驗證設定

執行 terraform fmt 以格式化設定檔,並 terraform validate 確認設定在語法上是否有效。

terraform fmt
terraform validate

建立 Terraform 執行計畫

執行 terraform plan 以建立執行計畫。

terraform plan -out main.tfplan

套用 Terraform 的執行計畫

執行terraform apply以套用執行計畫到你的 Azure 訂閱。

terraform apply main.tfplan

建立虛擬網路、Azure 防火牆 和 AKS 叢集需要幾分鐘才能完成。

驗證出口、防火牆和路由設定

  1. 從 Terraform 輸出中取得資源群組名稱、叢集名稱和防火牆的公開 IP 位址。

    RESOURCE_GROUP=$(terraform output -raw resource_group_name)
    CLUSTER_NAME=$(terraform output -raw aks_cluster_name)
    FW_PUBLIC_IP=$(terraform output -raw firewall_public_ip)
    
  2. 請使用 userDefinedRouting 命令確認叢集使用 az aks show 出站類型,且僅限於防火牆的公用 IP 位址。

    az aks show --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME --query "{outboundType:networkProfile.outboundType, authorizedIpRanges:apiServerAccessProfile.authorizedIpRanges}"
    
  3. 確認 AKS 子網的路由表將預設流量傳送至防火牆的私人 IP 位址。 用 az network route-table list 指令取得路由表名稱,然後用命令列出路由 az network route-table route list 。

    ROUTE_TABLE_NAME=$(az network route-table list --resource-group $RESOURCE_GROUP --query "[0].name" -o tsv)
    az network route-table route list --resource-group $RESOURCE_GROUP --route-table-name $ROUTE_TABLE_NAME --output table
    
  4. 確認網路和應用程式規則是否已生效。 用指令取得防火牆政策名稱 az network firewall policy list ,然後用指令 az network firewall policy rule-collection-group show 顯示規則收集群組。

    FW_POLICY_NAME=$(az network firewall policy list --resource-group $RESOURCE_GROUP --query "[0].name" -o tsv)
    az network firewall policy rule-collection-group show --resource-group $RESOURCE_GROUP --policy-name $FW_POLICY_NAME --name aks-egress-rules --query "ruleCollections[].name" -o tsv
    

連線至叢集

用指令設定 kubectl 連接你的叢集 az aks get-credentials ,然後用指令 kubectl get nodes 驗證連線。

az aks get-credentials --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME
kubectl get nodes

叢集的出站流量現在會經過 Azure 防火牆。 防火牆政策預設允許所需的 AKS 端點,並封鎖所有其他出站連線。

啟用對 API 伺服器的開發人員存取

如果您在先前步驟中為叢集使用核准的 IP 範圍,您需要將開發人員工具的 IP 位址新增至 AKS 叢集的核准 IP 範圍清單,您才可以從該處存取 API 伺服器。 你也可以在防火牆的 VNet 裡,用獨立子網設定一個帶有所需工具的跳板盒。

  1. 使用下列命令擷取您的 IP 位址:

    CURRENT_IP=$(dig @resolver1.opendns.com ANY myip.opendns.com +short)
    
  2. 使用 az aks update 指令將 IP 位址加入核准範圍。 這個 --api-server-authorized-ip-ranges 選項會取代目前的清單,所以在新增開發者 IP 位址時,請包含防火牆的公開 IP 位址。

    az aks update --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME --api-server-authorized-ip-ranges $FW_PUBLIC_IP,$CURRENT_IP/32
    

連線到 AKS 叢集

  • 使用 kubectl 命令,設定 az aks get-credentials 以連線到您的 AKS 叢集。

    az aks get-credentials --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME
    

在 AKS 上部署公用服務

您現在可以開始對此叢集公開服務並部署應用程式。 這個範例暴露了一個公共服務,但你也可以用 內部負載平衡器來暴露一個內部服務。

公用服務 DNAT

圖中顯示進入的公共流量抵達防火牆的公共 IP 位址,經由 DNAT 規則轉換,並轉發至 AKS 服務的公共負載平衡器前端 IP 位址。

  1. 請參考 AKS Store Demo 快速啟動 清單,了解已部署的元件。

  2. 請使用 kubectl apply 指令部署服務。

    kubectl apply -f https://raw.githubusercontent.com/Azure-Samples/aks-store-demo/main/aks-store-quickstart.yaml
    

取得負載平衡器的公開前端 IP

  1. 用指令 kubectl get services 取得分配給負載平衡器的公開前端 IP 位址。

    kubectl get services
    

    IP 位址應列在 EXTERNAL-IP 欄位中,如下範例輸出所示:

    NAME              TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)              AGE
    kubernetes        ClusterIP      10.0.0.1       <none>        443/TCP              9m10s
    order-service     ClusterIP      10.0.104.144   <none>        3000/TCP             11s
    product-service   ClusterIP      10.0.237.60    <none>        3002/TCP             10s
    rabbitmq          ClusterIP      10.0.161.128   <none>        5672/TCP,15672/TCP   11s
    store-front       LoadBalancer   10.0.89.139    20.39.18.6    80:32271/TCP         10s
    
  2. 請使用 kubectl get svc store-front 指令儲存公共負載平衡器的前端 IP 位址。

    SERVICE_IP=$(kubectl get svc store-front -o jsonpath='{.status.loadBalancer.ingress[*].ip}')
    

在 Azure 防火牆上建立 DNAT 規則

重要事項

當你使用 Azure 防火牆 限制出口流量並建立 UDR 強制所有出口流量時,請確保在 Azure 防火牆 中建立適當的 DNAT 規則,才能正確允許進入流量。 將 Azure 防火牆 與 UDR 搭配使用會因為非對稱路由而破壞輸入設定。 如果 AKS 子網路具有會前往防火牆私用 IP 位址的預設路由,但您使用公用負載平衡器 (輸入或 loadBalancer 服務類型的 Kubernetes 服務),就會發生此問題。 在此案例中,傳入的負載平衡器流量會透過其公用 IP 位址接收,但回傳路徑會經由防火牆的私用 IP 位址。 因為 Azure 防火牆 是有狀態的,它會丟棄回傳封包,因為 Azure 防火牆 不知道已建立的會話。 若要了解如何將 Azure 防火牆 與您的輸入或服務負載平衡器整合,請參閱將 Azure 防火牆 與 Azure Standard Load Balancer 整合。

要設定入站連線,請在 Azure 防火牆 建立 DNAT 規則。 為了測試與叢集的連線,規則會將防火牆前端的公網 IP 位址轉換成儲存在 $SERVICE_IP的公用負載平衡器前端 IP 位址。 你可以自訂目的地地址。 轉譯後的連接埠必須是 Kubernetes 服務所公開的連接埠。 若拓撲改用私人前端 IP 位址,請設定 內部負載平衡器,並使用其私人 IP 位址作為轉譯位址。

  • 使用 az network firewall nat-rule create 命令新增 NAT 規則。

    az network firewall nat-rule create --collection-name exampleset --destination-addresses $FW_PUBLIC_IP --destination-ports 80 --firewall-name $FW_NAME --name inboundrule --protocols Any --resource-group $RESOURCE_GROUP --source-addresses '*' --translated-port 80 --action Dnat --priority 100 --translated-address $SERVICE_IP
    

驗證連線能力

  • 在瀏覽器中瀏覽至 Azure 防火牆 前端 IP 位址以驗證連線能力。 您應該會看到 AKS 商店應用程式。 在此範例中,防火牆的公共 IP 為 52.253.228.132:

    顯示在本機瀏覽器中開啟 Azure Store Front App 的螢幕擷取畫面。

    在此頁面上,您可以檢視產品、將產品加入購物車,然後下單。

清除資源

如果您不再需要本文中創建的資源,可以刪除它們以避免未來產生費用。

  • 請使用 指令 az group delete 刪除 AKS 資源群組。

    az group delete --name $RESOURCE_GROUP
    

Azure 防火牆 會以每小時為單位計費,不論其是否正在主動處理流量,此外還會收取資料處理費用。 不要讓防火牆(或本範例)運行超過所需時間。

Warning

如果你在同一工作目錄中使用現有或生產資源,請在銷毀前仔細檢視執行計畫。 執行terraform destroy會移除 Terraform 狀態檔案中追蹤的所有資源,包括虛擬網路、子網路、Azure 防火牆、防火牆政策與規則、路由表、AKS 叢集,以及角色指派。 除非你確定可以安全地將它移除,否則請避免在共用基礎設施上執行它。

使用 -destroy 旗標執行 terraform plan,以查看 Terraform 會移除哪些內容。

terraform plan -destroy -out main.destroy.tfplan

執行terraform apply以套用銷毀計畫,並移除本範例所建立的所有資源,包括 Azure 防火牆。

terraform apply main.destroy.tfplan