通过


你当前正在访问 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 命令来检索密钥存储 CSI 驱动程序的 Azure 密钥保管库 提供程序使用的用户分配的托管标识clientIdkeyVault.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 的公共地址。
    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 机密。 请务必注意,Secrets Store CSI Driver for 密钥保管库 只有在 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 Workspace 工作区。 通过启用日志,您可以深入了解应用程序网关中由 Azure Web 应用程序防火墙(WAF)执行的重要评估、匹配和阻止操作。 有关详细信息,请参阅应用程序网关的诊断日志。 还可以使用Log Analytics来检查防火墙日志中的数据。 在Log Analytics工作区中有防火墙日志时,可以查看数据、写入查询、创建可视化效果并将其添加到门户仪表板。 有关日志查询的详细信息,请参阅 Azure Monitor 日志查询概述。

使用 Kusto 查询深入挖掘数据

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

还可以使用Log Analytics来检查防火墙日志中的数据。 在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 防火墙 提高解决方案的安全性和威胁防护。 有关详细信息,请参阅以下文章:

如果使用 NGINX 入口控制器或任何其他 AKS 托管入口控制器来代替Azure 应用程序网关,则可以使用 Azure 防火墙检查传入和传出 AKS 群集的流量,并保护群集免受数据外泄和其他不需要的网络流量。 有关详细信息,请参阅以下文章:

供稿人

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

主要作者:

其他参与者: