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.
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:

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:

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!