Si has usado Kubernetes un poco, seguro que conoces el concepto de sidecar container: Un contenedor que se ejecuta en el mismo pod que el contenedor principal y que ofrece servicios adicionales. Es muy habitual en implementaciones de Service Mesh tales como Istio. También dapr se basa en un sidecar así como Devspaces sin ir más lejos, por poner solo tres ejemplos.
Todo eso viene a cuento, porque cuando usas uno de esos sistemas, tus deployments son modificados automáticamente por el sistema para añadir el sidecar container. Es decir, el YAML que Kubernetes recibe no es el YAML que tu instalas, y el responsable es precisamente un admission webhook que suele ser la forma usada en esos casos para realizar esas modificaciones.
Qué és un admission webhook
Como su nombre indica es un “webhook” es decir, un endpoint http al que Kubernetes llamará cuando ocurra un determinado evento, tal como que se va a crear un pod o modificar un servicio. Dichos webhooks se suelen ejecutar en el clúster como contenedores, por lo que pueden ser desarrollados en cualquier lenguaje. Por lo general, todos los ejemplos los verás en Go, ya que este es el lenguaje de facto para Kubernetes y tiene una librería impresionante para interactuar con el clúster. Pero, como son contenedores, los puedes crear en cualquier lenguaje.
Para instalar un webhook, a grandes rasgos, solo debes hacer dos cosas:
- Desplegar el contenedor en el clúster (eso incluye, básicamente, el deployment y el servicio tal y como harías en cualquier otro caso)
- Configurar el clúster para que use el nuevo webhook. Eso se hace a través de un objeto propio de Kubernetes de tipo
ValidatingWebhookConfiguration
oMutatingWebhookConfiguration
en función de si nuestro webhook solo valida o también modifica los datos.
Efectivamente un admission webhook puede o bien:
- Validar que el objeto que se va a crear/modificar es válido según determinadas reglas y aceptar/rechazar dicha acción
- Mutar un objeto creado/modificado (p. ej. para añadir un sidecar container)
En el primer caso hablamos de un “validating webhook” y en el segundo de un “mutating webhook”. Aunque técnicamente, los “mutating webhooks” también pueden actuar como “validating webhooks” ya que tienen la potestad de denegar objetos, es mejor tener esas responsabilidades separadas. Además Kubernetes llama primero a los “mutating webhooks” y luego a los “validating webhooks”. Así de este modo un “validating webhook” siempre recibe la versión “real final” (ya modificada por los “mutating webhooks”) para validarla y decidir si la acepta o no.
Ambos webhooks reciben el mismo tipo de peticiones. Un objeto de tipo AdmissionReview
que tiene el formato tal y comp sigue:
|
|
Nota: Actualmente hay dos versiones de
AdmissionReview
(lav1beta1
y lav1
). El webhook puede elegir cuales acepta y en caso de aceptar todas, Kubernetes envía siempre la primera versión disponible.
Tenemos pues información sobre la acción (CREATE
en ese ejemplo), el usuario que la ha realizado, el espacio de nombres y en object
está todo el objeto que en este caso se va a crear. Así, si se crea un pod aquí tendrás toda la definición del pod (en json). Por lo tanto, el webhook puede inspeccionar este objeto y decidir si acepta o no esa acción. Interesantes son los atributos kind
y resource
. El primero tiene el tipo del recurso (lo que vendrá serializado en el campo object
) mientras que el segundo indica el recurso que está siendo modificado. A veces kind
y resource
coinciden, pero no siempre. P. ej. si se escala un deployment, en resource
tendrás el deployment que se escala, pero en kind
lo que tendrás el objeto Scale
(de autoascaling/v1
) asociado.
Un admission webhook debe responder con un código HTTP 200 y en el cuerpo de la respuesta (Content-Type: application/json
) debe ser un objeto AdmissionReview
(sí, el mimso tipo recibido, aunque se usan otros campos). Si es un “validating webhook” basta con una respuesta como la siguiente:
|
|
Con eso, se acepta o no la petición recibida.
Los “mutating webhook” pueden modificar la petición recibida y para ello usan los siguientes campos de response
:
patchType
: Indica el tipo de modificación. A día de hoy solo se soportaJSONPatch
.patch
: Las modificaciones a realizar
El campo patch
es el divertido: Se trata de un array de modificaciones JSONPatch codificado en BASE64.
Un ejemplo en .Net Core
Todo eso está muy bien, pero basta de cháchara y veamos un ejemplo en .Net Core. Para mantener el código bajo mínimos he usado FeaterHttp de David Fowler que permite crear aplicaciones ASP.NET Core de forma muy sencilla. Este es el código de Program.cs
:
|
|
Como FeatherHttp no está en el feed de nuget oficial, debo usar un nuget.config
propio:
|
|
No hay clase Startup
ni nada. Simplemento enruto las llamadas HTTP POST a /validate
a un DelegateRequest
definido y creo un fallback (solo para depuración porque no se llama nunca).
La clase Webhook
define el método CheckAndRun
que es el DelegateRequest
:
|
|
Esta clase solo valida que el content-type de la petición sea application/json
. Si lo es, deserializa el JSON recibido a un dynamic
y lo pasa al delegado que se ha especificado en el constructor. Solo comentar que uso Newtonsoft.Json para deserializar el JSON, porque System.Text.Json
no soporta dynamic
todavía.
Finalmente nos queda el código propio de validación, en mi caso ubicado en el método Validate.Run
:
|
|
Simplemente accedo al campo spec.containers[0].image
del objeto recibido (un pod) y miro si el tag es latest
. En caso de que sea latest
denego la petición. Simple, pero como ejemplo ya sirve :P Me apoyo en el método de extensión GenerateResponse
:
|
|
Y ya, no hay más código. Ya tengo mi webhook listo… Creo una imagen de Docker y… ¡a instalarlo!
Instalando el mutating webhook
Eso en teoría es sencillo: solo debo crear un servicio, un deployment y el objeto ValidatingWebhookConfiguration
. Pero, hay un pequeño temilla: los admission webhooks solo se pueden llamar via HTTPS. Y sí… eso implica un certificado :)
A ver, hay varias maneras de generar este certificado. Por lo general os encontraréis que mucha gente usa un script bash, para generar un certificado ya sea autofirmado o bien firmado por la propia CA del clúster (usando un CertificateSigningRequest
). Ambos sistemas funcionan, pero hay una alternativa que es usar Helm para generar un certificado y de este modo todo se puede desplegar via Helm. Para ello haremos uso de las funciones criptográficas de Helm, tal y como se menciona en este post. Así, vamos a crear un CA, un certificado propio y lo usaremos. La plantilla de Helm es como sigue:
|
|
La plantilla crea dos objetos: el propio ValidatingWebhookConfiguration
y un secreto para guardar el certificado TLS. Del primero me interesa comentaros el campo webhooks
que contiene una lista de los webhooks a configurar. Para cada webhook configuramos (en rules
) sobre qué objetos y operaciones aplica este webhook (p. ej. en mi caso al crear un pod usando la API v1
) y luego usando clientConfig
le indicamos donde está dicho webhook. En este caso está en un servicio (service
) ejecutándose en Kuberntes y con el nombre indicado. Y en el campo caBundle
le debemos indicar el certificado TLS.
En mi caso el servicio se llama samplewh
y el certificado debe usar este CN, así como los siguientes nombres alternativos: samplewh.default
(donde default
es el espacio de nombres del servicio) y samplewh.default.svc
. Esos nombres alternativos son los que se colocan en la variable $altNames
del chart.
Guay… Ahora solo nos falta una cosa…
Leer el certificado TLS desde Kestrel y configurar HTTPS
Aquí tenemos dos acciones a realizar. La primera es que Kestrel no soporta por configuración usar certificados PEM, ya que espera en su lugar certificados PKCS#12 (vamos, ficheros .pfx
). Con openssl es fácil pasar de uno a otro. Para que esa conversión ocurra sin que nadie tenga que lanzar script alguno, la he puesto en un init container de Kubernetes. Al iniciar el pod dicho contenedor simplemente ejecuta openssl
y convierte el certificado de PEM
a PKCS#12
y este certificado en .pfx
es el que se pasa a Kestrel. Para ello, el init container simplemente ejecuta un fichero .sh
que recibe a través de un config map:
|
|
Desde el chart se crea un config map con dicho fichero:
|
|
Y dicho config map se mapea como volúmen al init container que de este modo puede acceder y ejecutarlo:
|
|
El volúmen pfx
está montado en ambos contenedores (el init container y el que ejecuta el webhook) y es donde el primero deja el fichero .pfx
que lee el segundo.
Finalmente paso las siguientes variables de entorno a Kestrel para cargar el certificado .pfx
y habilitar HTTPS:
|
|
Así establecemos la ruta del fichero .pfx
, la contraseña (la establece el init container al hacer la conversión) y el puerto para Https.
¡Y listos! Ya tenemos nuestro admission webhook:
Como se puede ver en la imagen, el pod que tiene la imagen con la etiqueta latest
es denegado por el admission webhook y Kubernetes no nos permite crearlo. Por otro lado, el otro pod que tiene cualquier otra etiqueta si que se crea sin problemas.
¡Espero que os haya resultado interesante!
PD: Tenéis todo el código en este repo de GitHub.