En la entrada anterior indicaba la idea general en la que estoy trabajando para implementar una solución funcional de documentación como código.
Reducida a su mínima expresión, la prueba de concepto lo que tiene que mostrar es la velocidad a la que se puede ir actualizando la documentación si se sigue el mismo proceso -y herramientas- de desarrollo a las que está acostumbrado el equipo de proyecto.
No se trata de crear un sistema listo para producción, sino de mostrar algo que funcione ™ más o menos, como funcionaría la solución final.
La prueba de concepto se ha montado sobre un clúster mono-nodo de K3s:
Job (o ConJob) como “pipeline” de construcción
En el escenario ideal, el repositorio de código dispararía un webhook al recibir un commit o una pull request. En respuesta al evento, el orquestador ejecutaría las tareas de la pipeline para construir una nueva versión de la documentación (con los últimos cambios introducidos en el commit / pull request).
En la prueba de concepto, el webhook se simula mediante la ejecución manual de un Job (o periódica, si se usa un CronJob).
Las tareas de la pipeline son dos comandos concatenados en un script: git clone
y mkdocs build
; como el volumen donde se construye la versión web de la documentación es el mismo usado para publicar la documentación generada, tenemos cubierta automáticamente la parte de continuous deployment.
Publicación web (CD)
La rama derecha del diagrama describe los objetos de Kubernetes implicados en la publicación de contenido web.
Volumen
La clave de la prueba de concepto está en usar un volumen como “elemento común” para la construcción y para el deployment. Como comenté en la entrada anterior en el apartado de Publicación de la documentación, otras opciones más robustas complican la solución, así que opté por esta solución de espítiru macgyvero porque funciona y sobretodo, porque sirve para mostrar el concepto evitando la complejidad técnica.
Namespace
Para aislar los recursos de la prueba de concepto del resto de aplicaciones en el clúster, si las hubiera, creo el namespace doc-as-code
:
---
kind: Namespace
apiVersion: v1
metadata:
name: doc-as-code
Volumen compartido
El Persistent Volume Claim website-pvc
se monta en los pods basados en Nginx en modo read only.
Al crear el PVC, como no especificamos una storageClass, usamos la storageClass por defecto en el clúster. En el caso de K3s se usa local-path
, que permite la provisión dinámica de volúmenes de tipo hostPath
.
El fichero de definición del PVC:
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: website-pvc
namespace: doc-as-code
labels:
app.kubernetes.io/name: doc-as-code-pvc
app.kubernetes.io/component: storage
app.kubernetes.io/part-of: doc-as-code
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
Aplicamos algunas de las etiquetas recomandadas (Recommended Labels) de la documentación oficial de Kubernetes.
Servidor web
Usamos Nginx para servir la documentación en formato HTML generada por MkDocs.
Los ficheros servidos se publican desde el volumen montado en la ruta por defecto para Nginx /usr/share/nginx/html
(en modo ReadOnly).
Deployment
El Deployment está basado en Nginx y monta el volumen que contiene la documentación en formato web.
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: doc-as-code-nginx
namespace: doc-as-code
labels:
app.kubernetes.io/name: doc-as-code-nginx
app.kubernetes.io/component: webserver
app.kubernetes.io/part-of: doc-as-code
spec:
selector:
matchLabels:
app.kubernetes.io/name: doc-as-code-nginx
app.kubernetes.io/component: webserver
app.kubernetes.io/part-of: doc-as-code
replicas: 1
template:
metadata:
labels:
app.kubernetes.io/name: doc-as-code-nginx
app.kubernetes.io/component: webserver
app.kubernetes.io/part-of: doc-as-code
spec:
containers:
- name: nginx
image: nginx:stable-alpine
imagePullPolicy: IfNotPresent
ports:
- name: http-tcp
containerPort: 80
volumeMounts:
- name: webdocs
mountPath: /usr/share/nginx/html
readOnly: true # Montamos el volumen como ReadOnly en el webserver
volumes:
- name: webdocs
persistentVolumeClaim:
claimName: website-pvc
Service
Publicamos el servicio de forma interna usando ClusterIP
, porque la web será accesible desde fuera del clúster a través de un Ingress:
---
kind: Service
apiVersion: v1
metadata:
name: doc-as-code-web
namespace: doc-as-code
labels:
app.kubernetes.io/name: doc-as-code-service
app.kubernetes.io/component: webserver-service
app.kubernetes.io/part-of: doc-as-code
spec:
ports:
- port: 80
name: http-tcp
selector:
app.kubernetes.io/name: doc-as-code-nginx
app.kubernetes.io/component: webserver
app.kubernetes.io/part-of: doc-as-code
Ingress
La configuración del Ingress puede depende del (o de los) Ingress Controller que haya desplegados en el clúster.
En K3s el Ingress Controller por defecto es Traefik.
El nombre del host
docs.k3s.vm.lab
está definido en el fichero/etc/hosts
del equipo cliente desde el que se realiza la demo.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: doc-as-code-traefik-ingress
namespace: doc-as-code
labels:
app.kubernetes.io/name: doc-as-code-traeffik-ingress
app.kubernetes.io/component: ingress
app.kubernetes.io/part-of: doc-as-code
spec:
rules:
- host: docs.k3s.vm.lab
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: doc-as-code-web
port:
number: 80
Build
Repositorio
Para simplificar, se usa un repositorio público en GitHub. De esta forma evitamos tener que desplegar infraestructura adicional.
Al usar un repositorio público se evitan las complicaciones de la autenticación para acceder a un repositorio privado.
El repositorio público contiene un sitio de ejemplo elaborado con MkDocs.
Job - tarea 1: clonar el código fuente
La imagen squidfunk/mkdocs-material contiene Git, lo que permite clonar el repositorio sin necesidad de tener que usar una imagen personalizada en la que se haya instalado Git.
La imagen base declara la carpeta de trabajo como WORKDIR /docs
en el Dockerfile
; clonamos el repositorio en esa carpeta:
git clone https://github.com/onthedock/k8s-devops.git /docs
La URL del repositorio la pasaremos al contenedor a través de una variable de entorno cargada desde un ConfigMap.
Generamos el ConfigMap usando la opción --dry-run=client
:
kubectl create configmap doc-as-code-repo-url \
--from-literal repo_url=https://github.com/onthedock/k8s-devops.git \
--dry-run=client -o yaml | tee doc-as-code-repo-url-configmap.yaml
Esto genera, después de añadir algunas etiquetas adicionales:
apiVersion: v1
data:
repo_url: https://github.com/onthedock/k8s-devops.git
kind: ConfigMap
metadata:
namespace: doc-as-code
creationTimestamp: null
name: doc-as-code-repo-url
labels:
app.kubernetes.io/name: doc-as-code-configmap
app.kubernetes.io/component: configuration
app.kubernetes.io/part-of: doc-as-code
Job - tarea 2: construir la documentación en formato web
La segunda tarea de la pipeline es la contrucción de la documentación en formato web usando MkDocs.
Hemos clonado el código fuente en la carpeta docs/
, que es donde MkDocs espera encontrar el fichero mkdocs.yaml
con la configuración del sitio web a construir : mkdocs build ...
Job - tarea 3 (bueno, 2, continuada): publicar la documentación en el servidor web
Usamos la opción --site-dir
del comando mkdocs build
para generar la documentación web en la ruta /usr/share/nginx/html
del volumen montado. El volumen también está montado en el pod de Nginx, por lo que el servidor web publicará automáticamente los ficheros HTML, CSS, etc ubicados en esa carpeta.
Esto nos ahorra tener que copiar los ficheros web generados al servidor Nginx y simplifica la prueba de concepto.
El fichero de definición del Job queda:
---
kind: Job
apiVersion: batch/v1
metadata:
namespace: doc-as-code
labels:
app.kubernetes.io/name: doc-as-code-build
app.kubernetes.io/component: builder
app.kubernetes.io/part-of: doc-as-code
generateName: doc-as-code-builder-
spec:
template:
metadata:
labels:
app.kubernetes.io/name: doc-as-code-build
app.kubernetes.io/component: builder
app.kubernetes.io/part-of: doc-as-code
spec:
restartPolicy: Never
containers:
- name: doc-as-code-builder
image: squidfunk/mkdocs-material
imagePullPolicy: IfNotPresent
env:
- name: DOCS_REPO_URL
valueFrom:
configMapKeyRef:
name: doc-as-code-repo-url
key: repo_url
volumeMounts:
- name: website-docs
mountPath: /usr/share/nginx/html
command: ["/bin/sh"]
args:
- "-c"
- "git clone $DOCS_REPO_URL /docs && mkdocs build --site-dir /usr/share/nginx/html"
volumes:
- name: website-docs
persistentVolumeClaim:
claimName: website-pvc
Ejecución
Como hemos comentado al principio, ejecutamos el Job manualmente cada vez que se actualice la documentación en el repositorio:
kubectl create -f job.yaml
En los logs del pod generado por el Job observamos que se genera la documentación en /usr/share/nginx/html
(desde el pod de MkDocs):
Cloning into '/docs'...
INFO - Cleaning site directory
INFO - Building documentation to directory: /usr/share/nginx/html
INFO - Documentation built in 2.68 seconds
Resumen
La solución propuesta en este artículo permite hacer una demo rápida del concepto de documentación como codigo sin demasiadas complicaciones técnicas ni uso excesivo de recursos.
En las siguientes entradas escalaremos esta prueba de concepto en un escenario algo más realista; aunque seguimos usando un clúster mono-nodo, desplegamos Gitea y usamos Tekton Pipelines para actualizar automáticamente la construcción de la documentación cada vez que se guarda un cambio en el repositorio.