Hace unas semanas compartí contigo un escenario simplificado sobre cómo exponer tus APIs en AKS a través de API Management. Sin embargo, es habitual que sea Application Gateway la pata externa en la arquitectura y que API Management quede configurado como interno dentro de la red virtual, al igual que las APIs que se hospedan en AKS.

En este artículo quiero mostrarte el paso a paso para probarlo.
Variables de entorno necesarias
Antes de crear ningún recurso, he definido las siguientes variables para que sean fácilmente modificables a tu gusto:
CUSTOM_DOMAIN="thedev.es"
RESOURCE_GROUP="waf-apim-aks-${CUSTOM_DOMAIN//./}"
LOCATION="uksouth"
PRIVATE_DNS_ZONE_NAME="apis.internal"
VNET_NAME="aks-and-apim-vnet"
AKS_SUBNET_NAME="aks-subnet"
APIM_SUBNET_NAME="apim-subnet"
AKS_NAME="aks-cluster"
APIM_NAME="apim-demo-${CUSTOM_DOMAIN//./}"
APP_GW_NAME="app-gw"
APP_GW_PUBLIC_IP_NAME="app-gw-ip"
APP_GW_SUBNET_NAME="app-gw-subnet"
STORAGE_ACCOUNT_NAME="${APIM_NAME//-/}"
APP_GW_PUBLIC_IP_DNS_NAME="${CUSTOM_DOMAIN//./}"
# Colors for the output
RED='\033[0;31m'
# Highlight text with yellow background
HIGHLIGHT='\033[0;30;43m'
NC='\033[0m'
echo -e "${HIGHLIGHT}Variables set 👍${NC}"
Este script es un resumen de todo lo que vamos a necesitar para esta prueba de concepto: un dominio propio, un grupo de recursos, una zona privada para ExternalDNS, una red virtual donde vivirán nuestros servicios, una instancia de API Management, una private DNS configurado con el custom domain, un Application Gateway y una cuenta de almacenamiento. Todos los scripts que te muestro en este artículo están en mi repositorio de GitHub, y en el archivo steps.sh en qué orden deben de ser lanzados.
Crear grupo de recursos, red virtual y Azure private DNS para las APIs en AKS
Lo primero que necesitas es un grupo de recursos donde hospedar todos esto 😙, una red donde conectar los mismos y un DNS privado que le permita a API Management llegar a las APIs con un nombre más amigable que una IP interna, gracias a ExternalDNS.
echo -e "${HIGHLIGHT}Creating resource group ${RESOURCE_GROUP} in ${LOCATION}...${NC}"
RESOURCE_GROUP_ID=$(az group create --name ${RESOURCE_GROUP} --location ${LOCATION} --query id -o tsv)
echo -e "${HIGHLIGHT}Creating vnet ${VNET_NAME} in ${RESOURCE_GROUP}...${NC}"
az network vnet create \
--resource-group ${RESOURCE_GROUP} \
--name ${VNET_NAME} \
--address-prefixes 192.168.0.0/16 \
--subnet-name ${AKS_SUBNET_NAME} \
--subnet-prefix 192.168.1.0/24
echo -e "${HIGHLIGHT}Creating apim subnet ${APIM_SUBNET_NAME} in ${RESOURCE_GROUP}...${NC}"
# But https://learn.microsoft.com/en-us/azure/api-management/virtual-network-concepts?tabs=stv2#examples
az network vnet subnet create \
--resource-group ${RESOURCE_GROUP} \
--vnet-name ${VNET_NAME} \
--name ${APIM_SUBNET_NAME} \
--address-prefixes 192.168.2.0/28
echo -e "${HIGHLIGHT}Creating App Gw subnet ${APP_GW_SUBNET_NAME} in ${RESOURCE_GROUP}...${NC}"
az network vnet subnet create \
--resource-group ${RESOURCE_GROUP} \
--vnet-name ${VNET_NAME} \
--name ${APP_GW_SUBNET_NAME} \
--address-prefixes 192.168.3.0/28
echo -e "${HIGHLIGHT}Create private DNS zone $PRIVATE_DNS_ZONE_NAME...${NC}"
PRIVATE_DNS_ZONE_ID=$(az network private-dns zone create -g $RESOURCE_GROUP -n $PRIVATE_DNS_ZONE_NAME --query id -o tsv)
sleep 10
# Link the private DNS zone to the virtual network
echo -e "${HIGHLIGHT}Link the private DNS zone to the virtual network...${NC}"
az network private-dns link vnet create \
--resource-group $RESOURCE_GROUP \
--zone-name $PRIVATE_DNS_ZONE_NAME \
--name $PRIVATE_DNS_ZONE_NAME \
--virtual-network $VNET_NAME \
--registration-enabled false
Desplegar un clúster de AKS y ExternalDNS
El objetivo principal de esta prueba de concepto es que las APIs residan en un clúster de AKS y que estas no sean directamente expuestas al exterior sin ningún tipo de protección. En esta prueba de concepto despliego un clúster básico:
echo -e "${HIGHLIGHT}Creating an identity for AKS...${NC}"
az identity create \
--resource-group ${RESOURCE_GROUP} \
--name ${AKS_NAME}-identity
echo -e "${HIGHLIGHT}Waiting 60 seconds for the identity...${NC}"
sleep 60
IDENTITY_ID=$(az identity show --name $AKS_NAME-identity --resource-group $RESOURCE_GROUP --query id -o tsv)
IDENTITY_CLIENT_ID=$(az identity show --name $AKS_NAME-identity --resource-group $RESOURCE_GROUP --query clientId -o tsv)
# Get VNET id
VNET_ID=$(az network vnet show --resource-group $RESOURCE_GROUP --name $VNET_NAME --query id -o tsv)
# Assign Network Contributor role to the user identity
echo -e "${HIGHLIGHT}Assign roles to the identity...${NC}"
az role assignment create --assignee $IDENTITY_CLIENT_ID --scope $VNET_ID --role "Network Contributor"
# Permission granted to your cluster's managed identity used by Azure may take up 60 minutes to populate.
# Get roles assigned to the user identity
az role assignment list --assignee $IDENTITY_CLIENT_ID --all -o table
AKS_SUBNET_ID=$(az network vnet subnet show --resource-group $RESOURCE_GROUP --vnet-name $VNET_NAME --name $AKS_SUBNET_NAME --query id -o tsv)
echo -e "${HIGHLIGHT}Creating AKS cluster ${AKS_NAME} in ${RESOURCE_GROUP}...${NC}"
time az aks create \
--resource-group ${RESOURCE_GROUP} \
--name ${AKS_NAME} \
--node-vm-size Standard_B4ms \
--enable-managed-identity \
--vnet-subnet-id $AKS_SUBNET_ID \
--assign-identity $IDENTITY_ID \
--enable-addons monitoring \
--generate-ssh-keys
echo -e "${HIGHLIGHT}Getting AKS credentials...${NC}"
az aks get-credentials --resource-group ${RESOURCE_GROUP} --name ${AKS_NAME} --overwrite-existing
echo -e "${HIGHLIGHT}Fetching the kubelet identity...${NC}"
KUBELET_IDENTITY_CLIENT_ID=$(az aks show --resource-group $RESOURCE_GROUP --name $AKS_NAME --query "identityProfile.kubeletidentity.objectId" --output tsv)
echo -e "${HIGHLIGHT}Assign the 'Reader' and 'DNS Zone Contributor' role...${NC}"
az role assignment create --role "Reader" --assignee $KUBELET_IDENTITY_CLIENT_ID --scope $RESOURCE_GROUP_ID
az role assignment create --role "Private DNS Zone Contributor" --assignee $KUBELET_IDENTITY_CLIENT_ID --scope $PRIVATE_DNS_ZONE_ID
echo -e "${HIGHLIGHT}Create a configuration file for the identity...${NC}"
cat <<EOF > azure.json
{
"tenantId": "$(az account show --query tenantId -o tsv)",
"subscriptionId": "$(az account show --query id -o tsv)",
"resourceGroup": "$RESOURCE_GROUP",
"useManagedIdentityExtension": true
}
EOF
echo -e "${HIGHLIGHT}Create a secret for the identity...${NC}"
kubectl create secret generic azure-config-file --from-file=azure.json
echo -e "${HIGHLIGHT}Deploy ExternalDNS for Azure Private DNS...${NC}"
kubectl apply -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns-private
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services","endpoints","pods", "nodes"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer-private
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns-private
namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns-private
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns-private
template:
metadata:
labels:
app: external-dns-private
spec:
serviceAccountName: external-dns-private
containers:
- name: external-dns-private
image: registry.k8s.io/external-dns/external-dns:v0.13.4
args:
- --source=service
- --source=ingress
# - --domain-filter=$PUBLIC_DNS_ZONE_NAME # (optional)
- --provider=azure-private-dns
- --azure-resource-group=$RESOURCE_GROUP # (optional)
- --txt-prefix=externaldns-
- --publish-internal-services
volumeMounts:
- name: azure-config-file
mountPath: /etc/kubernetes
readOnly: true
volumes:
- name: azure-config-file
secret:
secretName: azure-config-file
EOF
# Wait for external-dns to be ready
kubectl wait --for=condition=available --timeout=600s deployment/external-dns-private
echo -e "${HIGHLIGHT}ExternalDNS deployed!${NC}"
Cuanto termina la creación del clúster despliego en él ExternalDNS para que sea consciente de los servicios e ingress que iré configurando más adelante.
Crear una instancia de API Management
Ahora que ya tienes un entorno donde vivirán tus APIs, lo siguiente es un servicio que nos permita orquestarlas, como es API Management:
echo "${HIGHLIGHT} Creating API Management instance at $(date)...${NC}"
time az apim create \
--resource-group ${RESOURCE_GROUP} \
--name ${APIM_NAME} \
--location ${LOCATION} \
--publisher-email "your@email.com" \
--publisher-name "return(GiS);" \
--sku-name Developer
echo -e "${HIGHLIGHT} API Management instance created${NC}"
echo -e "${HIGHLIGHT} Create private DNS zone ${CUSTOM_DOMAIN} to resolve the custom domain internally...${NC}"
az network private-dns zone create -g $RESOURCE_GROUP -n $CUSTOM_DOMAIN
echo -e "${HIGHLIGHT} Link the private DNS zone to the virtual network...${NC}"
az network private-dns link vnet create \
--resource-group $RESOURCE_GROUP \
--zone-name $CUSTOM_DOMAIN \
--name $CUSTOM_DOMAIN-link \
--virtual-network $VNET_NAME \
--registration-enabled false
echo -e "${HIGHLIGHT}Done 👍🏻${NC}"
El proceso de creación de API Management puede durar más de una hora, dependiendo de la región de Azure que elijas. Una vez se complete, necesitamos crear además un Private DNS para el dominio que vamos a utilizar. Esto es así porque necesitamos que Application Gateway sepa resolver el dominio de API Management de manera interna, esto es recibiendo la IP privada que se le ha asignado dentro de la red. Sin embargo, cuando queramos acceder a API Management a través de Application Gateway de forma pública necesitamos que el dominio a su vez esté configurado para devolver la IP pública del Application Gateway como veremos más adelante.
Configurar los custom domains para API Management
Una vez que el servicio se ha desplegado, ya puedes configurar los custom domains que hayas elegido. Durante la configuración debes asociar los diferentes endpoints a un certificado para lo que, en mi caso, he utilizado Certbot para que me ayude con Let’s Encrypt:
# Endpoints for custom domains
# There are several API Management endpoints to which you can assign a custom domain name. Currently, the following endpoints are available:
# Endpoint Default
# Gateway Default is: <apim-service-name>.azure-api.net. Gateway is the only endpoint available for configuration in the Consumption tier.
# The default Gateway endpoint configuration remains available after a custom Gateway domain is added.
# Developer portal (legacy) Default is: <apim-service-name>.portal.azure-api.net
# Developer portal Default is: <apim-service-name>.developer.azure-api.net
# Management Default is: <apim-service-name>.management.azure-api.net
# Configuration API (v2) Default is: <apim-service-name>.configuration.azure-api.net
# SCM Default is: <apim-service-name>.scm.azure-api.net
# https://stackoverflow.com/questions/64851783/azure-cli-bash-script-to-set-up-api-management-custom-domain
# Create certificates using certbot
# Install certbot
brew install certbot
# Create certificate
CERT_PASSWORD="1234"
EMAIL="giselatb@outlook.com"
LETSENCRYPT_URL="https://acme-v02.api.letsencrypt.org/directory"
echo -e "${HIGHLIGHT} Request certificate for management.$CUSTOM_DOMAIN ${NC}"
sudo certbot certonly \
--manual --preferred-challenges dns \
--email $EMAIL --server $LETSENCRYPT_URL \
--agree-tos -d management.$CUSTOM_DOMAIN
echo -e "${HIGHLIGHT} Export certificate for management.$CUSTOM_DOMAIN ${NC}"
sudo openssl pkcs12 -export -out ./management.$CUSTOM_DOMAIN.pfx \
-inkey /etc/letsencrypt/live/management.$CUSTOM_DOMAIN/privkey.pem \
-in /etc/letsencrypt/live/management.$CUSTOM_DOMAIN/fullchain.pem \
-passout pass:$CERT_PASSWORD
echo -e "${HIGHLIGHT} Request certificate for api.$CUSTOM_DOMAIN ${NC}"
sudo certbot certonly \
--manual --preferred-challenges dns \
--email $EMAIL --server $LETSENCRYPT_URL \
--agree-tos -d api.$CUSTOM_DOMAIN
echo -e "${HIGHLIGHT} Export certificate for api.$CUSTOM_DOMAIN ${NC}"
sudo openssl pkcs12 -export -out ./api.$CUSTOM_DOMAIN.pfx \
-inkey /etc/letsencrypt/live/api.$CUSTOM_DOMAIN/privkey.pem \
-in /etc/letsencrypt/live/api.$CUSTOM_DOMAIN/fullchain.pem \
-passout pass:$CERT_PASSWORD
echo -e "${HIGHLIGHT} Request certificate for portal.$CUSTOM_DOMAIN ${NC}"
sudo certbot certonly \
--manual --preferred-challenges dns \
--email $EMAIL --server $LETSENCRYPT_URL \
--agree-tos -d portal.$CUSTOM_DOMAIN
echo -e "${HIGHLIGHT} Export certificate for portal.$CUSTOM_DOMAIN ${NC}"
sudo openssl pkcs12 -export -out ./portal.$CUSTOM_DOMAIN.pfx \
-inkey /etc/letsencrypt/live/portal.$CUSTOM_DOMAIN/privkey.pem \
-in /etc/letsencrypt/live/portal.$CUSTOM_DOMAIN/fullchain.pem \
-passout pass:$CERT_PASSWORD
API_ENCODED_CERT_DATA=$(sudo base64 -i ./api.$CUSTOM_DOMAIN.pfx)
PORTAL_ENCODED_CERT_DATA=$(sudo base64 -i ./portal.$CUSTOM_DOMAIN.pfx)
MANAGEMENT_ENCODED_CERT_DATA=$(sudo base64 -i ./management.$CUSTOM_DOMAIN.pfx)
# Settings for custom domains
hostnamesConfiguration="[
{
\"hostName\": \"api.$CUSTOM_DOMAIN\",
\"type\": \"Proxy\",
\"encodedCertificate\": \"$API_ENCODED_CERT_DATA\",
\"certificatePassword\": \"$CERT_PASSWORD\"
},
{
\"hostName\": \"portal.$CUSTOM_DOMAIN\",
\"type\": \"DeveloperPortal\",
\"encodedCertificate\": \"$PORTAL_ENCODED_CERT_DATA\",
\"certificatePassword\": \"$CERT_PASSWORD\"
},
{
\"hostName\": \"management.$CUSTOM_DOMAIN\",
\"type\": \"Management\",
\"encodedCertificate\": \"$MANAGEMENT_ENCODED_CERT_DATA\",
\"certificatePassword\": \"$CERT_PASSWORD\"
}
]"
echo -e "${HIGHLIGHT} Configure custom domains${NC}"
az apim update \
--resource-group $RESOURCE_GROUP \
--name $APIM_NAME \
--set hostnameConfigurations=$hostnamesConfiguration
# Get custom domains configured for API Management
az apim show --name $APIM_NAME --resource-group $RESOURCE_GROUP --query "hostnameConfigurations" -o table
Este proceso durará también varios minutos hasta que se complete. El último comando muestra la configuración obtenida.
Conectar API Management de manera interna a la red virtual
La última configuración que debemos hacer para API Management, a nivel de infraestructura, es conectarlo a la red virtual en el modo Internal, lo cual significa que se limitará el acceso al gateway y al portal a esta red:
echo -e "${HIGHLIGHT} Connecting API Management to the virtual network ${NC}"
# Get API Management resource id
APIM_ID=$(az apim show -n ${APIM_NAME} -g ${RESOURCE_GROUP} --query "id")
APIM_SUBNET_ID=$(az network vnet subnet show -g ${RESOURCE_GROUP} -n $APIM_SUBNET_NAME --vnet-name $VNET_NAME --query "id")
# Create a public IP for APIM
# With an internal virtual network, the public IP address is used only for management operations. Learn more about IP addresses of API Management. (https://learn.microsoft.com/en-us/azure/api-management/api-management-using-with-vnet?tabs=stv2#prerequisites)
echo -e "${HIGHLIGHT} Create a public IP for APIM ${NC}"
# When creating a public IP address resource, ensure you assign a DNS name label to it. The label you choose to use does not matter but a label is required if this resource will be assigned to an API Management service.
APIM_PUBLIC_IP_ID=$(az network public-ip create \
-g ${RESOURCE_GROUP} \
-n ${APIM_NAME}-pip \
--dns-name ${APIM_NAME} \
--sku Standard \
--query "publicIp.id" -o tsv)
# You must configure NSG group for the APIM subnet
echo -e "${HIGHLIGHT} You must configure NSG group for the APIM subnet ${NC}"
# Create security group
echo -e "${HIGHLIGHT} Create security group ${NC}"
APIM_NSG_NAME="${APIM_NAME}-nsg"
az network nsg create -g ${RESOURCE_GROUP} -n $APIM_NSG_NAME
# Create security group rules (https://learn.microsoft.com/en-us/azure/api-management/api-management-using-with-internal-vnet?tabs=stv2#configure-nsg-rules)
echo -e "${HIGHLIGHT} Create security group rule ${NC}"
az network nsg rule create \
-g ${RESOURCE_GROUP} \
--nsg-name $APIM_NSG_NAME \
--name ManagementEndpointForAzurePortalAndPowerShell \
--priority 100 \
--direction Inbound \
--source-address-prefixes '*' \
--source-port-ranges '*' \
--destination-address-prefixes '*' \
--destination-port-ranges 3443 \
--access Allow \
--protocol Tcp \
--description "Management endpoint for Azure portal and PowerShell"
az network nsg rule create \
-g ${RESOURCE_GROUP} \
--nsg-name $APIM_NSG_NAME \
--name AzureInfrastructureLoadBalancer \
--priority 101 \
--direction Inbound \
--source-address-prefixes '*' \
--source-port-ranges '*' \
--destination-address-prefixes '*' \
--destination-port-ranges 6390 \
--access Allow \
--protocol Tcp \
--description "Azure Infrastructure Load Balancer"
az network nsg rule create \
-g ${RESOURCE_GROUP} \
--nsg-name $APIM_NSG_NAME \
--name DependencyOnAzureStorage \
--priority 102 \
--direction Outbound \
--source-address-prefixes '*' \
--source-port-ranges '*' \
--destination-address-prefixes '*' \
--destination-port-ranges 443 \
--access Allow \
--protocol Tcp \
--description "Dependency on Azure Storage"
az network nsg rule create \
-g ${RESOURCE_GROUP} \
--nsg-name $APIM_NSG_NAME \
--name AccessToAzureSQLendpoints \
--priority 103 \
--direction Outbound \
--source-address-prefixes '*' \
--source-port-ranges '*' \
--destination-address-prefixes '*' \
--destination-port-ranges 1433 \
--access Allow \
--protocol Tcp \
--description "Access to Azure SQL endpoints"
az network nsg rule create \
-g ${RESOURCE_GROUP} \
--nsg-name $APIM_NSG_NAME \
--name AccessToAzureKeyVault \
--priority 104 \
--direction Outbound \
--source-address-prefixes '*' \
--source-port-ranges '*' \
--destination-address-prefixes '*' \
--destination-port-ranges 433 \
--access Allow \
--protocol Tcp \
--description "Access to Azure Key Vault"
echo -e "${HIGHLIGHT}Associate NSG to APIM subnet${NC}"
az network vnet subnet update \
-n $APIM_SUBNET_NAME \
-g ${RESOURCE_GROUP} \
--vnet-name $VNET_NAME \
--network-security-group $APIM_NSG_NAME
echo -e "${HIGHLIGHT}Inject APIM in the vnet... (at $(date))${NC}"
time az resource update -n ${APIM_NAME} -g ${RESOURCE_GROUP} \
--resource-type Microsoft.ApiManagement/service \
--set properties.virtualNetworkType=Internal properties.virtualNetworkConfiguration.subnetResourceId=$APIM_SUBNET_ID properties.publicIpAddressId=$APIM_PUBLIC_IP_ID
echo -e "${HIGHLIGHT} Get internal APIM IP ${NC}"
APIM_PRIVATE_IP=$(az apim show -n ${APIM_NAME} -g ${RESOURCE_GROUP} --query "privateIpAddresses[0]" -o tsv)
# Create private DNS with the custom domain(https://learn.microsoft.com/en-us/azure/api-management/api-management-using-with-internal-vnet?tabs=stv2#dns-configuration)
echo -e "${HIGHLIGHT} Create private DNS zone ${CUSTOM_DOMAIN}...${NC}"
az network private-dns zone create -g $RESOURCE_GROUP -n $CUSTOM_DOMAIN
# Link the private DNS zone to the virtual network
echo -e "${HIGHLIGHT} Link the private DNS zone to the virtual network...${NC}"
az network private-dns link vnet create \
--resource-group $RESOURCE_GROUP \
--zone-name $CUSTOM_DOMAIN \
--name $CUSTOM_DOMAIN-link \
--virtual-network $VNET_NAME \
--registration-enabled false
# Add records to the private DNS zone
echo -e "${HIGHLIGHT} Add records to the private DNS zone...${NC}"
az network private-dns record-set a add-record \
--resource-group $RESOURCE_GROUP \
--zone-name $CUSTOM_DOMAIN \
--record-set-name $APIM_NAME \
--ipv4-address $APIM_PRIVATE_IP
az network private-dns record-set a add-record \
--resource-group $RESOURCE_GROUP \
--zone-name $CUSTOM_DOMAIN \
--record-set-name api \
--ipv4-address $APIM_PRIVATE_IP
az network private-dns record-set a add-record \
--resource-group $RESOURCE_GROUP \
--zone-name $CUSTOM_DOMAIN \
--record-set-name portal \
--ipv4-address $APIM_PRIVATE_IP
az network private-dns record-set a add-record \
--resource-group $RESOURCE_GROUP \
--zone-name $CUSTOM_DOMAIN \
--record-set-name management \
--ipv4-address $APIM_PRIVATE_IP
Para que la instancia de API Management se mantenga en la versión stv2 es necesario que durante la actualización al modo interno en la red virtual le asignemos las reglas al network security group que se muestran en el código así como una IP pública que se utiliza únicamente para el mantenimiento del servicio, pero que no da acceso al gateway al exterior. Esta además debe tener un nombre DNS asociado. De no hacerlo así la actualización de tu API Management convertirá el mismo a la versión antigua, stv1.
Una vez que el proceso haya finalizado lo siguiente es añadir a la Private DNS del custom domain los registros necesarios para poder resolverlo de manera interna y que nuestro futuro Application Gateway pueda comunicarse correctamente con el API Management.
Desplegar Application Gateway
El último componente principal que nos queda por desplegar es Application Gateway que, como decía en la introducción de este artículo, será el que reciba las peticiones y, si estas pasan las reglas de WAF de forma satisfactoria, le dará paso a API Management para que enrute a la API dentro del clúster de AKS:
echo "${HIGHLIGHT}Creating Application Gateway...${NC}"
# Create public ip
az network public-ip create \
--resource-group $RESOURCE_GROUP \
--name $APP_GW_PUBLIC_IP_NAME \
--allocation-method Static \
--sku Standard \
--dns-name $APP_GW_PUBLIC_IP_DNS_NAME
# Create a WAF policy
GENERAL_WAF_POLICY="general-waf-policies"
az network application-gateway waf-policy create \
--name $GENERAL_WAF_POLICY \
--resource-group $RESOURCE_GROUP \
--type OWASP \
--version 3.2
# Get WAF policy ID
WAF_POLICY_ID=$(az network application-gateway waf-policy show --name $GENERAL_WAF_POLICY --resource-group $RESOURCE_GROUP --query id -o tsv)
# Create the app gateway
time az network application-gateway create \
--resource-group $RESOURCE_GROUP \
--name $APP_GW_NAME \
--location $LOCATION \
--vnet-name $VNET_NAME \
--subnet $APP_GW_SUBNET_NAME \
--public-ip-address $APP_GW_PUBLIC_IP_NAME \
--sku WAF_v2 \
--capacity 1 \
--priority 1 \
--waf-policy $GENERAL_WAF_POLICY
APP_GW_ID=$(az network application-gateway show --name $APP_GW_NAME --resource-group $RESOURCE_GROUP --query id -o tsv)
echo -e "${HIGHLIGHT}Application Gateway created${NC}"
echo -e "${HIGHLIGHT}Create workspace for diagnostics${NC}"
WORKSPACE_ID=$(az monitor log-analytics workspace create \
--resource-group $RESOURCE_GROUP \
--workspace-name $APP_GW_NAME-workspace \
--location $LOCATION \
--query id -o tsv)
echo -e "${HIGHLIGHT}Enable diagnostics settings${NC}"
az monitor diagnostic-settings create \
--name $APP_GW_NAME-diag \
--resource $APP_GW_ID \
--resource-group $RESOURCE_GROUP \
--workspace $WORKSPACE_ID \
--logs '[
{
"category": "ApplicationGatewayAccessLog",
"enabled": true,
"retentionPolicy": {
"enabled": false,
"days": 0
}
},
{
"category": "ApplicationGatewayPerformanceLog",
"enabled": true,
"retentionPolicy": {
"enabled": false,
"days": 0
}
},
{
"category": "ApplicationGatewayFirewallLog",
"enabled": true,
"retentionPolicy": {
"enabled": false,
"days": 0
}
}
]'
Crear una máquina virtual de salto dentro de la red
Para esta prueba, es buena idea tener una máquina virtual dentro de la misma red virtual para poder hacer troubleshooting si en algún momento no están funcionando las cosas como esperamos.
echo -e "${HIGHLIGHT}Getting the newest image of Windows 11${NC}"
WIN11_VM_IMAGES=$(az vm image list --publisher "microsoftwindowsdesktop" --architecture "x64" --offer "Windows-11" --location $LOCATION --all)
# Get the newest image
WIN11_VM_IMAGE=$(echo $WIN11_VM_IMAGES | jq -r '.[0].urn')
VM_SUBNET_NAME=vm-subnet
echo -e "${HIGHLIGHT}Create a subnet for the VM...${NC}"
az network vnet subnet create \
--resource-group $RESOURCE_GROUP \
--vnet-name $VNET_NAME \
--name $VM_SUBNET_NAME \
--address-prefixes 192.168.4.0/24
VM_NAME=vm-jumpbox
RANDOM_PASSWORD=$(openssl rand -base64 32)
echo -e "${HIGHLIGHT}Create Windows VM inside the VNET with password ${RANDOM_PASSWORD}${NC}"
az vm create \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--name $VM_NAME \
--image $WIN11_VM_IMAGE \
--admin-username azureuser \
--admin-password $RANDOM_PASSWORD \
--vnet-name $VNET_NAME \
--subnet $VM_SUBNET_NAME
Si inicias sesión en la misma comprobarás que puedes acceder al portal del desarrollador de tu API Management de forma interna, utilizando tu dominio personalizado.

Nota: Este inicialmente no estará publicado al ser un despliegue nuevo de API Management, pero puedes habilitarlo desde la sección Developer portal > portal overview y haciendo clic en el enlace Developer portal desde la máquina de salto o copiando el enlace que genera desde tu máquina local y accediendo desde el enlace que genera, con token incluido, desde la máquina de salto.

Luego ya puedes darle al botón Publish en la misma sección.
Configurar Application Gateway
Ahora ha llegado el momento de configurar Application Gateway para establecer API Management como backend y poder exponer el acceso al mismo a través de este:
echo -e "${HIGHLIGHT}Configuring App Gw to use APIM as backend... ${NC}"
echo -e "${HIGHLIGHT}Create APIM backend pool with the API Management service ${NC}"
az network application-gateway address-pool create \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name apim-portal \
--servers portal.$CUSTOM_DOMAIN
az network application-gateway address-pool create \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name apim-gateway \
--servers api.$CUSTOM_DOMAIN
az network application-gateway address-pool create \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name apim-management \
--servers management.$CUSTOM_DOMAIN
echo -e "${HIGHLIGHT}Create 443 frontend port... ${NC}"
az network application-gateway frontend-port create \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name port_443 \
--port 443
echo -e "${HIGHLIGHT} Check frontend ports ${NC}"
az network application-gateway frontend-port list --gateway-name $APP_GW_NAME -g $RESOURCE_GROUP -o table
echo -e "${HIGHLIGHT}Create health probes for the backends${NC}"
az network application-gateway probe create \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name "apim-api-probe" \
--path "/status-0123456789abcdef" \
--host-name-from-http-settings true \
--protocol "Https" \
--interval 30 \
--threshold 3 \
--timeout 30
az network application-gateway probe create \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name "apim-management-probe" \
--path "/ServiceStatus" \
--host-name-from-http-settings true \
--protocol "Https" \
--interval 30 \
--threshold 3 \
--timeout 30
az network application-gateway probe create \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name "apim-portal-probe" \
--path "/signin" \
--host-name-from-http-settings true \
--protocol "Https" \
--interval 30 \
--threshold 3 \
--timeout 30
echo -e "${HIGHLIGHT}Create backend settings${NC}"
az network application-gateway http-settings create \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name "apim-api" \
--port 443 \
--protocol Https \
--cookie-based-affinity Disabled \
--timeout 20 \
--probe "apim-api-probe" \
--host-name-from-backend-pool true
az network application-gateway http-settings create \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name "apim-management" \
--port 443 \
--protocol Https \
--cookie-based-affinity Disabled \
--timeout 20 \
--probe "apim-management-probe" \
--host-name-from-backend-pool true
az network application-gateway http-settings create \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name "apim-portal" \
--port 443 \
--protocol Https \
--cookie-based-affinity Disabled \
--timeout 20 \
--probe "apim-portal-probe" \
--host-name-from-backend-pool true
echo -e "${HIGHLIGHT} Upload certificates to App Gw...${NC}"
sudo az network application-gateway ssl-cert create \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name apim-api \
--cert-file api.$CUSTOM_DOMAIN.pfx \
--cert-password $CERT_PASSWORD
sudo az network application-gateway ssl-cert create \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name apim-management \
--cert-file management.$CUSTOM_DOMAIN.pfx \
--cert-password $CERT_PASSWORD
sudo az network application-gateway ssl-cert create \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name apim-portal \
--cert-file portal.$CUSTOM_DOMAIN.pfx \
--cert-password $CERT_PASSWORD
echo -e "${HIGHLIGHT} Create listeners for the endpoints...${NC}"
az network application-gateway http-listener create \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name "portal.$CUSTOM_DOMAIN" \
--frontend-ip appGatewayFrontendIP \
--frontend-port port_443 \
--ssl-cert apim-portal \
--host-name portal.$CUSTOM_DOMAIN
az network application-gateway http-listener create \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name "api.$CUSTOM_DOMAIN" \
--frontend-ip appGatewayFrontendIP \
--frontend-port port_443 \
--ssl-cert apim-api \
--host-name api.$CUSTOM_DOMAIN
az network application-gateway http-listener create \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name "management.$CUSTOM_DOMAIN" \
--frontend-ip appGatewayFrontendIP \
--frontend-port port_443 \
--ssl-cert apim-management \
--host-name management.$CUSTOM_DOMAIN
echo -e "${HIGHLIGHT}Create rules that glue the listeners and the backend pools...${NC}"
az network application-gateway rule create \
--gateway-name $APP_GW_NAME \
--name "apim-portal-rule" \
--resource-group $RESOURCE_GROUP \
--http-listener "portal.$CUSTOM_DOMAIN" \
--rule-type "Basic" \
--address-pool "apim-portal" \
--http-settings "apim-portal" \
--priority 100
az network application-gateway rule create \
--gateway-name $APP_GW_NAME \
--name "apim-api-rule" \
--resource-group $RESOURCE_GROUP \
--http-listener "api.$CUSTOM_DOMAIN" \
--rule-type "Basic" \
--address-pool "apim-gateway" \
--http-settings "apim-api" \
--priority 101
az network application-gateway rule create \
--gateway-name $APP_GW_NAME \
--name "apim-management-rule" \
--resource-group $RESOURCE_GROUP \
--http-listener "management.$CUSTOM_DOMAIN" \
--rule-type "Basic" \
--address-pool "apim-management" \
--http-settings "apim-management" \
--priority 102
echo -e "${HIGHLIGHT} Check backend health ${NC}"
az network application-gateway show-backend-health \
--name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--query "backendAddressPools[].backendHttpSettingsCollection[].servers[]" -o table
echo -e "${HIGHLIGHT}Delete default settings...${NC}"
echo -e "${HIGHLIGHT}Delete rule1${NC}"
az network application-gateway rule delete \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name "rule1"
echo -e "${HIGHLIGHT}Delete appGatewayHttpListener${NC}"
az network application-gateway http-listener delete \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name "appGatewayHttpListener"
echo -e "${HIGHLIGHT}Check appGatewayBackendHttpSettings ${NC}"
az network application-gateway http-settings delete \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name "appGatewayBackendHttpSettings"
# Delete default backend pool
echo -e "${HIGHLIGHT}Delete default backend pool ${NC}"
az network application-gateway address-pool delete \
--gateway-name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--name "appGatewayBackendPool"
echo -e "${HIGHLIGHT} Done 🫡... ${NC}"
He eliminado la configuración por defecto que genera Application Gateway al desplegarse, para que todo quede más limpio. Para saber que todo ha ido correctamente puedes revisar la sección Backend health que también he devuelto como output al lanzar este script.

Para finalizar con la configuración de Application Gateway debes actualizar tu domino en tu proveedor de DNS apuntando a la IP publica de este servicio los subdominios api, portal y management, para que los listeners configurados funcionen correctamente.
# Get public IP of the App Gateway
APP_GW_PUBLIC_IP=$(az network public-ip show --name $APP_GW_PUBLIC_IP_NAME --resource-group $RESOURCE_GROUP --query ipAddress -o tsv)
echo "Update your DNS provider with the public IP of the App Gateway $APP_GW_PUBLIC_IP for the three subdomains"
Desplegar APIs de ejemplo
Ahora ya si, para poder probar que todo funciona correctamente, puedes desplegar algunas APIs de prueba. Para este ejemplo voy a hacer uso de mi API Tour of Heroes:
echo -e "${HIGHLIGHT}Deploying sample API to AKS cluster ${AKS_NAME} in ${RESOURCE_GROUP}...${NC}"
kubectl create namespace tour-of-heroes-api
kubectl apply -f manifests/tour-of-heroes-api --recursive --namespace tour-of-heroes-api
echo -e "${HIGHLIGHT}Waiting for sample API to be ready...${NC}"
kubectl wait --for=condition=available --timeout=600s deployment/tour-of-heroes-sql --namespace tour-of-heroes-api
echo -e "${HIGHLIGHT}Done 👍🏻${NC}"
echo -e "${HIGHLIGHT} Get internal IP for tour-of-heroes-api service ${NC}"
INTERNAL_IP_API=$(kubectl get service tour-of-heroes-api -n tour-of-heroes-api -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo -e "${HIGHLIGHT} Test tour of heroes API ${NC}"
kubectl run -it --rm test --image=debian --restart=Never -n tour-of-heroes-api -- bash -c "apt-get update && apt-get install curl -y && curl -k -v http://tour-of-heroes-api/api/hero"
kubectl run -it --rm test --image=debian --restart=Never -n tour-of-heroes-api -- bash -c "apt-get update && apt-get install curl -y && curl -k -v http://${INTERNAL_IP_API}/api/hero"
Los últimos comandos comprueban que desde dentro del clúster la API es accesible, con lo que confirmo que está funcionando. El siguiente paso es darla de alta en API Management:
echo -e "${HIGHLIGHT} Create Tour of Heroes API in API Management ${NC}"
az apim api create \
--resource-group ${RESOURCE_GROUP} \
--service-name ${APIM_NAME} \
--api-id tour-of-heroes-api \
--subscription-required true \
--path /tour-of-heroes-api \
--display-name "Tour of Heroes API" \
--service-url "http://tour-of-heroes.${PRIVATE_DNS_ZONE_NAME}/api/hero" \
--protocols http https
echo -e "${HIGHLIGHT} Add GET operation to the API ${NC}"
az apim api operation create \
--resource-group ${RESOURCE_GROUP} \
--service-name ${APIM_NAME} \
--api-id tour-of-heroes-api \
--url-template / \
--method GET \
--display-name "Get all heroes"
echo -e "${HIGHLIGHT} Add POST operation to the API ${NC}"
az apim api operation create \
--resource-group ${RESOURCE_GROUP} \
--service-name ${APIM_NAME} \
--api-id tour-of-heroes-api \
--url-template / \
--method POST \
--display-name "Add hero"
echo -e "${GREEN} Add the API to the Starter product ${NC}"
az apim product api add \
--resource-group ${RESOURCE_GROUP} \
--service-name ${APIM_NAME} \
--product-id Starter \
--api-id tour-of-heroes-api
En lugar de utilizar la IP interna asociada al balanceador, atachada al recurso de tipo Service, estoy haciendo uso del Private DNS, en este caso apis.internal, que ExternalDNS está gestionando por mi.
Si toda la configuración ha ido bien, puedes probar a llamar a la API de la siguiente manera:
# Get subscription id
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
# Get starter API Key
API_KEY=$(az rest --method get \
--uri "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.ApiManagement/service/${APIM_NAME}/subscriptions?api-version=2018-01-01" \
| jq --raw-output '.value[] | select(.properties.productId | endswith("starter")) | .properties.primaryKey')
echo -e "${GREEN} Test tour of heroes API ${NC}"
echo -e "${GREEN} Get heroes ${NC}"
curl -H "Ocp-Apim-Subscription-Key: ${API_KEY}" "https://api.${CUSTOM_DOMAIN}/tour-of-heroes-api/" | jq
echo -e "${GREEN} Add hero ${NC}"
curl -H "Ocp-Apim-Subscription-Key: ${API_KEY}" \
-H "Content-Type: application/json" \
-X POST \
-d '{"name": "Arrow", "alterEgo": "Oliver Queen", "description": "Multimillonario playboy Oliver Queen (Stephen Amell), quien, cinco años después de estar varado en una isla hostil, regresa a casa para luchar contra el crimen y la corrupción como un vigilante secreto cuya arma de elección es un arco y flechas." }' \
"https://api.${CUSTOM_DOMAIN}/tour-of-heroes-api/"
echo -e "${GREEN} Get heroes ${NC}"
curl -k -H "Ocp-Apim-Subscription-Key: ${API_KEY}" "https://api.$CUSTOM_DOMAIN/tour-of-heroes-api/" | jq
curl -k -H "Ocp-Apim-Subscription-Key: ${API_KEY}" "https://api.${CUSTOM_DOMAIN}/tour-of-heroes-api/" | jq
echo -e "${GREEN} Get heroes using HTTPS ${NC}"
curl -k -H "Ocp-Apim-Subscription-Key: ${API_KEY}" "https://api.$CUSTOM_DOMAIN/tour-of-heroes-api/" | jq
Si te fijas la llamada se hace a través del subdominio «api» de nuestro dominio personalizado que, al estar realizándose la petición desde Internet, mi proveedor del dominio le devuelve la IP pública del Application Gateway, lo que hace que este sepa enrutar la petición correctamente gracias al hostname que le llega.
Otra API que tengo de ejemplo es una llamada Goat API que pretende simular una API con algunos fallos desde el punto de vista de seguridad. Puedes desplegarla con los siguiente comandos:
echo -e "${GREEN}Deploying Goat API to AKS cluster ${AKS_NAME} in ${RESOURCE_GROUP}...${NC}"
kubectl create namespace goat-api
kubectl apply -f manifests/goat-api --recursive --namespace goat-api
# Deploy Traefik using Helm
helm repo add traefik https://traefik.github.io/charts
helm install traefik traefik/traefik --values manifests/traefik-config/values.yaml
echo -e "${GREEN}Waiting for Goat API to be ready...${NC}"
kubectl wait --for=condition=available --timeout=600s deployment/mssql-deployment --namespace goat-api
echo -e "${GREEN}Let's wait 30 seconds for the SQL Server to be ready...${NC}"
sleep 30
kubectl run -it --rm sqlclient --image=mcr.microsoft.com/mssql-tools --restart=Never -n goat-api -- bash -c 'apt-get update && apt-get install curl -y && curl -L https://raw.githubusercontent.com/0GiS0/dotnet-goat-api/main/owaspdb.sql -o owaspdb.sql && /opt/mssql-tools/bin/sqlcmd -S mssql-svc -U sa -P "YourStrong!Passw0rd" -i owaspdb.sql'
# Select Customers table
kubectl run -it --rm sqlclient --image=mcr.microsoft.com/mssql-tools --restart=Never -n goat-api -- bash -c '/opt/mssql-tools/bin/sqlcmd -S mssql-svc -U sa -P "YourStrong!Passw0rd" -Q "USE owaspdb; SELECT * FROM Customers;"'
echo -e "${GREEN}Done 👍🏻"
echo -e "${GREEN} Get internal IP for goat-api service ${NC}"
INTERNAL_IP_API=$(kubectl get service goat-api-svc -n goat-api -o jsonpath='{.spec.clusterIP}')
echo -e "${GREEN} Test Goat API ${NC}"
kubectl run -it --rm test --image=debian --restart=Never -n goat-api -- bash -c "apt-get update && apt-get install curl -y && curl -k -v http://goat-api-svc/customer?id=1"
kubectl run -it --rm test --image=debian --restart=Never -n goat-api -- bash -c "apt-get update && apt-get install curl -y && curl -k -v http://${INTERNAL_IP_API}/customer?id=1"
# Add API to APIM
echo -e "${GREEN} Add Goat API to APIM ${NC}"
az apim api create \
--resource-group ${RESOURCE_GROUP} \
--service-name ${APIM_NAME} \
--api-id goat-api \
--subscription-required true \
--path /goat \
--display-name "Goat API" \
--service-url "http://goat.${PRIVATE_DNS_ZONE_NAME}" \
--protocols http https
echo -e "${GREEN} Add customer operation to the API ${NC}"
az apim api operation create \
--resource-group ${RESOURCE_GROUP} \
--service-name ${APIM_NAME} \
--api-id goat-api \
--url-template "/customer?id={id}" \
--method GET \
--display-name "Get customer" \
--template-parameters name=id description="Customer ID" type="string" required=true
echo -e "${GREEN} Add POST operation to the API ${NC}"
az apim api operation create \
--resource-group ${RESOURCE_GROUP} \
--service-name ${APIM_NAME} \
--api-id goat-api \
--url-template /addcreditcard \
--method POST \
--display-name "Add credit card"
# Add api to a starter product
echo -e "${GREEN} Add the API to the Starter product ${NC}"
az apim product api add \
--resource-group ${RESOURCE_GROUP} \
--service-name ${APIM_NAME} \
--product-id Starter \
--api-id goat-api
sleep 10
echo -e "${GREEN} Call customer operation ${NC}"
curl -H "Ocp-Apim-Subscription-Key: ${API_KEY}" "https://api.$CUSTOM_DOMAIN/goat/customer?id=1" | jq
echo -e "${GREEN} SQL Injection ${NC}"
curl -H "Ocp-Apim-Subscription-Key: ${API_KEY}" "https://api.${CUSTOM_DOMAIN}/goat/customer?id=1%20or%201=1" | jq
echo -e "${GREEN} SQL Injection via HTTPS ${NC}"
curl -k -H "Ocp-Apim-Subscription-Key: ${API_KEY}" "https://api.${CUSTOM_DOMAIN}/goat/customer?id=1%20or%201=1" | jq
echo -e "${GREEN} Add credit card ${NC}"
curl -H "Ocp-Apim-Subscription-Key: ${API_KEY}" -H "Content-Type: application/json" -d '{"id":1,"CardNumber":"1234567890123456","ExpirationDate":"2021-12-31","CVV": "100", "UserId": "1"}' "https://api.${CUSTOM_DOMAIN}/goat/addcreditcard" | jq
En este caso la misma se apoya en Traefik como Ingress Controller por lo que la IP asociada a su hostname ExternalDNS la ha mapeado a la IP del servicio de Traefik.
Habilitar el WAF de Application Gateway y los errores personalizados
Para terminar, la última prueba que quería hacer en este entorno es como Application Gateway nos ayuda a proteger nuestras APIs a través de su WAF, además de modificar las páginas de error por defecto de este servicio:
echo -e "${GREEN}Create Azure Storage Account"
az storage account create \
--name $STORAGE_ACCOUNT_NAME \
--resource-group $RESOURCE_GROUP \
--location $LOCATION
echo -e "${GREEN}Enable static website"
az storage blob service-properties update \
--account-name $STORAGE_ACCOUNT_NAME \
--static-website
echo -e "${GREEN}Get static website url for custom error pages"
STATIC_WEB_SITE_URL=$(az storage account show \
--name $STORAGE_ACCOUNT_NAME \
--resource-group $RESOURCE_GROUP \
--query primaryEndpoints.web \
--output tsv)
echo -e "${GREEN}Upload custom error pages"
az storage blob upload-batch \
--account-name $STORAGE_ACCOUNT_NAME \
--destination \$web \
--source custom-error-pages
echo -e "${GREEN}Update App Gw to use custom error pages"
az network application-gateway update \
--name $APP_GW_NAME \
--resource-group $RESOURCE_GROUP \
--custom-error-pages 403="$STATIC_WEB_SITE_URL/403.html" 502="$STATIC_WEB_SITE_URL/502.html"
echo -e "${GREEN}Change WAF policy mode to prevention${NC}"
az network application-gateway waf-policy policy-setting update \
--mode Prevention \
--policy-name $GENERAL_WAF_POLICY \
--resource-group $RESOURCE_GROUP \
--state Enabled
echo -e "${GREEN}Check APIs${NC}"
echo "https://api.$CUSTOM_DOMAIN/tour-of-heroes-api/?subscription-key=${API_KEY}"
echo "https://api.$CUSTOM_DOMAIN/goat/customer?id=1&subscription-key=${API_KEY}"
echo "https://api.$CUSTOM_DOMAIN/goat/customer?subscription-key=${API_KEY}&id=1%20or%201=1"
Al intentar acceder a la API a través de Application Gateway con parámetros maliciosos deberías de ver algo como esto 😙

Recuerda que esto es una prueba de concepto y hay otros puntos que abordar para que el entorno sea totalmente productivo.
Todo el código lo tienes en mi GitHub.
¡Saludos!