Apache HTTP Server en AKS con Azure Files

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:

Nueva cuenta de almacenamiento para el volumen persistente

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.

Apache web server no funciona correctamente con AKS y Azure Files

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.

Archivo corrupto servido por Apache HTTP Server

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.

Apache Web Server funciona con AKS y Azure Files si deshabilitas EnableMMAP

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

¡Saludos!