Acceder a un clúster privado de AKS a través de VPN P2S

Hoy he estado jugando con el siguiente escenario: Necesitaba montarme un clúster de AKS privado y, como no soy una empresa 🙂 y no quería instalarme una máquina de salto en la misma red que mi clúster, quería acceder a este a través de una conexión VPN P2S, conectada a la red donde está mi AKS. Te cuento cómo lo he hecho.

Crear la red virtual

Lo primero que voy a hacer es crear la red, y aprovecho para crear el grupo de recursos, donde estará mi AKS.

#Variables
RESOURCE_GROUP="private-aks"
LOCATION="North Europe"
VNET_NAME="aks-vnet"
AKS_VNET_CIDR=10.10.0.0/16
AKS_SUBNET_NAME="aks-subnet"
AKS_SUBNET_CIDR=10.10.1.0/24

#Create resource group
az group create -n $RESOURCE_GROUP -l $LOCATION

#Create the virtual network
az network vnet create \
--name $VNET_NAME \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--address-prefixes $AKS_VNET_CIDR \
--subnet-name $AKS_SUBNET_NAME \
--subnet-prefixes $AKS_SUBNET_CIDR

No he dejado que se genere durante la creación del clúster porque me va a ser más sencillo manipular esta misma después. También necesito crear un service principal que tenga permisos de Network contributor sobre este para que mi AKS pueda luego manipular la red.

#Create a service principal and assign permissions
az ad sp create-for-rbac --skip-assignment > auth.json

VNET_ID=$(az network vnet show --resource-group $RESOURCE_GROUP --name $VNET_NAME --query id -o tsv)
SUBNET_ID=$(az network vnet subnet show --resource-group $RESOURCE_GROUP --vnet-name $VNET_NAME --name $AKS_SUBNET_NAME --query id -o tsv)

APP_ID=$(jq -r .appId auth.json)
SECRET=$(jq -r .password auth.json)

# The service principal needs to have permissions to manage the virtual network
az role assignment create --assignee $APP_ID --scope $VNET_ID --role "Network Contributor"

Ahora que ya tengo la red,la subnet que va a usar mi AKS y el Service Principal el siguiente paso es crear el clúster.

Crear un clúster de AKS privado en una red propia

Para este paso puedo utilizar este comando, uniéndolo con todo lo anterior:

AKS_NAME="private-aks"

#Create a private AKS cluster
az aks create \
--resource-group $RESOURCE_GROUP \
--name $AKS_NAME \
--node-count 1 \
--vnet-subnet-id $SUBNET_ID \
--service-principal $APP_ID \
--client-secret $SECRET \
--generate-ssh-keys \
--load-balancer-sku standard \
--enable-private-cluster

Ahora bien, una vez que el proceso termine, si recupero las credenciales:

#Get credentials
az aks get-credentials -n $AKS_NAME -g $RESOURCE_GROUP

e intento cualquier operación sobre el AKS, el resultado será que no es posible, ya que no estoy en la misma red que el clúster y este es privado.

No puedo acceder desde local al clúster de AKS porque no tiene IP pública

Es por ello que necesitamos un componente que nos permita conectarnos a dicha red desde mi local, un VPN Gateway.

Crear un VPN Gateway

En realidad, te mostré un caso parecido con un App Service usando private links hace unos meses. En este caso le toca el turno a AKS, por lo que me creo este componente dentro de mi grupo de recursos, asociado a la misma red, de la siguiente manera:

#Let's create a VPN gateway to connect to the cluster
VPN_GATEWAY_NAME="gateway"
VPN_GATEWAY_CIDR=10.10.2.0/24

#Create a subnet in the vnet for the gateway
az network vnet subnet create \
--name GatewaySubnet \
--vnet-name $VNET_NAME \
--resource-group $RESOURCE_GROUP \
--address-prefixes $VPN_GATEWAY_CIDR

#Create a public IP for the VPN Gateway
az network public-ip create \
  --name "${VPN_GATEWAY_NAME}-ip" \
  --resource-group $RESOURCE_GROUP \
  --allocation-method Dynamic
  
# Define CIDR block for the VPN clients
ADDRESS_POOL_FOR_VPN_CLIENTS=10.30.0.0/16

# Azure Active Directory info
#https://login.microsoftonline.com/<YOUR_TENANT_ID>
TENANT_ID=$(jq -r .tenant auth.json)
AZURE_VPN_CLIENT_ID="41b23e61-6c1e-4545-b367-cd054e0ed4b4"
#You have to consent Azure VPN application in your tenant first:
https://login.microsoftonline.com/common/oauth2/authorize?client_id=41b23e61-6c1e-4545-b367-cd054e0ed4b4&response_type=code&redirect_uri=https://portal.azure.com&nonce=1234&prompt=admin_consent

# Create a VPN Gateway
az network vnet-gateway create \
  --name $VPN_GATEWAY_NAME \
  --location $LOCATION \
  --public-ip-address "${VPN_GATEWAY_NAME}-ip" \
  --resource-group $RESOURCE_GROUP \
  --vnet $VNET_NAME \
  --gateway-type Vpn \
  --sku VpnGw2 \
  --vpn-type RouteBased \
  --address-prefixes $ADDRESS_POOL_FOR_VPN_CLIENTS \
  --client-protocol OpenVPN \
  --vpn-auth-type AAD \
  --aad-tenant "https://login.microsoftonline.com/${TENANT_ID}" \
  --aad-audience $AZURE_VPN_CLIENT_ID \
  --aad-issuer "https://sts.windows.net/${TENANT_ID}/"

# Get VPN client configuration
az network vnet-gateway vpn-client generate \
--resource-group $RESOURCE_GROUP \
--name $VPN_GATEWAY_NAME

Estos comandos crearán una nueva subnet en la red donde está alojado mi clúster de AKS, generará una IP pública que asociaremos a un VPN gateway, que además ya he dejado configurado para que pueda autenticarme usando Azure Active Directory. El último comando nos proporciona un zip con la configuración para usarla con Azure VPN, como mostré en el articulo de App Service con Private Link en este mismo escenario.

Sin embargo, si vuelvo a ejecutar el comando anterior, kubectl get nodes, seguiré obteniendo el mismo error. ¿Por qué ocurre esto? Porque si bien tenemos acceso a los recursos de esa red no estamos siendo capaces de resolver el nombre del private link asociado la IP del API Server de nuestro Kubernetes.

Modificar /etc/hosts

Para este artículo voy a dejarlo en la opción más sencilla, que es añadir el nombre a resolver en el archivo /etc/hosts. Con estos comandos puedes recuperar el nombre a resolver, el del private link, y la IP que hay detrás de este:

#Get AKS private link
PRIVATE_FQDN=$(az aks show --resource-group $RESOURCE_GROUP --name $AKS_NAME --query "privateFqdn" -o tsv)

#Get private IP
NODE_RESOURCE_GROUP=$(az aks show --resource-group $RESOURCE_GROUP --name $AKS_NAME --query "nodeResourceGroup" -o tsv)
KUBE_API_SERVER_IP=$(az network nic list --resource-group $NODE_RESOURCE_GROUP --query "[?contains(name, 'kube-apiserver')].ipConfigurations[0].privateIpAddress" -o tsv)

echo "$KUBE_API_SERVER_IP $PRIVATE_FQDN" >> /etc/hosts

En mi caso quedaría una entrada como la siguiente:

10.10.1.4 private-ak-private-aks-ca0cd4-1ca3b861.c1a215f7-292d-47ea-825e-2f8af8ffefa1.privatelink.northeurope.azmk8s.io

Si ahora vuelves a intentar hablar con tu clúster verás que ya es posible.

Para un entorno de pruebas/desarrollo esta sería una opción válida. Sin embargo en entornos productivos se recomienda el uso de un DNS Forwarder.

El código con el script lo tienes en mi GitHub.

¡Saludos!