arquitectura · producción · desarrollo de software · deployment

Producción no necesita todo lo que desarrollo necesita

Por qué las herramientas necesarias para construir un producto no tienen por qué llegar a producción, y cómo separar ambas responsabilidades reduce complejidad, mantenimiento y superficie de fallo.

Cuando una aplicación necesita una herramienta para construirse, resulta muy fácil asumir que esa herramienta también debe existir en producción. Si desarrollamos con Node, instalamos Node en el servidor. Si utilizamos un framework, desplegamos el framework. Si durante el desarrollo necesitamos un gestor de paquetes, parece natural que producción también lo necesite.

A veces es exactamente lo correcto. Otras veces estamos trasladando al servidor una complejidad que solo pertenece al proceso de construcción.

Distinguir ambas cosas es una de esas decisiones aparentemente pequeñas que terminan afectando a mantenimiento, seguridad, despliegues y fiabilidad. Y vuelve a ser un buen ejemplo de algo que los desarrolladores llevamos años aprendiendo: el entorno que construye un producto y el entorno que lo ejecuta no tienen por qué ser el mismo.

Construir y ejecutar son problemas diferentes

Pensemos en una web generada de forma estática. Durante el desarrollo podemos necesitar bastantes piezas: un runtime, dependencias, herramientas de compilación, un framework, procesadores de estilos, validadores y todo lo necesario para transformar nuestro código fuente en el resultado final. Pero después del build quizá lo único que quede sean archivos HTML, CSS, JavaScript e imágenes.

En ese momento aparece una pregunta bastante útil: ¿qué necesita realmente producción para servir esto?

No qué hemos utilizado para crearlo, ni qué tenemos instalado en nuestro equipo, ni qué utiliza el framework durante el desarrollo. Lo importante es qué necesita el producto terminado para funcionar.

La respuesta puede ser sorprendentemente pequeña.

El stack de desarrollo no es automáticamente el stack de producción

Esta diferencia parece evidente cuando se explica así, pero en la práctica es fácil perderla. El entorno de desarrollo está pensado para modificar el sistema: necesita compilar, instalar dependencias, ejecutar herramientas, mostrar errores detallados, lanzar pruebas y facilitar cambios rápidos.

Producción tiene otra responsabilidad. Tiene que ejecutar o servir un resultado conocido de forma fiable.

Son objetivos diferentes, así que no deberíamos sorprendernos de que sus necesidades también lo sean. Una herramienta puede ser imprescindible antes del deployment y completamente innecesaria después.

Esto ocurre constantemente en ingeniería de software. Compiladores, generadores de código, herramientas de empaquetado o dependencias de desarrollo son necesarias para obtener un artefacto, pero no necesariamente forman parte del artefacto que finalmente ejecutamos. Con los frameworks modernos resulta fácil olvidar esa separación porque durante el desarrollo todo parece formar parte de una única experiencia.

Pero el servidor no necesita nuestra experiencia de desarrollo. Necesita lo mínimo necesario para ejecutar el producto.

Cada dependencia que llega a producción tiene un coste

Instalar algo en un servidor no es gratis aunque el software sea gratuito. Hay que mantenerlo, actualizarlo y configurarlo. Puede tener vulnerabilidades, introducir cambios incompatibles, generar procesos adicionales o convertirse simplemente en otro elemento que debemos entender dentro de seis meses cuando algo deje de funcionar.

Individualmente, cualquiera de esos costes puede parecer insignificante. El problema aparece cuando se acumulan.

Un runtime que realmente no necesitamos puede traer consigo un gestor de paquetes. Ese gestor instala dependencias. Las dependencias tienen versiones. Las versiones reciben actualizaciones; algunas son de seguridad y otras cambian comportamientos. De pronto estamos manteniendo una pequeña cadena tecnológica para ejecutar algo que quizá podría haberse servido como un conjunto de archivos.

Reducir componentes no es una cuestión estética. Es reducir cosas que pueden cambiar.

Menos piezas también significa menos superficie de fallo

En desarrollo solemos valorar mucho la capacidad. Queremos herramientas que nos permitan compilar, inspeccionar, depurar y modificar rápidamente. En producción, muchas veces ocurre lo contrario: una capacidad que no necesitamos puede convertirse simplemente en otra forma de que algo falle.

Si el servidor no necesita construir la aplicación, no necesita saber construirla. Si no necesita instalar dependencias, no necesita un gestor de paquetes. Si no necesita ejecutar el framework, no necesita mantener su runtime.

Esto no significa llevar el minimalismo hasta el absurdo. Significa que cada pieza debería estar allí porque cumple una función concreta.

La pregunta útil no es si podríamos instalar algo, sino qué problema de producción resuelve tenerlo instalado. Si la respuesta es “ninguno”, probablemente merece la pena dejarlo fuera.

El artefacto es una frontera muy útil

Hay una forma de pensar en esto que me resulta especialmente clara: considerar el resultado del proceso de construcción como una frontera.

A un lado está desarrollo, con el código fuente, las dependencias, las herramientas, la compilación, las pruebas y todo lo necesario para producir una versión válida. Al otro está producción, que recibe el artefacto terminado.

Esa frontera obliga a concretar qué estamos desplegando realmente. En lugar de pensar simplemente que “desplegamos nuestra aplicación”, podemos preguntarnos qué significa físicamente esa aplicación después de construirla.

Puede ser un binario, una imagen de contenedor, un paquete, una colección de archivos estáticos o un conjunto de artefactos acompañado por una configuración mínima. Lo importante es que producción reciba un resultado definido y verificable, no necesariamente todo el entorno utilizado para obtenerlo.

Esto también mejora los despliegues

Cuando reducimos la distancia conceptual entre “artefacto terminado” y “lo que producción sirve”, los despliegues suelen hacerse más fáciles de razonar.

Podemos construir fuera de producción, validar el resultado y desplegar exactamente aquello que hemos comprobado. El servidor no tiene que reconstruir nuestra aplicación bajo unas condiciones potencialmente distintas, resolver nuevamente dependencias ni confiar en que una instalación siga produciendo mañana exactamente el mismo resultado.

Recibe algo que ya ha sido construido.

Esto simplifica también otra operación que considero fundamental: volver atrás. Si una release es esencialmente un artefacto autocontenido, sustituirla por la anterior puede ser mucho más sencillo que intentar reconstruir en caliente el estado previo de un entorno complejo.

Y cuanto más sencillo sea un rollback, menos heroica necesita ser una recuperación.

Pero menos producción exige más disciplina antes de producción

Hay una contrapartida importante. Eliminar herramientas del servidor no elimina el trabajo; lo desplaza al lugar correcto.

Si producción no compila, debemos asegurarnos de que el build que recibe está bien. Si no instala dependencias, debemos resolverlas antes. Si sirve directamente un artefacto terminado, necesitamos validar ese artefacto antes del cambio.

Build limpio, pruebas, comprobaciones mínimas, staging cuando sea necesario y una estrategia de rollback pasan a ser todavía más importantes. La simplicidad de producción funciona bien cuando el proceso anterior es disciplinado.

De lo contrario solo estaríamos desplazando incertidumbre de un sitio a otro.

No todo debería ser estático

También conviene evitar convertir una buena idea en una regla universal. Hay aplicaciones que necesitan un runtime en producción. Hay lógica de servidor que tiene que ejecutarse allí, procesamiento en tiempo real, autenticación, APIs, tareas asíncronas, conexiones persistentes y muchas otras necesidades legítimas.

En esos casos, eliminar el runtime sería eliminar parte del producto.

El criterio no consiste en hacer producción tan pequeña como sea posible a cualquier precio, sino en hacerla tan pequeña como permita el problema real.

La ingeniería no consiste en elegir siempre la arquitectura más simple. Consiste en evitar complejidad que no está justificando su existencia.

El desarrollo también puede engañarnos

Trabajamos tantas horas dentro de nuestros entornos de desarrollo que terminamos identificándolos con el producto. Vemos el editor, el runtime, las dependencias, los scripts y el framework constantemente, y todo eso parece “la aplicación” porque es el espacio desde el que la construimos.

Pero el usuario nunca ve gran parte de ese sistema y producción tampoco tiene por qué verlo.

Es una distinción parecida a la que existe entre una fábrica y aquello que fabrica. Necesitamos maquinaria para producir algo, pero eso no significa que tengamos que entregar también la fábrica con cada producto.

Puede parecer una comparación exagerada, pero obliga a formular una pregunta muy útil: ¿estamos desplegando el producto o estamos desplegando innecesariamente parte de nuestra cadena de producción?

Otra vez, pensar como programador marca la diferencia

Este tipo de decisiones son una de las razones por las que creo que el conocimiento de desarrollo sigue siendo especialmente valioso aunque las herramientas cambien. No se trata de memorizar qué framework instalar o qué comando ejecutar, sino de aprender a distinguir responsabilidades, entender qué ocurre en cada fase, identificar dependencias reales, preguntarse qué componente necesita conocer a cuál, separar construcción de ejecución, reducir superficie de fallo y diseñar cambios reversibles.

Son patrones que aparecen una y otra vez.

También aparecen ahora al trabajar con IA, agentes, automatizaciones y nuevas capas de herramientas. La tecnología cambia rápidamente, pero muchas de las preguntas importantes siguen siendo sorprendentemente familiares.

Cuando aparece una nueva capacidad, el impulso suele ser incorporarla. Un desarrollador aprende también a preguntar si realmente necesita llegar hasta producción.

La simplicidad útil no consiste en tener menos por tener menos

Hay una versión superficial del minimalismo tecnológico que consiste en presumir de utilizar pocas herramientas. No me interesa demasiado. Si una herramienta resuelve un problema real, quiero poder utilizarla.

La simplicidad que me interesa es otra: que cada componente tenga una razón para existir.

Desarrollo puede ser sofisticado porque construir productos es un problema sofisticado. Producción puede ser mucho más pequeña si el resultado de ese proceso permite que lo sea. No hay contradicción entre ambas cosas; de hecho, una puede ser consecuencia de la otra.

Un buen entorno de desarrollo puede encargarse de toda la complejidad necesaria para producir un artefacto que después sea extremadamente sencillo de operar. Y esa separación puede terminar siendo una ventaja bastante mayor que ahorrarnos instalar un par de herramientas, porque cada vez que eliminamos de producción algo que producción no necesita, eliminamos también una parte de lo que mañana tendremos que mantener, actualizar, asegurar, diagnosticar y recordar.

Una herramienta necesaria para construir un sistema no tiene por qué formar parte del sistema que lo ejecuta.

Parece una distinción pequeña.

Hasta que empiezas a diseñar producción a partir de ella.