Esta semana he tenido que configurar el siguiente escenario para uno de nuestros clientes: un AKS que utilice Azure Files para el montaje de volúmenes persistentes, donde la cuenta de almacenamiento solo es accesible a través de Private Link. Además este clúster tiene que ser multi-zona. En este artículo te comparto los pasos que he dado para montar este entorno.
Definir las variables y el grupo de recursos a utilizar
Antes de comenzar a crear los recursos, he definido un conjunto de variables y el grupo de recursos donde voy a añadir todos los componentes que necesito para este escenario.
#Variables
RESOURCE_GROUP="AKS-Demo"
AKS_NAME="ha-aks"
LOCATION="northeurope"
VNET="aks-vnet"
SUBNET_AKS="aks-subnet"
SUBNET_ENDPOINTS="endpoints"
PRIVATE_DNS_ZONE="private.file.core.windows.net"
PRIVATE_STORAGE_ACCOUNT="webapache"
RECORD_SET_NAME="web-apache-storage"
#Create Resource Group
az group create --name $RESOURCE_GROUP --location $LOCATION
Crear una Virtual Network
Tanto para mi nuevo clúster de AKS como para la cuenta de almacenamiento, a la que accederé de manera privada, necesito definir una red virtual como la siguiente:
#Create a VNET
az network vnet create --resource-group $RESOURCE_GROUP \
--name $VNET --address-prefixes 192.168.0.0/16 \
--subnet-name $SUBNET_AKS \
--subnet-prefixes 192.168.1.0/24
VNET_ID=$(az network vnet show --name $VNET --resource-group $RESOURCE_GROUP --query "id" --output tsv)
SUBNET_AKS_ID=$(az network vnet subnet show --name $SUBNET_AKS --resource-group $RESOURCE_GROUP --vnet-name $VNET --query "id" --output tsv)
#Create a subnet for the endpoints
az network vnet subnet create --resource-group $RESOURCE_GROUP \
--vnet-name $VNET --name $SUBNET_ENDPOINTS --address-prefixes 192.168.2.0/24 \
--disable-private-endpoint-network-policies true
Como ves, he recuperado además el ID de la VNET, así como el de la subnet donde irá el AKS. He creado también una segunda subnet a la que asociaré el endpoint privado de la cuenta de almacenamiento. El resultado debería de ser el siguiente:

Crear un DNS privado
Para poder referirnos a nuestra cuenta de almacenamiento en la configuración que vamos a hacer para AKS creamos un DNS privado que vamos a asociar a la VNET que acabamos de crear.
#Create a private DNS zone
az network private-dns zone create --resource-group $RESOURCE_GROUP \
--name $PRIVATE_DNS_ZONE
#Create an association between the VNET and the private DNS zone
az network private-dns link vnet create \
--name "link_between_dns_and_vnet" \
--resource-group $RESOURCE_GROUP \
--zone-name $PRIVATE_DNS_ZONE \
--virtual-network $VNET \
--registration-enabled false
Con ello conseguimos que lo que esté dentro de dicha VNET sepa resolver los nombres que demos de alta como registros en este resolvedor de nombres privado. Si todo ha ido bien, en el Private Zone DNS deberías de ver la conexión con aks-vnet:

Crear una cuenta de almacenamiento y un endpoint privado para ella
Ahora que ya tienes la red virtual donde va a estar el AKS y la cuenta de almacenamiento, así como el DNS privado que resolverá los nombres, lo siguiente que voy a crear es la cuenta de almacenamiento:
# Create an Azure Storage only accesible via private link
# So you need:
# 1. Create a storage account
az storage account create -g $RESOURCE_GROUP \
--name $PRIVATE_STORAGE_ACCOUNT \
--sku Standard_ZRS \
--default-action Deny
STORAGE_ACCOUNT_ID=$(az storage account show --name $PRIVATE_STORAGE_ACCOUNT --resource-group $RESOURCE_GROUP --query "id" --output tsv)
En este caso es importante señalar que he puesto como –default-action a Deny, lo cual significa que nadie tiene acceso desde fuera por defecto.
Ahora lo siguiente que vamos a hacer es crear un private endpoint:
# 2. Create a private endpoint for the storage account
az network private-endpoint create --resource-group $RESOURCE_GROUP \
--name $PRIVATE_STORAGE_ACCOUNT \
--vnet-name $VNET \
--subnet $SUBNET_ENDPOINTS \
--location $LOCATION \
--connection-name $PRIVATE_STORAGE_ACCOUNT \
--group-id file \
--private-connection-resource-id $STORAGE_ACCOUNT_ID
Esto lo que hace es crear una NIC asociada a esta cuenta de almacenamiento:

lo que le permitirá tener una IP dentro de la subred que hemos indicado, correspondiente a la VNET que creamos anteriormente:

Recuperamos los valores de la NIC, para poder obtener la IP asociada dentro de la red desde el script, y damos de alta un nuevo registro en nuestro DNS privado para la cuenta de almacenamiento:
# 3. Get the ID of the azure storage NIC
STORAGE_NIC_ID=$(az network private-endpoint show --name $PRIVATE_STORAGE_ACCOUNT -g $RESOURCE_GROUP --query 'networkInterfaces[0].id' -o tsv)
# 4. Get the IP of the azure storage NIC
STORAGE_ACCOUNT_PRIVATE_IP=$(az resource show --ids $STORAGE_NIC_ID --query 'properties.ipConfigurations[0].properties.privateIPAddress' --output tsv)
# 5. Setup DNS with the new private endpoint
# The record set name to the private IP of the storage account
az network private-dns record-set a add-record \
--record-set-name $RECORD_SET_NAME \
--resource-group $RESOURCE_GROUP \
--zone-name $PRIVATE_DNS_ZONE \
--ipv4-address $STORAGE_ACCOUNT_PRIVATE_IP
El resultado debería de ser el siguiente:

Crear el clúster de Kubernetes
Ahora que ya tenemos todas las piezas anteriores, lo último que nos queda, en cuanto a infraestructura, es el clúster de Kubernetes. Para ello podemos lanzar los siguientes comandos:
#Create a Service Principal
az ad sp create-for-rbac -n $AKS_NAME --role Contributor \
--scopes $STORAGE_ACCOUNT_ID $VNET_ID > auth.json
appId=$(jq -r ".appId" auth.json)
password=$(jq -r ".password" auth.json)
#Create AKS cluster
az aks create \
--resource-group $RESOURCE_GROUP \
--name $AKS_NAME \
--generate-ssh-keys \
--load-balancer-sku standard \
--node-count 3 \
--zones 1 2 3 \
--vnet-subnet-id $SUBNET_AKS_ID \
--service-principal $appId \
--client-secret $password
# Get AKS credentials
az aks get-credentials --resource-group $RESOURCE_GROUP --name $AKS_NAME
Es importante que el Service Principal tenga permisos tanto sobre la VNET como sobre la cuenta de almacenamiento, para poder gestionar las acciones que se vayan solicitando. Este clúster, como comentamos al principio del artículo tiene habilitada la multi-zona, que simplemente es añadir la opción –zones y las zonas en las que quieres que se desplieguen los nodos (en este caso en la 1, 2 y 3). Una vez que termine podrás recuperar las credenciales de tu clúster y comenzar con las pruebas.
Comprobar que es un clúster multi-zona
Lo primero que me gustaría comprobar es que efectivamente el clúster se ha desplegado en las zonas que le he indicado. Para ello es tan sencillo como lanzar el siguiente comando:
#See the zone for the nodes
kubectl get nodes -o custom-columns=NAME:'{.metadata.name}',REGION:'{.metadata.labels.topology\.kubernetes\.io/region}',ZONE:'{metadata.labels.topology\.kubernetes\.io/zone}'
En mi ejemplo, que está desplegado en North Europe veré una salida como la siguiente:
Instalar el driver CSI para Azure Files
Ahora, para poder configurar Azure Files con private link es necesario usar el driver Azure Files CSI (Container Storage Interface). Para ello puedes hacerlo con el siguiente comando:
# Install CSI Driver for Azure Files
curl -skSL https://raw.githubusercontent.com/kubernetes-sigs/azurefile-csi-driver/v1.5.0/deploy/install-driver.sh | bash -s v1.5.0 --
Este creará diferentes recursos dentro de tu clúster que ayudarán a saber cómo gestionar la configuración que daremos de alta como una nueva Storage Class.
Nota: si quieres crear todo lo creado hasta ahora de manera dinámica, lee este otro artículo donde el driver CSI hace todo esto por ti.
Storage Class para volúmenes persistentes con Private Link
Tal y como indica la documentación oficial, y siguiendo con las variables de nuestro ejemplo, lo primero que voy a crear es una Storage Class que me permita interactuar con la cuenta de almacenamiento que hemos configurado a través de su enlace privado:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: azurefile-csi
provisioner: file.csi.azure.com
allowVolumeExpansion: true
parameters:
resourceGroup: AKS-Demo
storageAccount: webapache
server: web-apache-storage.private.file.core.windows.net
shareName: content
reclaimPolicy: Delete
volumeBindingMode: Immediate
mountOptions:
- dir_mode=0777
- file_mode=0777
- uid=0
- gid=0
- mfsymlinks
- cache=strict
- nosharesock
- actimeo=30
Como ves, en ella a parte de especificar como provisioner file.csi.azure.com utilizo como parámetros el grupo de recursos donde está mi cuenta de almacenamiento, el nombre de la cuenta y el nombre completo que registré en mi zona DNS privada.
Persistent Volume Claim usando la nueva Storage Class
En este ejemplo, vamos a generar de forma dinámica los recursos que hacen falta y es por ello que voy a hacer uso de un PersistentVolumeClaim como el siguiente, que utiliza la storage class anterior:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: apache-content
spec:
resources:
requests:
storage: 50Gi
volumeMode: Filesystem
storageClassName: azurefile-csi
accessModes:
- ReadWriteMany
Aplicación de ejemplo
Ya hemos llegado al final y es que solo queda probar que todo esto que «te he hecho configurar» 🙂 funciona correctamente. Para ello he creado una página en PHP muy sencilla que simplemente muestra el nombre del pod y el nodo que está sirviendo esta página:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Apache Web Server Information</title>
</head>
<body>
<h1>Apache Web Server Information</h1>
<p>
<ul>
<li><strong>Pod name</strong>: <?php echo gethostname(); ?></li>
<li><strong>Node name</strong>: <?php echo getenv('NODE_NAME') ?></li>
</ul>
</body>
</html>
Para probarlo, primero voy a crear un despliegue con tres réplicas que utilice un Apache, con PHP configurado, que utilice el PersistentVolumeClaim anterior, que a su vez hace uso de la StorageClass que se entiende con la cuenta de almacenamiento por Private Link:
apiVersion: apps/v1
kind: Deployment
metadata:
name: apache-web-server
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: apache
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
image: php:7.2-apache
ports:
- containerPort: 80
volumeMounts:
- mountPath: /var/www/html
name: html
volumes:
- name: html
persistentVolumeClaim:
claimName: apache-content
Si te fijas, lo que hago es montar sobre la ruta que el Apache sirve los archivos un File Share de nuestra cuenta de almacenamiento, por lo que si todo va bien cuando se generen mis pods, se cree la carpeta content, todo lo que suelte ahí estará siendo servido por las tres réplicas.
Por último, creo un recurso de tipo Service que me facilite una IP pública para probar el escenario:
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 80
targetPort: 80
Si ejecuto todos estos archivos:
k apply -f k8s/
Deberá crearse la StorageClass, el PersistentVolumeClaim, el Service, y el Deployment (y su ReplicaSet) con tres Pods:
Como puedes ver, los pods se están ejecutando correctamente, la IP pública ya está asignada (52.155.89.205) y si compruebo los persistent volume claims y persistent volumes verás que tenemos uno creado, que además está montado en todas las réplicas, usando la clase que definimos:

Si ahora accedemos a la IP pública verás que no ves nada 🙂
Y esto es porque todavía no hemos subido el archivo index.php a la carpeta compartida que se ha creado como resultado de nuestra petición de volumen, a través del PersistentVolumeClaim. Para subir dicho archivo puedes hacer uso de este comando:
STORAGE_KEY=$(az storage account keys list -g $RESOURCE_GROUP -n $PRIVATE_STORAGE_ACCOUNT --query '[0].value' -o tsv)
# Upload file to azure files share
az storage file upload --source apache-content/index.php \
--account-name $PRIVATE_STORAGE_ACCOUNT \
--account-key $STORAGE_KEY \
--share-name content
Sin embargo, date cuenta de que al ser una cuenta de almacenamiento que solo permite el acceso privado a través de la VNET por defecto no es posible hacer esta subida desde tu local:

De hecho, si intentas acceder desde el portal al nuevo File Share verás el siguiente mensaje de error:

Para poder hacer la subida de este archivo para esa prueba necesitas habilitar en el Firewall de la cuenta de almacenamiento tu IP pública desde la que estás accediendo:

Una vez hecho esto podrás lanzar de nuevo el comando anterior sin problemas.
A través de la IP pública del despliegue de apache-web-server podrás ver el resultado de todos los pods que sirven el contenido a través del File Share de la cuenta de almacenamiento a la que se está accediendo a través de private link.
Por otro lado, si quisieras probar los pods forzando el nodo, y por lo tanto la zona, puedes usar este otro ejemplo:
apiVersion: v1
kind: Pod
metadata:
name: northeurope-1
labels:
name: northeurope-1
spec:
containers:
- name: apache
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
image: php:7.2-apache
ports:
- containerPort: 80
volumeMounts:
- mountPath: /var/www/html
name: html
volumes:
- name: html
persistentVolumeClaim:
claimName: apache-content
nodeName: aks-nodepool1-25530853-vmss000000
---
apiVersion: v1
kind: Service
metadata:
name: web-in-northeurope-1
spec:
type: LoadBalancer
selector:
name: northeurope-1
ports:
- port: 80
targetPort: 80
Solo debes cambiar el valor nodeName por el nombre del nodo donde quieres desplegar el pod.
El código del ejemplo lo tienes en mi GitHub. También te dejo el artículo de mi compañero Carlos Mendible, que ha hecho un ejercicio parecido ¡además con Bicep y Terraform!
¡Saludos!