Abre el directorio de módulos de un repositorio de infraestructura que lleve tres años funcionando y normalmente encuentras los dos modos de fallo uno al lado del otro.
Está el módulo de cubo de almacenamiento, que toma un nombre y crea un cubo, pasando once variables al recurso sin cambiarlas. No aporta nada. Todos los consumidores tienen que conocer igualmente los argumentos del recurso, y cuando alguien necesita una función que el módulo no expone, añade una duodécima variable.
Y está el módulo de aplicación, que crea un balanceador, un servicio de clúster, una base de datos, registros DNS, alarmas y un pipeline, toma cuarenta y tres variables, y tiene un contador en la mitad de sus recursos. Nadie lo modifica, porque el radio de impacto de un cambio son todos los entornos a la vez.
Un buen módulo se sitúa entre estos dos, y el rasgo que lo distingue es que codifica una decisión.
Un módulo debe codificar una decisión, no envolver un recurso
La prueba que hay que aplicar antes de crear uno: ¿representa este módulo una opinión que sostiene tu organización y que de otro modo se repetiría?
Un módulo de cubo que solo crea un cubo suspende la prueba. Un módulo de cubo que crea un cubo con el acceso público bloqueado, cifrado con tu clave gestionada, versionado activado, una regla de ciclo de vida que caduca las versiones no vigentes, registro de accesos hacia tu cubo de registros y un conjunto estándar de etiquetas la aprueba. El segundo no es un envoltorio, es tu estándar expresado como código, y usarlo significa que un equipo obtiene la configuración correcta por defecto y no por acordarse.
Ese es el valor real: no ahorrar teclas, sino hacer que la versión que cumple sea la versión fácil. Es el mismo razonamiento que el argumento del andamiaje en un portal de desarrollo vale lo que su catálogo.
El corolario es que la abstracción prematura es el error más común. Escribe los recursos en línea primero. Cuando el mismo patrón aparezca por tercera vez, y sepas qué partes varían, extrae un módulo. Dos apariciones son una coincidencia.
Interfaz: entradas mínimas, valores por defecto seguros, salidas componibles
Entradas. Expón el conjunto más pequeño de variables que cubra la variación real. Cada variable es un compromiso permanente, porque quitar una es un cambio incompatible. Usa los tipos como corresponde, restringe con bloques de validación donde el valor tenga reglas, y marca los secretos como sensibles.
Resiste la variable de paso. Si un consumidor necesita configurar algo que tu módulo no expone, eso es una señal, o de añadirlo a conciencia, o de concluir que ese consumidor no debería estar usando el módulo.
Valores por defecto. Cada valor por defecto es una decisión tomada en nombre de todos los que usan el módulo, así que haz que el seguro sea el de por defecto. Cifrado activado, acceso público desactivado, protección contra borrado activada, retención fijada, registro habilitado. Un consumidor que necesite la versión insegura puede ponerla explícitamente, y ese ajuste explícito se ve en la revisión de código, que es exactamente donde quieres esa conversación.
Evita valores por defecto para cosas que deberían ser una elección deliberada, como el nombre del entorno o el tamaño de instancia en producción. Una variable obligatoria fuerza una decisión.
Salidas. Exporta lo que un consumidor necesita para componer con otros módulos: identificadores, nombres de recurso, extremos, el grupo de seguridad, el rol. Un módulo con salidas insuficientes obliga a los consumidores a buscar recursos por fuentes de datos, lo que los acopla a tus interioridades.
No saques el objeto de recurso entero. Eso expone todos los atributos y convierte cualquier cambio interno en un cambio potencialmente incompatible.
Versionado, y qué cuenta como incompatible
Los módulos necesitan versiones, y las versiones necesitan significar algo.
Un cambio es incompatible si elimina o renombra una variable o una salida, si cambia un valor por defecto de forma que altere infraestructura existente, o si provoca que un recurso se destruya y se vuelva a crear. Este último es el importante y el que la gente se salta: cambiar el nombre de un recurso dentro de un módulo, o reestructurar una iteración, puede producir un plan que destruye producción. Si tienes que hacerlo, publica los bloques de movimiento que le dicen al estado cómo seguir el cambio.
Los consumidores deberían fijarse a una versión, nunca a una rama. Un módulo referenciado por rama cambia bajo sus consumidores sin un pull request, que es lo contrario de la idea.
Publica a través de un registro si tienes uno, o por etiqueta de git si no. Las etiquetas son perfectamente adecuadas; el registro añade descubrimiento y presentación de documentación, no capacidad.
Mantén un registro de cambios. Nadie lee el código de un módulo para averiguar qué hace una subida de versión.
La cuestión de la composición
Hay dos escuelas, y mezclarlas produce el módulo de cuarenta y tres variables.
Módulos finos compuestos por quien llama. Cada módulo hace una cosa coherente, y una pila los cablea. Más verboso en el punto de llamada, mucho más fácil de cambiar, y cada módulo se puede probar de forma independiente.
Módulos gruesos que construyen una pila entera. Una llamada produce un entorno. Conciso, y se convierte en lo que nadie se atreve a modificar, porque cada cambio afecta a todos los consumidores.
Nuestro valor por defecto son módulos finos con un número pequeño de módulos compuestos deliberados para patrones genuinamente estándar, como un despliegue de servicio que cinco equipos usan igual. Los compuestos reciben revisión más estricta y publicación más lenta.
Una regla relacionada: un módulo no debería crear recursos que sobrevivan a aquello que representa. Un módulo que crea una red compartida junto a una aplicación hace que el ciclo de vida de la aplicación controle el de la red, lo cual aflora la primera vez que alguien destruye una pila.
Documentación y pruebas, brevemente
Genera la documentación de entradas y salidas desde el código con una de las herramientas estándar, conectada a integración continua para que no pueda desactualizarse. Las tablas de variables mantenidas a mano están mal en dos versiones.
Incluye un directorio de ejemplo que funcione. Es documentación que la gente sí lee, y hace además de escenario para las pruebas.
Prueba al nivel que el módulo merezca. Validación y análisis estático para todo, aserciones sobre el plan para los módulos con lógica real, y pruebas de integración que aplican y destruyen para el número pequeño de módulos fundacionales donde un error sale caro. La pirámide está en probar el código de infraestructura.
Lo que se olvida
- La configuración del proveedor es de quien llama. Un módulo que configura su propio proveedor no se puede usar dos veces en una configuración ni entre regiones.
- Contadores e iteraciones sobre el módulo. Un contador basado en índice reordena al quitar un elemento y destruye lo que no es. Prefiere la iteración con claves estables.
- Las etiquetas se fusionan, no se sustituyen. Acepta un mapa de etiquetas y fusiónalo con las propias del módulo, o los consumidores no pueden añadir las suyas.
- Los módulos son cadena de suministro. Un módulo traído de un registro público se ejecuta en tu pipeline con tus credenciales. Revisa o internaliza aquellos de los que dependes.
- Retira explícitamente. Marca las variables antiguas como obsoletas en la documentación y déjalas funcionando una versión en vez de quitarlas y romperle el día a todo el mundo.
Qué hacer esta semana
Coge tu módulo más usado y cuenta sus variables. Después comprueba cuántos consumidores fijan cada una. Cualquier variable que fijen todos los consumidores probablemente no debería tener valor por defecto y debería ser obligatoria; cualquier variable que no fije nadie probablemente debería desaparecer. Esa única auditoría suele reducir la interfaz en un tercio y hace el módulo más fácil de entender de inmediato. Hacemos esta pasada durante la revisión de plataforma de un proyecto cloud.