Azure Application Gateway con WAF y wildcard + Nginx Controller para AKS

Hace ya unas cuantas semanas atrás, mis compañeros Carlos Mendible, David Sancho y una servidora estuvimos dándole vueltas a una necesidad de un cliente que requería lo siguiente: la idea era utilizar Azure Application Gateway, por su característica de WAF, como Ingress de Kubernetes. El problema es que a día de hoy, al poner este en modo WAF solo soporta hasta 40 listeners y en este escenario consideraban que se les quedaría pequeño en un futuro cercano. Es por ello que se presentó la siguiente alternativa: usar Application Gateway con la posibilidad de usar wildcard para el listener, de tal manera que pudiéramos tener más de un listener en uno, y que un Ingress Controller, como por ejemplo Nginx, tuviera las reglas necesarias para saber a dónde tenía que mandar la petición.

En este artículo te comparto la prueba de concepto que se desarrolló, por si te fuera de utilidad.

Configuración del clúster

Lo primero que necesito es un clúster de Kubernetes sobre el que montar esta prueba. Para ello puedo utilizar los siguientes comandos, para crear el grupo de recursos y este:

# Variables
RESOURCE_GROUP="waf-and-nginx-ingress"
LOCATION="northeurope"
AKS_NAME="aks-with-nginx-ingress"
VNET_NAME="aks-vnet"
AKS_SUBNET="aks-subnet"
# Create a resource group
az group create -n $RESOURCE_GROUP -l $LOCATION
# Create a VNET and aks subnet
az network vnet create -g $RESOURCE_GROUP -n $VNET_NAME \
--address-prefixes 10.0.0.0/8 \
--subnet-name $AKS_SUBNET \
--subnet-prefix 10.10.0.0/16
#Create service principal for aks
az ad sp create-for-rbac --skip-assignment > auth.json
#Get the service principal ID
APP_ID=$(jq -r '.appId' auth.json)
#Get the service principal password
PASSWORD=$(jq -r '.password' auth.json)
# Get VNET id
VNET_ID=$(az network vnet show -g $RESOURCE_GROUP -n $VNET_NAME --query id -o tsv)
SUBNET_ID=$(az network vnet subnet show -g $RESOURCE_GROUP -n $AKS_SUBNET --vnet-name $VNET_NAME --query id -o tsv)
#Give AKS permissions to the VNET
az role assignment create --assignee $APP_ID --scope $VNET_ID --role "Network Contributor"
# Create an AKS
az aks create -n $AKS_NAME -g $RESOURCE_GROUP \
--vnet-subnet-id $SUBNET_ID \
--service-principal $APP_ID \
--client-secret $PASSWORD \
--generate-ssh-keys \
--node-count 1
# Get credentials
az aks get-credentials -n $AKS_NAME -g $RESOURCE_GROUP

Ahora ya tenemos un clúster limpio donde montar el escenario descrito al inicio.

Instalar Nginx Controller

Lo siguiente que vamos a hacer es desplegar un Ingress con Nginx que será el encargado de enrutar a los servicios correctos cuando llegue una petición desde mi WAF. Para esta prueba vamos a instalar la configuración básica que aparece en la documentación:

# Install nginx
NAMESPACE="ingress-basic"
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx --create-namespace --namespace $NAMESPACE -f internal-ingress.yaml

Durante la instalación con Helm le he pasado un archivo adicional con la configuración necesaria para que el ingress controller tenga un balanceador privado, no una IP pública:

controller:
  service:
    loadBalancerIP: 10.10.0.50
    annotations:
      service.beta.kubernetes.io/azure-load-balancer-internal: "true"

Una vez que finalice, podemos comprobar que el balanceador tiene la IP correcta:

kubectl get svc -n ingress-basic

y ya podemos poner algunas aplicaciones de ejemplo.

Clientes de ejemplo y health probe

Ahora vamos a simular que tenemos varios despliegues en nuestro clúster, que corresponden a diferentes clientes y entornos para estos. Este es un ejemplo de un entorno de dev para client1:

apiVersion: v1
kind: Namespace
metadata:
  name: client1-dev
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp-client1-dev-whoami
  namespace: client1-dev
spec:
  selector:
    matchLabels:
      app: webapp-client1-dev-whoami
  replicas: 3
  template:
    metadata:
      labels:
        app: webapp-client1-dev-whoami
    spec:
      containers:
        - name: whoami
          image: traefik/whoami
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: svc-client1-dev-whoami
  namespace: client1-dev
spec:
  selector:
    app: webapp-client1-dev-whoami
  ports:
    - port: 7070
      targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: webapp-client1-dev-ingress
  namespace: client1-dev
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/ssl-redirect: "false"
    nginx.ingress.kubernetes.io/use-regex: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  rules:
  - host: client1.dev.azuredemo.es
    http:
      paths:
      - path: /(/|$)(.*)
        pathType: Prefix
        backend:
          service:
            name: svc-client1-dev-whoami
            port:
              number: 7070

Como ves, creo un conjunto de recursos con estos nombres:

  • Namespace: client1-dev
  • Deployment: webapp-client1-dev-whoami
  • Service: svc-client1-dev-whoami
  • Ingress: webapp-client1-dev-ingress

Me he preocupado de que todos los recursos que pertenecen al mismo entorno tenga el identificador del cliente al que corresponde para que, a la hora de validar la prueba, supiera que estaba bien. Hay otra carpeta en el repositorio de GitHub que simula hasta tres clientes más, siguiendo la misma estructura con el nombre de los clientes.

Clientes de prueba

Puedes desplegar estos utilizando el siguiente comando:

# Create client environments
kubectl apply -f client-environments/.

Por otro lado, además de los clientes, he creado un despliegue en un namespace diferente para las pruebas de disponibilidad que necesita Application Gateway, para saber que un backend está respondiendo correctamente:

apiVersion: v1
kind: Namespace
metadata:
  name: probe
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp-probe-whoami
  namespace: probe
spec:
  selector:
    matchLabels:
      app: webapp-probe-whoami
  replicas: 3
  template:
    metadata:
      labels:
        app: webapp-probe-whoami
    spec:
      containers:
        - name: whoami
          image: traefik/whoami
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: svc-probe-whoami
  namespace: probe
spec:
  selector:
    app: webapp-probe-whoami
  ports:
    - port: 7070
      targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: webapp-probe-ingress
  namespace: probe
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/ssl-redirect: "false"
    nginx.ingress.kubernetes.io/use-regex: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  rules:
  - http:
      paths:
      - path: /check
        pathType: Prefix
        backend:
          service:
            name: svc-probe-whoami
            port:
              number: 7070

Para desplegarlo, puedes utilizar este comando:

# Create probe test
kubectl apply -f health-probe/app-gw-probe.yaml

Si quisieras comprobar que este test funciona correctamente puedes hacerlo a través de estos comandos:

kubectl run -it --rm aks-ingress-test --image=mcr.microsoft.com/aks/fundamental/base-ubuntu:v0.0.11 --namespace ingress-basic
apt-get update && apt-get install -y curl
curl -L http://10.10.0.50/check
exit

Application Gateway

El siguiente paso es crear un Application Gateway en modo WAF dentro de una subnet de la red donde se encuentra este clúster de Kubernetes, por simplicidad en la PoC. El mismo se conectará de manera interna con el ingress controller al que le pasará las peticiones con el hostname con el que le han llegado. Para todo esto puedes utilizar los siguientes comandos:

#Variables for Application Gateway
APPGW_SUBNET="appgw-subnet"
APPGW_NAME="AppGw"
APPGW_PUBLIC_IP_NAME="AppGwPublicIP"
INTERNAL_LOAD_BALANCER_IP_FOR_NGINX_INGRESS="10.10.0.50"
YOUR_DOMAIN="azuredemo.es"
# Create an appgw subnet
az network vnet subnet create -g $RESOURCE_GROUP -n $APPGW_SUBNET \
--vnet-name $VNET_NAME \
--address-prefixes 10.20.0.0/16
#Create a standard public IP 
az network public-ip create --name $APPGW_PUBLIC_IP_NAME --resource-group $RESOURCE_GROUP --sku Standard
PUBLIC_IP=$(az network public-ip show --name $APPGW_PUBLIC_IP_NAME --resource-group $RESOURCE_GROUP --query ipAddress -o tsv)
echo "This the public IP you have to configure in your domain: $PUBLIC_IP"
# Create an App Gw with WAF enabled
az network application-gateway create -n $APPGW_NAME -g $RESOURCE_GROUP \
--sku WAF_v2 --public-ip-address $APPGW_PUBLIC_IP_NAME \
--vnet-name $VNET_NAME \
--subnet $APPGW_SUBNET
# Add backend pool with the load balancer for nginx ingress
az network application-gateway address-pool create \
--gateway-name $APPGW_NAME \
--resource-group $RESOURCE_GROUP \
--name nginx-controller-pool \
--servers $INTERNAL_LOAD_BALANCER_IP_FOR_NGINX_INGRESS
# Now add listener, using wildcards, that points to the Nginx Controller Ip and routes to the proper web app
az network application-gateway http-listener create \
  --name aks-ingress-listener \
  --frontend-ip appGatewayFrontendIP \
  --frontend-port appGatewayFrontendPort \
  --resource-group $RESOURCE_GROUP \
  --gateway-name $APPGW_NAME \
  --host-names "*.$YOUR_DOMAIN"
#Add a rule that glues the listener and the backend pool
az network application-gateway rule create \
  --gateway-name $APPGW_NAME \
  --name aks-apps \
  --resource-group $RESOURCE_GROUP \
  --http-listener aks-ingress-listener \
  --rule-type Basic \
  --address-pool nginx-controller-pool
#Create health probe
az network application-gateway probe create \
--gateway-name $APPGW_NAME \
--resource-group $RESOURCE_GROUP \
--name "probe-azuredemo-es" \
--host $INTERNAL_LOAD_BALANCER_IP_FOR_NGINX_INGRESS \
--path "/check" \
--protocol "Http"
#Add HTTP settings a custom probe
az network application-gateway http-settings update \
--gateway-name $APPGW_NAME \
--name "appGatewayBackendHttpSettings" \
--resource-group $RESOURCE_GROUP \
--probe "probe-azuredemo-es"

En mi ejemplo, voy a usar el dominio azuredemo.es al que configuro con un wildcard, de tal forma que pueda usar subdominios con el mismo listener.

Nota: esta funcionalidad todavía está en Preview, por lo que más allá de la prueba de concepto todavía no puede ser usada en un entorno productivo.

Configuración del dominio

Para que todo esto funcione, además es necesario configurar el DNS de tu dominio para que acepte los subdominios que he utilizado para este ejercicio:

La IP que aparece para el registro A es la IP pública de mi Application Gateway.

Cómo probar que funciona

Si ahora compruebo todos los recursos de tipo Ingress que me he generado:

kubectl get ingress --all-namespaces

Debería de ver algo como lo siguiente:

recursos de tipo Ingess creados para enrutar las peticiones

Las pruebas que me he generado, para ver que efectivamente todo esto funciona, han sido utilizando la extensión para Visual Studio Code llamada REST Client y las he dejado en este archivo, el cual contiene lo siguiente:

###
GET http://client1.dev.azuredemo.es HTTP/1.1
###
GET http://client2.qa.azuredemo.es HTTP/1.1
###
GET http://client3.prep.azuredemo.es HTTP/1.1
###
GET http://client4.prep.azuredemo.es HTTP/1.1
###
GET http://client5.qa.azuredemo.es HTTP/1.1

El resultado sería algo como esto:

Resultado de la PoC

La última petición debería de fallar porque no hay ningún entorno para ese cliente (así confirmamos que funciona realmente cuando debe).

El código del ejemplo lo tienes en mi GitHub.

¡Saludos!