Configurar más de un Application Gateway con AGIC para AKS

Desde hace ya bastante tiempo, además de las opciones como Nginx o Taefik que puedes utilizar como Ingress Controllers también es posible hacer uso de Azure Application Gateway con tu clúster en AKS utilizando AGIC (Application Gateway Ingress Controller). Si solo necesitas utilizar un Application Gateway como controlador puedes hacerlo de manera sencilla con el add-on que hay disponible. Sin embargo, si queremos usar varios AGIC, cada uno con su Application Gateway, para escuchar cada uno de ellos a diferentes namespaces, necesitas hacer la configuración a través del chart de Helm. En este artículo te muestro como.

Arquitectura multi AGIC

Para que puedas verlo visualmente, el objetivo que estamos persiguiendo es el siguiente:

Arquitectura con múltiples AGIC

Como ves, tenemos dos AGIC que están configurados para funcionar con dos Application Gateways distintos. Cada uno de ellos solamente está observando las potenciales configuraciones de namespaces específicos en el clúster de AKS. Vamos a ver cómo configurarlo.

Crear un clúster en AKS

Lo primero que necesitas es un clúster en AKS. En este ejemplo voy a mostrarlo utilizando manage identity:

# Variables
RESOURCE_GROUP="aks-multiple-agic-demo"
LOCATION="northeurope"
AKS_NAME="aks-multiple-agic-demo"
VNET_NAME="aks-vnet"
AKS_SUBNET="aks-subnet"
# Create resource group
az group create --name $RESOURCE_GROUP --location $LOCATION
RESOURCE_GROUP_ID=$(az group show --name $RESOURCE_GROUP --query id -o tsv)

# Create a virtual network and a subnet for the AKS
az network vnet create \
--resource-group $RESOURCE_GROUP \
--name $VNET_NAME \
--address-prefixes 10.0.0.0/8 \
--subnet-name $AKS_SUBNET \
--subnet-prefix 10.10.0.0/16
# Get VNET id
SUBNET_ID=$(az network vnet subnet show -g $RESOURCE_GROUP -n $AKS_SUBNET --vnet-name $VNET_NAME --query id -o tsv)
# Create a user identity for the AKS
az identity create --name $AKS_NAME-identity --resource-group $RESOURCE_GROUP
# Get managed identity ID
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)
# Create the AKS cluster
az aks create \
--resource-group $RESOURCE_GROUP \
--name $AKS_NAME \
--node-vm-size Standard_B4ms \
--network-plugin azure \
--vnet-subnet-id $SUBNET_ID \
--docker-bridge-address 172.17.0.1/16 \
--dns-service-ip 10.2.0.10 \
--service-cidr 10.2.0.0/24 \
--enable-managed-identity \
--assign-identity $IDENTITY_ID

En este ejemplo además estoy utilizando mi propia red virtual, por lo que le estoy asignando una identidad del tipo user assigned identity, ya que es la opción recomendada tal y como se indica cuando no lo haces:

Cuando no asignas una user assigned identity

Una vez creada, asigno los roles que voy a necesitas sobre la identidad del clúster para que funcione correctamente con Azure AD Pod Identity, que configuraremos después:

# Assign the roles needed for Azure AD pod Identity to the user managed identity:  https://azure.github.io/aad-pod-identity/docs/getting-started/role-assignment/
#AKS cluster with managed identity	
KUBELET_CLIENT_ID=$(az aks show -g $RESOURCE_GROUP -n $AKS_NAME --query identityProfile.kubeletidentity.clientId -o tsv)
# Get Node resource group name
NODE_RESOURCE_GROUP=$(az aks show --resource-group $RESOURCE_GROUP --name $AKS_NAME --query nodeResourceGroup -o tsv)
NODE_RESOURCE_GROUP_ID=$(az group show --name $NODE_RESOURCE_GROUP --query id -o tsv)
# Performing role assignments 
az role assignment create --role "Managed Identity Operator" --assignee $KUBELET_CLIENT_ID --scope $NODE_RESOURCE_GROUP_ID
az role assignment create --role "Virtual Machine Contributor" --assignee $KUBELET_CLIENT_ID --scope $NODE_RESOURCE_GROUP_ID
# User-assigned identities that are not within the node resource group 
az role assignment create --role "Managed Identity Operator" --assignee $KUBELET_CLIENT_ID --scope $RESOURCE_GROUP_ID
az role assignment list --assignee $KUBELET_CLIENT_ID -o table --all

Desde el punto de vista del clúster ya tendrías todo lo necesario para continuar.

Configurar un AGIC para los namespaces de desarrollo

Vamos a empezar por el Application Gateway que se va a utilizar para los namespaces destinados a los proyectos en desarrollo:

##########################################
##### Create Dev Application Gateway ####
##########################################
APPGW_SUBNET="appgw-subnet"
## Create subnet for application gateway
az network vnet subnet create \
--resource-group $RESOURCE_GROUP \
--vnet-name $VNET_NAME \
--name $APPGW_SUBNET \
--address-prefixes 10.20.0.0/16
### Application Gateway for dev ###
APP_GW_DEV_NAME="app-gw-dev"
# Create public ip
az network public-ip create \
--resource-group $RESOURCE_GROUP \
--name $APP_GW_DEV_NAME-public-ip \
--allocation-method Static \
--sku Standard
# Create the app gateway
az network application-gateway create \
--resource-group $RESOURCE_GROUP \
--name $APP_GW_DEV_NAME \
--location $LOCATION \
--vnet-name $VNET_NAME \
--subnet $APPGW_SUBNET \
--public-ip-address $APP_GW_DEV_NAME-public-ip \
--sku WAF_v2 \
--capacity 1 

Una vez generado, recuperamos las credenciales del clúster de AKS:

# Get AKS credentials
az aks get-credentials --resource-group $RESOURCE_GROUP --name $AKS_NAME

Instalamos Azure AD Pod Identity, ya que también vamos a usar identidades manejadas para gestionar los Application Gateways:

# Install Azure AD Pod Identity
helm repo add aad-pod-identity https://raw.githubusercontent.com/Azure/aad-pod-identity/master/charts
helm install aad-pod-identity aad-pod-identity/aad-pod-identity -n kube-system
# Check Azure AD Pod identity pods
kubectl get pods -w -n kube-system -l app.kubernetes.io/name=aad-pod-identity

Y creamos una identidad para este Application Gateway, con el rol de Contributor sobre el recurso en cuestión y de Reader sobre el grupo de recursos donde está ubicado:

# Create an identity for the AGIC controller
az identity create -g $RESOURCE_GROUP -n $APP_GW_DEV_NAME-identity
# Get client id and id for the identity
APP_GW_DEV_IDENTITY_CLIENT_ID=$(az identity show -g $RESOURCE_GROUP -n $APP_GW_DEV_NAME-identity -o tsv --query "clientId")
APP_GW_DEV_IDENTITY_RESOURCE_ID=$(az identity show -g $RESOURCE_GROUP -n $APP_GW_DEV_NAME-identity -o tsv --query "id")
APP_GW_DEV_ID=$(az network application-gateway show -g $RESOURCE_GROUP -n $APP_GW_DEV_NAME --query id -o tsv)
# Assign Contributor role to the identity over the application gateway
az role assignment create \
    --role "Contributor" \
    --assignee $APP_GW_DEV_IDENTITY_CLIENT_ID \
    --scope $APP_GW_DEV_ID
az role assignment create \
    --role "Reader" \
    --assignee $APP_GW_DEV_IDENTITY_CLIENT_ID \
    --scope $RESOURCE_GROUP_ID
# Check role assignments for the user identities
az role assignment list --assignee $APP_GW_DEV_IDENTITY_CLIENT_ID --all -o table

Como último paso añadimos el repositorio de AGIC a Helm:

# Add the AGIC Helm repository
helm repo add application-gateway-kubernetes-ingress https://appgwingress.blob.core.windows.net/ingress-azure-helm-package/
helm repo update

Y confeccionamos una configuración para el chart como la siguiente:

# This file contains the essential configs for the ingress controller helm chart
# Verbosity level of the App Gateway Ingress Controller
verbosityLevel: 3
################################################################################
# Specify which application gateway the ingress controller will manage
#
appgw:
    subscriptionId: <YOUR_SUBSCRIPTION_ID>
    resourceGroup: aks-multiple-agic-demo
    name: app-gw-dev
    usePrivateIP: false
    # Setting appgw.shared to "true" will create an AzureIngressProhibitedTarget CRD.
    # This prohibits AGIC from applying config for any host/path.
    # Use "kubectl get AzureIngressProhibitedTargets" to view and change this.
    shared: false
################################################################################
# Specify which kubernetes namespace the ingress controller will watch
# Default value is "default"
# Leaving this variable out or setting it to blank or empty string would
# result in Ingress Controller observing all acessible namespaces.
#
kubernetes:
    watchNamespace: dev-wordpress,dev-aspnetapp
    ingressClassResource:
        name: azure-application-gateway-dev
        enabled: true
        default: false
        controllerValue: "azure/application-gateway-dev"
################################################################################
# Specify the authentication with Azure Resource Manager
#
# Two authentication methods are available:
# - Option 1: AAD-Pod-Identity (https://github.com/Azure/aad-pod-identity)
armAuth:
    type: aadPodIdentity
    identityResourceID: <VALUE_OF_APP_GW_DEV_IDENTITY_RESOURCE_ID>
    identityClientID: <VALUE_OF_APP_GW_DEV_IDENTITY_CLIENT_ID>
## Alternatively you can use Service Principal credentials
# armAuth:
#    type: servicePrincipal
#    secretJSON: <<Generate this value with: "az ad sp create-for-rbac --subscription <subscription-uuid> --sdk-auth | base64 -w0" >>
################################################################################
# Specify if the cluster is RBAC enabled or not
rbac:
    enabled: true # true/false

En este ejemplo, este AGIC va a observar, por ahora, dos namespaces: dev-wordpress y dev-aspnetapp. Por otro lado, hay dos opciones a día de hoy para autenticarse y poder interactuar con la API de ARM, que mandará las acciones pertinentes a nuestro Application Gateway. En este ejemplo he utilizado una identidad manejada, por lo que debes configurar los valores de APP_GW_DEV_IDENTITY_CLIENT_ID y de APP_GW_DEV_IDENTITY_RESOURCE_ID, que recuperé en el paso anterior en identityResourceID y identityClientID respectivamente.

Importante: Antes de instalar el chart, los namespaces que va a observar deben estar creados previamente:

# Create dev namespaces
kubectl create namespace dev-wordpress
kubectl create namespace dev-aspnetapp

Ahora instala el chart con estos valores de la siguiente forma:

# Install Helm chart application-gateway-kubernetes-ingress with the helm-config.yaml configuration from the previous step
helm install agic-dev -f mi/dev-helm-config.yaml application-gateway-kubernetes-ingress/ingress-azure -n kube-system

Comprueba que todo ha ido correctamente, revisando que el pod del AGIC está ejecutándose:

# Check that ingress is ready
kubectl get pods -n kube-system -l release=agic-dev

Ahora ya tienes tu Application Gateway asociado al AGIC, listo para recibir configuraciones de Ingress que estén en los dos namespaces configurados. Para hacer una prueba voy a desplegar dos aplicaciones que también están en el repo de GitHub:

# Deploy a aspnetapp for dev-aspnetapp
kubectl apply -f dev/aspnetsample.yaml
kubectl get pods -n dev-aspnetapp 
kubectl get ingress -n dev-aspnetapp
# Test the app
APP_GW_DEV_PUBLIC_IP=$(az network public-ip show -g $RESOURCE_GROUP -n $APP_GW_DEV_NAME-public-ip -o tsv --query ipAddress)
curl http://$APP_GW_DEV_PUBLIC_IP
echo http://$APP_GW_DEV_PUBLIC_IP
# Deploy wordpress for dev-wordpress
kubectl apply -f dev/wordpress.yaml -n dev-wordpress
kubectl get pods -n dev-wordpress
kubectl get ingress -n dev-wordpress
# Test the wordpress app
curl http://$APP_GW_DEV_PUBLIC_IP:8080
echo http://$APP_GW_DEV_PUBLIC_IP

El ingress de la aplicación de ejemplo en ASP.NET tendrá esta pinta:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: aspnetapp
  namespace: dev-aspnetapp
  annotations:
    kubernetes.io/ingress.class: azure/application-gateway-dev
spec:
  rules:
  - http:
      paths:
      - path: /
        backend:
          service:
            name: aspnetapp
            port:
              number: 80
        pathType: Exact

y el de WordPress esta otra:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: wordpress  
  annotations:
    kubernetes.io/ingress.class: azure/application-gateway-dev
    appgw.ingress.kubernetes.io/override-frontend-port: "8080"
spec:
  rules:
  - http:
      paths:
      - path: /
        backend:
          service:
            name: wordpress
            port:
              number: 80
        pathType: Exact

Por lo tanto a la primera podrás acceder a través de la IP del Gateway, en la raíz, con el puerto 80, y la segunda de ellas a través del puerto 8080. Si todo ha ido bien los logs del AGIC, los cuales puedes comprobar así:

kubectl logs $(kubectl get pod -n kube-system -l release=agic-dev -o jsonpath='{.items[0].metadata.name}') -n kube-system

Se verán de la siguiente manera:

Logs del AGIC de desarrollo

¡Ahora vamos a por la configuración del AGIC para staging!

Configurar un AGIC para los namespaces de staging

Ahora que ya hemos visto que todo funciona correctamente para desarrollo vamos a hacer lo mismo pero para stating. Creamos el Application Gateway:

### Application Gateway for staging ###
APP_GW_STAGING_NAME="app-gw-staging"
# Create public ip
az network public-ip create \
--resource-group $RESOURCE_GROUP \
--name $APP_GW_STAGING_NAME-public-ip \
--allocation-method Static \
--sku Standard
# Create the app gateway
az network application-gateway create \
--resource-group $RESOURCE_GROUP \
--name $APP_GW_STAGING_NAME \
--location $LOCATION \
--vnet-name $VNET_NAME \
--subnet $APPGW_SUBNET \
--public-ip-address $APP_GW_STAGING_NAME-public-ip \
--sku WAF_v2 \
--capacity 1 

Creamos la identidad para este recurso y le asignamos los permisos necesarios:

# Create an identity for the AGIC controller
az identity create -g $RESOURCE_GROUP -n $APP_GW_STAGING_NAME-identity
# Get client id and id for the identity
APP_GW_STAGING_IDENTITY_CLIENT_ID=$(az identity show -g $RESOURCE_GROUP -n $APP_GW_STAGING_NAME-identity -o tsv --query "clientId")
APP_GW_STAGING_IDENTITY_RESOURCE_ID=$(az identity show -g $RESOURCE_GROUP -n $APP_GW_STAGING_NAME-identity -o tsv --query "id")
APP_GW_STAGING_NAME_ID=$(az network application-gateway show -g $RESOURCE_GROUP -n $APP_GW_STAGING_NAME --query id -o tsv)

# Assign Contributor role to the identity over the application gateway
az role assignment create \
    --role "Contributor" \
    --assignee $APP_GW_STAGING_IDENTITY_CLIENT_ID \
    --scope $APP_GW_STAGING_NAME_ID
az role assignment create \
    --role "Reader" \
    --assignee $APP_GW_STAGING_IDENTITY_CLIENT_ID \
    --scope $RESOURCE_GROUP_ID
# Check role assignments for the user identities
az role assignment list --assignee $APP_GW_STAGING_IDENTITY_CLIENT_ID --all -o table

Creamos los dos namespaces que vamos a monitorizar con este AGIC:

# Create dev namespaces
kubectl create namespace staging-whoami
kubectl create namespace staging-drupal

Y adecuamos la configuración para esta nueva instalación del chart:

# This file contains the essential configs for the ingress controller helm chart
# Verbosity level of the App Gateway Ingress Controller
verbosityLevel: 3
################################################################################
# Specify which application gateway the ingress controller will manage
#
appgw:
    subscriptionId: <SUBSCRIPTION_ID>
    resourceGroup: aks-multiple-agic-demo
    name: app-gw-staging
    usePrivateIP: false
    # Setting appgw.shared to "true" will create an AzureIngressProhibitedTarget CRD.
    # This prohibits AGIC from applying config for any host/path.
    # Use "kubectl get AzureIngressProhibitedTargets" to view and change this.
    shared: false
################################################################################
# Specify which kubernetes namespace the ingress controller will watch
# Default value is "default"
# Leaving this variable out or setting it to blank or empty string would
# result in Ingress Controller observing all acessible namespaces.
#
kubernetes:
    watchNamespace: staging-whoami,staging-drupal
    ingressClassResource:
        name: azure-application-gateway-staging
        enabled: true
        default: false
        controllerValue: "azure/application-gateway-staging"
   
################################################################################
# Specify the authentication with Azure Resource Manager
#
# Two authentication methods are available:
# - Option 1: AAD-Pod-Identity (https://github.com/Azure/aad-pod-identity)
armAuth:
    type: aadPodIdentity
    identityResourceID: <VALUE_OF_APP_GW_STAGING_IDENTITY_RESOURCE_ID>
    identityClientID: <VALUE_OF_APP_GW_STAGING_IDENTITY_CLIENT_ID>
## Alternatively you can use Service Principal credentials
# armAuth:
#    type: servicePrincipal
#    secretJSON: <<Generate this value with: "az ad sp create-for-rbac --subscription <subscription-uuid> --sdk-auth | base64 -w0" >>
################################################################################
# Specify if the cluster is RBAC enabled or not
rbac:
    enabled: true # true/false

Instalamos de nuevo el chart para este entorno:

# Install Helm chart application-gateway-kubernetes-ingress with the helm-config.yaml configuration from the previous step
helm install agic-staging -f mi/staging-helm-config.yaml application-gateway-kubernetes-ingress/ingress-azure -n kube-system

Y comprobamos que efectivamente ahora tenemos tres charts instalados: el de Azure AD Pod Identity y dos del tipo AGIC:

# Check that ingress is ready
helm list -n kube-system

E igual que antes comprobamos que el pod del controller de staging se inicializa correctamente:

kubectl get pods -n kube-system -l release=agic-staging

Una vez haya terminado, podemos desplegar un par de aplicaciones de ejemplo como en desarrollo:

# Deploy whoami for staging-whoami
kubectl apply -f staging/whoami.yaml -n staging-whoami
APP_GW_STAGING_PUBLIC_IP=$(az network public-ip show -g $RESOURCE_GROUP -n $APP_GW_STAGING_NAME-public-ip -o tsv --query ipAddress)
curl http://$APP_GW_STAGING_PUBLIC_IP/whoami
echo http://$APP_GW_STAGING_PUBLIC_IP/whoami
# Deploy drupal for staging-drupal
kubectl apply -f staging/drupal.yaml -n staging-drupal
kubectl get pods -n staging-drupal -w
kubectl logs $(kubectl get pod -l release=agic-staging -o jsonpath='{.items[0].metadata.name}' -n kube-system) -n kube-system
# Test app
echo http://$APP_GW_STAGING_PUBLIC_IP:9090

Si has seguido el escenario tal cual te lo comparto, la foto quedaría de la siguiente manera:

Resultado final de la demo

Todos los pasos los tienes en este repo de GitHub.

¡Saludos!