部署亞馬遜網路服務(AWS)網頁應用程式到 Azure

在本文中,你會將 Yelb 應用程式部署到你在 前一篇文章中建立的 Azure Kubernetes Service (AKS) 叢集。

檢查環境

在部署應用程式前,請使用以下指令確保你的 AKS 叢集已正確配置:

  1. kubectl get namespace 指令列出叢集中的命名空間。

    kubectl get namespace
    

    如果你用應用程式路由外掛安裝 NGINX 入口控制器,你應該會在輸出中看到命名空間:app-routing-system

    NAME                 STATUS   AGE
    app-routing-system   Active   4h28m
    cert-manager         Active   109s
    dapr-system          Active   4h18m
    default              Active   4h29m
    gatekeeper-system    Active   4h28m
    kube-node-lease      Active   4h29m
    kube-public          Active   4h29m
    kube-system          Active   4h29m
    

    如果你透過 Helm 安裝了 NGINX 入口控制器,你應該會在輸出中看到 ingress-basic 命名空間:

    NAME                STATUS   AGE
    cert-manager        Active   7m42s
    dapr-system         Active   11m
    default             Active   21m
    gatekeeper-system   Active   20m
    ingress-basic       Active   7m19s
    kube-node-lease     Active   21m
    kube-public         Active   21m
    kube-system         Active   21m
    prometheus          Active   8m9s
    
  2. 使用 app-routing-system 獲取 ingress-basickubectl get service command 命名空間的服務詳情。

    kubectl get service --namespace <namespace-name> -o wide
    

    如果你用的是應用程式路由外掛,應該會看到EXTERNAL-IPnginx服務是私有 IP 位址。 此位址是您 AKS 叢集中的 kubernetes-internal 私有負載平衡器前端 IP 設定的私有 IP。

    NAME    TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)                                      AGE     SELECTOR
    nginx   LoadBalancer   172.16.55.104   10.240.0.7    80:31447/TCP,443:31772/TCP,10254:30459/TCP   4h28m   app=nginx
    

    如果你用過 Helm,應該會看到 EXTERNAL-IP 服務 nginx-ingress-ingress-nginx-controller 是私人 IP 位址。 這個位址是你 AKS 叢集私有負載平衡器中 kubernetes-internal 前端 IP 設定的私有 IP。

    NAME                                               TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
    nginx-ingress-ingress-nginx-controller             LoadBalancer   172.16.42.152    10.240.0.7    80:32117/TCP,443:32513/TCP   7m31s
    nginx-ingress-ingress-nginx-controller-admission   ClusterIP      172.16.78.85     <none>        443/TCP                      7m31s
    nginx-ingress-ingress-nginx-controller-metrics     ClusterIP      172.16.109.138   <none>        10254/TCP                    7m31s
    

準備部署 Yelb 應用程式

如果您想使用透過 HTTP 在應用程式閘道終止 TLS,並調用 Yelb 的方法來部署範例,您可以在 資料夾中找到 Bash 指令碼和 YAML 範本,以部署 http 應用程式。

如果你想使用使用 Azure 應用程式閘道 的端對端 TLS 架構來部署範例,你可以在 資料夾中找到用於部署網頁應用程式的 Bash 腳本和 YAML 範本。

在本文剩餘部分,我們將引導您使用端對端 TLS 方法部署範例應用程式。

自訂變數

  1. 在執行任何腳本之前,你需要先自訂檔案中 00-variables.sh 變數的值。 此檔案包含於所有腳本中,並包含以下變數:

    # Azure subscription and tenant
    RESOURCE_GROUP_NAME="<aks-resource-group>"
    SUBSCRIPTION_ID="$(az account show --query id --output tsv)"
    SUBSCRIPTION_NAME="$(az account show --query name --output tsv)"
    TENANT_ID="$(az account show --query tenantId --output tsv)"
    AKS_CLUSTER_NAME="<aks-name>"
    AGW_NAME="<application-gateway-name>"
    AGW_PUBLIC_IP_NAME="<application-gateway-public-ip-name>"
    DNS_ZONE_NAME="<your-azure-dns-zone-name-eg-contoso.com>"
    DNS_ZONE_RESOURCE_GROUP_NAME="<your-azure-dns-zone-resource-group-name>"
    DNS_ZONE_SUBSCRIPTION_ID="<your-azure-dns-zone-subscription-id>"
    
    # NGINX ingress controller installed via Helm
    NGINX_NAMESPACE="ingress-basic"
    NGINX_REPO_NAME="ingress-nginx"
    NGINX_REPO_URL="https://kubernetes.github.io/ingress-nginx"
    NGINX_CHART_NAME="ingress-nginx"
    NGINX_RELEASE_NAME="ingress-nginx"
    NGINX_REPLICA_COUNT=3
    
    # Specify the ingress class name for the ingress controller
    # - nginx: Unmanaged NGINX ingress controller installed via Helm
    # - webapprouting.kubernetes.azure.com: Managed NGINX ingress controller installed via AKS application routing add-on
    INGRESS_CLASS_NAME="webapprouting.kubernetes.azure.com"
    
    # Subdomain of the Yelb UI service
    SUBDOMAIN="<yelb-application-subdomain>"
    
    # URL of the Yelb UI service
    URL="https://$SUBDOMAIN.$DNS_ZONE_NAME"
    
    # Secret provider class
    KEY_VAULT_NAME="<key-vault-name>"
    KEY_VAULT_CERTIFICATE_NAME="<key-vault-resource-group-name>"
    KEY_VAULT_SECRET_PROVIDER_IDENTITY_CLIENT_ID="<key-vault-secret-provider-identity-client-id>"
    TLS_SECRET_NAME="yelb-tls-secret"
    NAMESPACE="yelb"
    
  2. 你可以執行以下 az aks show 指令,取得 clientId用戶指派管理身份,該身份由 Azure Key Vault Provider for Secrets Store CSI Driver 使用。 keyVault.bicep模組 金鑰保存庫管理員角色到附加元件的使用者指派受控識別,以讓它能檢索 Kubernetes Ingress 用來透過 NGINX ingress 控制器公開yelb-ui服務的憑證。

    az aks show \
      --name <aks-name> \
      --resource-group <aks-resource-group-name> \
      --query addonProfiles.azureKeyvaultSecretsProvider.identity.clientId \
      --output tsv \
      --only-show-errors
    
  3. 如果你使用本範例提供的Bicep模組部署了Azure基礎設施,你可以繼續部署 Yelb 應用程式。 如果你想將應用程式部署在 AKS 叢集中,可以使用以下腳本來設定你的環境。 您可以使用 02-create-nginx-ingress-controller.sh,在啟用 ModSecurity 開放原始碼 Web 應用程式防火牆 (WAF) 的情況下安裝 NGINX 輸入控制器

    #!/bin/bash
    
    # Variables
    source ./00-variables.sh
    
    # Check if the NGINX ingress controller Helm chart is already installed
    result=$(helm list -n $NGINX_NAMESPACE | grep $NGINX_RELEASE_NAME | awk '{print $1}')
    
    if [[ -n $result ]]; then
      echo "[$NGINX_RELEASE_NAME] NGINX ingress controller release already exists in the [$NGINX_NAMESPACE] namespace"
    else
      # Check if the NGINX ingress controller repository is not already added
      result=$(helm repo list | grep $NGINX_REPO_NAME | awk '{print $1}')
    
      if [[ -n $result ]]; then
        echo "[$NGINX_REPO_NAME] Helm repo already exists"
      else
        # Add the NGINX ingress controller repository
        echo "Adding [$NGINX_REPO_NAME] Helm repo..."
        helm repo add $NGINX_REPO_NAME $NGINX_REPO_URL
      fi
    
      # Update your local Helm chart repository cache
      echo 'Updating Helm repos...'
      helm repo update
    
      # Deploy NGINX ingress controller
      echo "Deploying [$NGINX_RELEASE_NAME] NGINX ingress controller to the [$NGINX_NAMESPACE] namespace..."
      helm install $NGINX_RELEASE_NAME $NGINX_REPO_NAME/$nginxChartName \
        --create-namespace \
        --namespace $NGINX_NAMESPACE \
        --set controller.nodeSelector."kubernetes\.io/os"=linux \
        --set controller.replicaCount=$NGINX_REPLICA_COUNT \
        --set defaultBackend.nodeSelector."kubernetes\.io/os"=linux \
        --set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"=/healthz
    fi
    
    # Get values
    helm get values $NGINX_RELEASE_NAME --namespace $NGINX_NAMESPACE
    

部署應用程式

  1. 執行以下 03-deploy-yelb.sh 腳本部署 Yelb 應用程式及 Kubernetes Ingress 物件,使 yelb-ui 服務能對公開網際網路開放。

    #!/bin/bash
    
    # Variables
    source ./00-variables.sh
    
    # Check if namespace exists in the cluster
    result=$(kubectl get namespace -o jsonpath="{.items[?(@.metadata.name=='$NAMESPACE')].metadata.name}")
    
    if [[ -n $result ]]; then
      echo "$NAMESPACE namespace already exists in the cluster"
    else
      echo "$NAMESPACE namespace does not exist in the cluster"
      echo "creating $NAMESPACE namespace in the cluster..."
      kubectl create namespace $NAMESPACE
    fi
    
    # Create the Secret Provider Class object
    echo "Creating the secret provider class object..."
    cat <<EOF | kubectl apply -f -
    apiVersion: secrets-store.csi.x-k8s.io/v1
    kind: SecretProviderClass
    metadata:
      namespace: $NAMESPACE
      name: yelb
    spec:
      provider: azure
      secretObjects:
        - secretName: $TLS_SECRET_NAME
          type: kubernetes.io/tls
          data: 
            - objectName: $KEY_VAULT_CERTIFICATE_NAME
              key: tls.key
            - objectName: $KEY_VAULT_CERTIFICATE_NAME
              key: tls.crt
      parameters:
        usePodIdentity: "false"
        useVMManagedIdentity: "true"
        userAssignedIdentityID: $KEY_VAULT_SECRET_PROVIDER_IDENTITY_CLIENT_ID
        keyvaultName: $KEY_VAULT_NAME
        objects: |
          array:
            - |
              objectName: $KEY_VAULT_CERTIFICATE_NAME
              objectType: secret
        tenantId: $TENANT_ID
    EOF
    
    # Apply the YAML configuration
    kubectl apply -f yelb.yml
    
    echo "waiting for secret $TLS_SECRET_NAME in namespace $namespace..."
    
    while true; do
      if kubectl get secret -n $NAMESPACE $TLS_SECRET_NAME >/dev/null 2>&1; then
        echo "secret $TLS_SECRET_NAME found!"
        break
      else
        printf "."
        sleep 3
      fi
    done
    
    # Create chat-ingress
    cat ingress.yml |
      yq "(.spec.ingressClassName)|="\""$INGRESS_CLASS_NAME"\" |
      yq "(.spec.tls[0].hosts[0])|="\""$SUBDOMAIN.$DNS_ZONE_NAME"\" |
      yq "(.spec.tls[0].secretName)|="\""$TLS_SECRET_NAME"\" |
      yq "(.spec.rules[0].host)|="\""$SUBDOMAIN.$DNS_ZONE_NAME"\" |
      kubectl apply -f -
    
    # Check the deployed resources within the yelb namespace:
    kubectl get all -n yelb
    
  2. 更新 yelb-ui YAML 資訊,加入 csi volume 定義,並設定 volume mount 將憑證作為秘密從 Azure Key Vault 中讀取。

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: yelb
  name: yelb-ui
spec:
  replicas: 1
  selector:
    matchLabels:
      app: yelb-ui
      tier: frontend
  template:
    metadata:
      labels:
        app: yelb-ui
        tier: frontend
    spec:
      containers:
        - name: yelb-ui
          image: mreferre/yelb-ui:0.7
          ports:
            - containerPort: 80
          volumeMounts:
            - name: secrets-store-inline
              mountPath: "/mnt/secrets-store"
              readOnly: true
      volumes:
        - name: secrets-store-inline
          csi:
            driver: secrets-store.csi.k8s.io
            readOnly: true
            volumeAttributes:
              secretProviderClass: yelb
  1. 你現在可以部署應用程式了。 腳本使用 yelb.yml YAML manifest 來部署應用程式,並 ingress.yml 建立入口物件。 如果你使用 Azure 公共 DNS 區域來解析網域名稱,你可以使用 04-configure-dns.sh 腳本。 此腳本將 NGINX 入口控制器的公開 IP 位址與入口物件所使用的網域關聯,從而暴露該服務。yelb-ui 該指令碼會執行下列步驟:

    1. 取得應用閘道前端 IP 配置所使用的 Azure 公共 IP 的公網位址。
    2. 檢查服務所使用的A子網域是否有yelb-ui紀錄存在。
    3. 如果 A 紀錄不存在,腳本會自動產生。
source ./00-variables.sh

# Get the address of the Application Gateway Public IP
echo "Retrieving the address of the [$AGW_PUBLIC_IP_NAME] public IP address of the [$AGW_NAME] Application Gateway..."
PUBLIC_IP_ADDRESS=$(az network public-ip show \
    --resource-group $RESOURCE_GROUP_NAME \
    --name $AGW_PUBLIC_IP_NAME \
    --query ipAddress \
    --output tsv \
    --only-show-errors)
if [[ -n $PUBLIC_IP_ADDRESS ]]; then
    echo "[$PUBLIC_IP_ADDRESS] public IP address successfully retrieved for the [$AGW_NAME] Application Gateway"
else
    echo "Failed to retrieve the public IP address of the [$AGW_NAME] Application Gateway"
    exit
fi
# Check if an A record for todolist subdomain exists in the DNS Zone
echo "Retrieving the A record for the [$SUBDOMAIN] subdomain from the [$DNS_ZONE_NAME] DNS zone..."
IPV4_ADDRESS=$(az network dns record-set a list \
    --zone-name $DNS_ZONE_NAME \
    --resource-group $DNS_ZONE_RESOURCE_GROUP_NAME \
    --subscription $DNS_ZONE_SUBSCRIPTION_ID \
    --query "[?name=='$SUBDOMAIN'].ARecords[].IPV4_ADDRESS" \
    --output tsv \
    --only-show-errors)
if [[ -n $IPV4_ADDRESS ]]; then
    echo "An A record already exists in [$DNS_ZONE_NAME] DNS zone for the [$SUBDOMAIN] subdomain with [$IPV4_ADDRESS] IP address"
    if [[ $IPV4_ADDRESS == $PUBLIC_IP_ADDRESS ]]; then
        echo "The [$IPV4_ADDRESS] ip address of the existing A record is equal to the ip address of the ingress"
        echo "No additional step is required"
        continue
    else
        echo "The [$IPV4_ADDRESS] ip address of the existing A record is different than the ip address of the ingress"
    fi
    # Retrieving name of the record set relative to the zone
    echo "Retrieving the name of the record set relative to the [$DNS_ZONE_NAME] zone..."
    RECORDSET_NAME=$(az network dns record-set a list \
        --zone-name $DNS_ZONE_NAME \
        --resource-group $DNS_ZONE_RESOURCE_GROUP_NAME \
        --subscription $DNS_ZONE_SUBSCRIPTION_ID \
        --query "[?name=='$SUBDOMAIN'].name" \
        --output tsv \
        --only-show-errors 2>/dev/null)
    if [[ -n $RECORDSET_NAME ]]; then
        echo "[$RECORDSET_NAME] record set name successfully retrieved"
    else
        echo "Failed to retrieve the name of the record set relative to the [$DNS_ZONE_NAME] zone"
        exit
    fi
    # Remove the A record
    echo "Removing the A record from the record set relative to the [$DNS_ZONE_NAME] zone..."
    az network dns record-set a remove-record \
        --ipv4-address $IPV4_ADDRESS \
        --record-set-name $RECORDSET_NAME \
        --zone-name $DNS_ZONE_NAME \
        --resource-group $DNS_ZONE_RESOURCE_GROUP_NAME \
        --subscription $DNS_ZONE_SUBSCRIPTION_ID \
        --only-show-errors 1>/dev/null
    if [[ $? == 0 ]]; then
        echo "[$IPV4_ADDRESS] ip address successfully removed from the [$RECORDSET_NAME] record set"
    else
        echo "Failed to remove the [$IPV4_ADDRESS] ip address from the [$RECORDSET_NAME] record set"
        exit
    fi
fi
# Create the A record
echo "Creating an A record in [$DNS_ZONE_NAME] DNS zone for the [$SUBDOMAIN] subdomain with [$PUBLIC_IP_ADDRESS] IP address..."
az network dns record-set a add-record \
    --zone-name $DNS_ZONE_NAME \
    --resource-group $DNS_ZONE_RESOURCE_GROUP_NAME \
    --subscription $DNS_ZONE_SUBSCRIPTION_ID \
    --record-set-name $SUBDOMAIN \
    --ipv4-address $PUBLIC_IP_ADDRESS \
    --only-show-errors 1>/dev/null
if [[ $? == 0 ]]; then
    echo "A record for the [$SUBDOMAIN] subdomain with [$PUBLIC_IP_ADDRESS] IP address successfully created in [$DNS_ZONE_NAME] DNS zone"
else
    echo "Failed to create an A record for the $SUBDOMAIN subdomain with [$PUBLIC_IP_ADDRESS] IP address in [$DNS_ZONE_NAME] DNS zone"
fi

備註

在部署 Yelb 應用程式並建立 ingress 物件之前,腳本會產生一個 SecretProviderClass,從 Azure Key Vault 取得 TLS 憑證,並產生 ingress 物件的 Kubernetes 秘密。 請務必注意,只有在 中包含 SecretProviderClass 和磁碟區定義時,deployment才會建立包含 TLS 憑證的 Kubernetes 祕密。 為確保 TLS 憑證能正確從 Azure Key Vault 取得並儲存在 ingress 物件所使用的 Kubernetes 秘密中,我們需要對 yelb-ui 部署的 YAML 清單做以下修改:

  • 使用 csi volume 驅動程式新增 secrets-store.csi.k8s.io 定義,該驅動程式參考負責從 Azure Key Vault 取得 TLS 憑證的 SecretProviderClass 物件。
  • 加入 volume mount,以從 Azure Key Vault 以秘密方式讀取憑證。

欲了解更多資訊,請參閱 設定秘密儲存 CSI 驅動程式以啟用帶有 TLS 的 NGINX 入口控制器

測試應用程式

使用 05-call-yelb-ui.sh 腳本呼叫 yelb-ui 服務、模擬 SQL 注入、XSS 攻擊,並觀察 ModSecurity 管理規則集如何阻擋惡意請求。

#!/bin/bash
# Variables
source ./00-variables.sh
# Call REST API
echo "Calling Yelb UI service at $URL..."
curl -w 'HTTP Status: %{http_code}\n' -s -o /dev/null $URL
# Simulate SQL injection
echo "Simulating SQL injection when calling $URL..."
curl -w 'HTTP Status: %{http_code}\n' -s -o /dev/null $URL/?users=ExampleSQLInjection%27%20--
# Simulate XSS
echo "Simulating XSS when calling $URL..."
curl -w 'HTTP Status: %{http_code}\n' -s -o /dev/null $URL/?users=ExampleXSS%3Cscript%3Ealert%28%27XSS%27%29%3C%2Fscript%3E
# A custom rule blocks any request with the word blockme in the querystring.
echo "Simulating query string manipulation with the 'blockme' word in the query string..."
curl -w 'HTTP Status: %{http_code}\n' -s -o /dev/null $URL/?users?task=blockme

Bash 腳本應產生以下輸出,當第一個呼叫成功時,而 ModSecurity 規則會阻擋以下兩個呼叫:

Calling Yelb UI service at https://yelb.contoso.com...
HTTP Status: 200
Simulating SQL injection when calling https://yelb.contoso.com...
HTTP Status: 403
Simulating XSS when calling https://yelb.contoso.com...
HTTP Status: 403
Simulating query string manipulation with the 'blockme' word in the query string...
HTTP Status: 403

監控應用程式

在所提解決方案中,部署流程會自動配置 Azure 應用程式閘道 資源,以收集診斷日誌與指標至 Azure Log Analytics Workspace 工作區。 啟用日誌後,您可以獲得對應用閘道內 Azure Web 應用程式防火牆(WAF) 所執行評估、匹配與封鎖的寶貴見解。 如需詳細資訊,請參閱 應用程式閘道的診斷記錄。 你也可以使用 Log Analytics 來檢視防火牆日誌中的資料。 當你在 Log Analytics 工作區中擁有防火牆日誌後,你可以查看資料、撰寫查詢、建立視覺化,並將它們加入入口網站儀表板。 關於日誌查詢的詳細資訊,請參見 Azure 監視器 中的 日誌查詢概述。

使用 Kusto 查詢探索資料

在所提解決方案中,部署流程會自動將 Azure 應用程式閘道 資源配置為收集診斷日誌與指標,以收集至 Azure Log Analytics 工作空間。 啟用日誌後,您可以深入了解應用閘道內 Azure Web 應用程式防火牆(WAF) 所執行的評估、匹配與阻擋。 如需詳細資訊,請參閱 應用程式閘道的診斷記錄

你也可以使用 Log Analytics 來檢視防火牆日誌中的資料。 當你在 Log Analytics 工作區中擁有防火牆日誌後,你可以查看資料、撰寫查詢、建立視覺化,並將它們加入入口網站儀表板。 欲了解更多日誌查詢資訊,請參閱 Azure 監視器 中日誌查詢概述。

AzureDiagnostics 
| where ResourceProvider == "MICROSOFT.NETWORK" and Category == "ApplicationGatewayFirewallLog"
| limit 10

或者,在處理 資源專屬 資料表時,可以透過以下查詢存取原始防火牆日誌資料。 欲了解更多資源專屬資料表,請參閱 監控資料參考 文件。

AGWFirewallLogs
| limit 10

一旦有了資料,你就可以更深入地製作圖表或視覺化。 以下是一些可使用的 KQL 查詢額外範例:

依 IP 匹配/封鎖的請求

AzureDiagnostics
| where ResourceProvider == "MICROSOFT.NETWORK" and Category == "ApplicationGatewayFirewallLog"
| summarize count() by clientIp_s, bin(TimeGenerated, 1m)
| render timechart

依 URI 匹配/封鎖的請求

AzureDiagnostics
| where ResourceProvider == "MICROSOFT.NETWORK" and Category == "ApplicationGatewayFirewallLog"
| summarize count() by requestUri_s, bin(TimeGenerated, 1m)
| render timechart

最匹配的規則

| where ResourceProvider == "MICROSOFT.NETWORK" and Category == "ApplicationGatewayFirewallLog"
| summarize count() by ruleId_s, bin(TimeGenerated, 1m)
| where count_ > 10
| render timechart

前五個最符合的規則群組

AzureDiagnostics
| where ResourceProvider == "MICROSOFT.NETWORK" and Category == "ApplicationGatewayFirewallLog"
| summarize Count=count() by details_file_s, action_s
| top 5 by Count desc
| render piechart

檢閱已部署的資源

你可以使用 Azure CLI 或 Azure PowerShell 來列出資源群組中已部署的資源。

使用 [az resource list][az-resource-list] 指令列出資源群組中已部署的資源。

az resource list --resource-group <resource-group-name>

當你不再需要本教學中建立的資源時,可以使用 Azure CLI 或 Azure PowerShell 來刪除資源群組。

使用指令 az group delete 刪除資源群組及其相關資源。

az group delete --name <resource-group-name>

下一步

你可以使用 Azure DDoS Protection 以及 Azure 防火牆 來提升解決方案的安全性與威脅防護。 如需詳細資訊,請參閱下列文章:

如果你用 NGINX 入口控制器或其他 AKS 託管的入口控制器代替 Azure 應用程式閘道,你可以用 Azure 防火牆 來檢查 AKS 叢集的流量,保護叢集免受資料外洩及其他不想要的網路流量。 如需詳細資訊,請參閱下列文章:

貢獻者們

Microsoft 維護本文。 下列參與者最初撰寫了這份內容:

主要作者:

其他貢獻者: