Si quieres usar el servidor web de Apache con varios frontales es posible que quieras que el contenido a servir pueda estar compartido por todas las instancias. En un entorno de Kubernetes esta configuración puede conseguirse con lo que se conoce como volúmenes persistentes. Si además nos vamos a un entorno en Microsoft Azure, para que estos volúmenes puedan compartirse entre más de un pod, necesitamos usar Azure Files como tipo. El caso es que cuando he montado dicha configuración esta no estaba funcionando como se espera y es por ello que hoy quiero compartir contigo todos los pasos.
Crea un clúster en AKS
Si todavía no lo tienes, lo primero que necesitas es crear un clúster de Kubernetes en AKS. Para ello puedes hacer uso de los siguientes comandos:
#Variables
RESOURCE_GROUP="k8s-httpd-azure-file"
LOCATION="northeurope"
AKS_NAME="httpd-demo"
AZURE_STORAGE_NAME="httpdfiles"
SHARE_NAME="assets"
#Create resource group
az group create -n $RESOURCE_GROUP -l $LOCATION
# Create AKS cluster
az aks create -n $AKS_NAME -g $RESOURCE_GROUP --node-count 1 --generate-ssh-keys
#Get the context for the new AKS
az aks get-credentials -n $AKS_NAME -g $RESOURCE_GROUP
Crear un volumen persistente, un despliegue y un servicio
Una vez que ya tenemos dónde desplegar nuestro entorno, vamos a necesitar de tres recursos fundamentales:
Persistent Volume Claim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: apache-pvc
spec:
resources:
requests:
storage: 5Gi
volumeMode: Filesystem
storageClassName: azurefile
accessModes:
- ReadWriteMany
En este ejemplo vamos a generar la cuenta de almacenamiento de manera dinámica, para lo que necesitamos un recurso llamado PersistentVolumeClaim. Este nos va a permitir indicar la capacidad que queremos reservar para la carpeta compartida y qué Storage Class queremos usar. Para este ejemplo, como AKS ya crea por nosotros dos StorageClass que cubren tanto Azure Files como Azure Disk he elegido la correspondiente para lo que necesitamos, que es la llamada azurefile.
Deployment
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
image: httpd:2.4
ports:
- containerPort: 80
volumeMounts:
- mountPath: /usr/local/apache2/htdocs/
name: html
volumes:
- name: html
persistentVolumeClaim:
claimName: apache-pvc
Lo siguiente que necesito es el despliegue que define los pods que ejecutarán el servidor web de Apache. En él he indicado que quiero tres réplicas y que en la ruta /usr/local/apache2/htdocs se va a montar el volumen creado a través del PersistentVolumeClaim que acabamos de crear llamado apache-pvc.
Service
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 80
targetPort: 80
Por último el servicio de tipo LoadBalancer me permitirá acceder al servicio desde fuera.
Para aplicar todos estos cambios en tu clúster puedes hacerlo de forma fácil con el siguiente comando:
kubectl apply -f .
Una vez finalice lo que ocurrirá es que se creará una nueva cuenta de almacenamiento en el grupo de recursos secundario que genera AKS:

Si accedes a esta, podrás ver que hay además un share file generado con el tamaño que pusiste en el recurso PersistentVolumeClaim.

Puedes comprobar además que este está asociado con el despliegue si lanzas el siguiente comando:
➜ kubectl describe pvc apache-pvc
Name: apache-pvc
Namespace: default
StorageClass: azurefile
Status: Bound
Volume: pvc-a5e45f33-6ea1-4e79-9b2c-b8ec1bf883da
Labels: <none>
Annotations: pv.kubernetes.io/bind-completed: yes
pv.kubernetes.io/bound-by-controller: yes
volume.beta.kubernetes.io/storage-provisioner: kubernetes.io/azure-file
Finalizers: [kubernetes.io/pvc-protection]
Capacity: 5Gi
Access Modes: RWX
VolumeMode: Filesystem
Mounted By: apache-web-server-77c57ff9b5-7mx75
apache-web-server-77c57ff9b5-mmzzm
apache-web-server-77c57ff9b5-ntfqc
Events: <none>
Como puedes ver, el volumen se ha montado en las tres réplicas generadas gracias a nuestro despliegue.
Ahora bien, según la teoría, todo esto lo hemos montado para poder servir con nuestro Apache todo lo que caiga en este espacio compartido. Para comprobarlo voy a subir un archivo HTML de ejemplo como el que sigue:
<!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 Server in AKS with Azure File</title>
</head>
<body>
Hello world, from Azure Files!
</body>
</html>
Genera un archivo con dicho contenido, o el que tú quieras, y súbelo a esa zona compartida en la nueva cuenta de almacenamiento (puedes hacerlo a través del portal o con Azure Storage Explorer). Ahora recupera la IP pública generada para nuestro Service:
➜ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.0.0.1 <none> 443/TCP 46m
web LoadBalancer 10.0.38.109 20.67.213.172 80:32060/TCP 31m
La sorpresa es que al hacer esto, en lugar de mostrar el contenido como hubieras esperado se obtiene un resultado extraño, que es la descarga un archivo.

Si lo abres comprobarás que es nuestro HTML pero con caracteres especiales, además de cabeceras HTTP, que hacen que el mismo no se interprete como debe, y por lo tanto queda corrupto.

El bug de Apache Server con SMB
Me tiré días para descubrir que lo que realmente estaba sucediendo es que existe un bug que afecta a Apache Server cuando utilizas SMB para el montaje de carpetas, en este caso con Azure Files. Para solucionarlo, debemos modificar la propiedad EnableMMAP a Off. Para hacerlo, podemos hacer una copia del archivo de configuración que viene por defecto en httpd de la siguiente forma:
docker run --rm httpd:2.4 cat /usr/local/apache2/conf/httpd.conf > my-httpd.conf
Y en él añadir la siguiente línea:
EnableMMAP Off
Ahora, lo que voy a hacer es utilizar un ConfigMap donde guardaré esta nueva configuración:
#Create a new config map with Apache configuration
kubectl create configmap httpdconf --from-file=httpd.conf
Y por último modificar el recurso Deployment para montar esta configuración dentro de los pods y que apliquen los cambios:
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
image: httpd:2.4
ports:
- containerPort: 80
volumeMounts:
- mountPath: /usr/local/apache2/htdocs/
name: html
- mountPath: /usr/local/apache2/conf/httpd.conf
name: apache-config
subPath: httpd.conf
volumes:
- name: html
persistentVolumeClaim:
claimName: apache-pvc
- name: apache-config
configMap:
name: httpdconf
Si vuelves a aplicar los cambios, a través de kubectl apply, nuevos pods reemplazarán los antiguos con la nueva configuración que hemos definido. Para comprobar que ahora funciona correctamente, vuelve a acceder a tu Apache a través de la IP pública del servicio y verás tu HTML sin problemas.

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