你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

将 Amazon Web Services (AWS) Web 应用部署到 Azure

在本文中,你将 Yelb 应用部署到你在上一篇文章中创建的 Azure Kubernetes 服务 (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-IP 服务的 nginx 视作一个专用 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 应用

如果要使用位于应用程序网关的 TLS 终止以及通过 HTTP 的 Yelb 调用方法部署示例,可以查找 Bash 脚本和 YAML 模板在 文件中部署 http 应用。

如果要使用使用 Azure 应用程序网关实现端到端 TLS 体系结构部署示例,可以查找 Bash 脚本和 YAML 模板在 https 文件中部署 Web 应用。

在本文剩余部分,我们将引导你完成使用端到端 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使用的用户分配的托管标识keyVault.bicep 模块密钥保管库管理员角色到附加项的用户分配托管标识,让它检索用于通过 NGINX 入口控制器公开 yelb-ui 服务的 Kubernetes 入口使用的证书。

    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 安装 NGINX 入口控制器,并启用 ModSecurity 开源 Web 应用程序防火墙 (WAF)。

    #!/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 入口对象,让 yelb-ui 服务可以访问公共 Internet。

    #!/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 密钥保管库的密钥。

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 清单部署应用和 ingress.yml,以创建入口对象。 如果域名解析使用的是 Azure 公共 DNS 区域,可以部署 04-configure-dns.sh 脚本。 此脚本可将 NGINX 入口控制器的公共 IP 地址与入口对象(会公开 yelb-ui 服务)使用的域进行关联。 此脚本会执行以下步骤:

    1. 检索应用程序网关的前端 IP 配置使用的 Azure 公共 IP 的公共 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 密钥保管库中的 TLS 证书,并为 ingress 对象生成 Kubernetes 密钥。 注意事项:密钥保管库的密钥存储 CSI 驱动程序仅在 SecretProviderClass 中包含 deployment 和卷定义时,才会创建包含 TLS 密钥的 Kubernetes 密钥。 为确保从 Azure 密钥保管库中正确检索到 TLS 证书并将其存储在 ingress 对象使用的 Kubernetes 密钥中,需要对 yelb-ui 部署的 YAML 清单进行以下修改:

  • 使用 csi volume 驱动程序添加 secrets-store.csi.k8s.io 定义,引用负责从 Azure 密钥保管库中检索 TLS 证书的 SecretProviderClass 对象。
  • 包括 volume mount 以将证书读取为 Azure 密钥保管库中的密钥。

有关详细信息,请参阅设置机密存储 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 工作区工作区。 通过启用日志,可以获得有价值的洞察,深入了解应用程序网关内 Azure Web 应用程序防火墙 (WAF) 执行的评估、匹配和阻止。 有关详细信息,请参阅应用程序网关的诊断日志。 还可以使用日志分析检查防火墙日志里的数据。 当 Log Analytics 工作区中有防火墙日志时,你可以查看数据、编写查询、创建可视化效果,并将这些内容添加到门户仪表板。 有关日志查询的详细信息,请参阅 Azure Monitor 中的日志查询概述

使用 Kusto 查询深入挖掘数据

在提议的解决方案中,部署流程会自动将 Azure 应用程序网关资源配置为将诊断日志和指南收集到 Azure Log Analytics 工作区。 通过启用日志,可以获得洞察,深入了解应用程序网关内 Azure Web 应用程序防火墙 (WAF) 执行的评估、匹配和阻止。 有关详细信息,请参阅应用程序网关的诊断日志

还可以使用日志分析检查防火墙日志里的数据。 当 Log Analytics 工作区中有防火墙日志时,你可以查看数据、编写查询、创建可视化效果,并将这些内容添加到门户仪表板。 有关日志查询的详细信息,请参阅 Azure Monitor 中的日志查询概述

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 防护Azure 防火墙增强解决方案的安全与威胁防护等级。 有关详细信息,请参阅以下文章:

如果在 Azure 应用程序网关中使用 NGINX 入口控制器或任何其他 AKS 托管的入口控制器,则可以使用 Azure 防火墙检测进出 AKS 群集的流量,保护群集不发生数据外泄以及其他不需要的网络流量。 有关详细信息,请参阅以下文章:

供稿人

Microsoft维护本文。 本系列文章为以下参与者的原创作品:

主要作者:

其他参与者: