Saltar a contenido

La Metodología de 12 Factores (12-Factor App)

¿Qué es la "12-Factor App"? 🤔

La "12-Factor App" es un conjunto de 12 principios que sirven como guía para construir aplicaciones web modernas, especialmente aquellas que se ejecutan en la nube (como microservicios). Piensen en ella como un "manual de buenas prácticas" para garantizar que su aplicación sea fácil de desplegar, escalar y mantener a largo plazo.

¿Por qué usar la 12-Factor App?

  • Portabilidad: Su aplicación funciona en cualquier entorno (desarrollo, staging, producción) sin necesidad de grandes cambios.
  • Escalabilidad: La aplicación se puede escalar fácilmente de forma horizontal (agregando más instancias).
  • Mantenibilidad: Facilita el mantenimiento y la actualización de la aplicación a lo largo del tiempo.
  • Agilidad: Permite que el equipo de desarrollo trabaje de forma más eficiente y con menos dolores de cabeza.

¿Cómo nos ayuda la 12-Factor App en este proyecto? 🛠️

Vamos a analizar los factores que pueden ayudarnos a construir mejores aplicaciones:

  1. I. Codebase (Base de Código): Un código base rastreado en un sistema de control de versiones

    • Qué es: Una base de código por aplicación, rastreada en un sistema de control de versiones como Git. Múltiples despliegues comparten la misma base de código.
    • Cómo ayuda: Garantiza que todos los miembros del equipo estén trabajando con la misma versión del código y facilita el seguimiento de los cambios.
    • En Nuestro Proyecto: ¡Ya estamos usando Git, lo cual es genial! Asegúrense de que todos los commits sean pequeños y descriptivos, facilitando la comprensión del historial.

      • Ejemplos de Mensajes de Commit Descriptivos:
        • feat: agregar validación de email en el formulario de registro
        • fix: corregir bug que impedía el envío de mensajes
        • refactor: extraer componente de listado de productos
        • docs: agregar documentación para la API de autenticación
        • test: agregar pruebas unitarias para la función de cálculo de flete
  2. III. Config (Configuración): Almacene la configuración en el entorno

    • Qué es: La configuración de la aplicación (ej: URLs de bases de datos, claves de API, secretos) debe almacenarse en el entorno (variables de entorno), no en el código.
    • Cómo ayuda: Permite que la misma aplicación se ejecute en diferentes entornos (desarrollo, staging, producción) sin necesidad de recompilar o modificar el código. Mejora la seguridad, evitando el almacenamiento de información sensible en el código.
    • En Nuestro Proyecto: ¡Eviten el "hardcoding" de configuraciones! Usen variables de entorno para almacenar información sensible y específica de cada entorno.

      • Spring Boot: usen @Value("${variable.entorno}")

        @Value("${spring.datasource.url}")
        private String dbUrl;
        
      • FastAPI: usen os.environ.get("VARIABLE_ENTORNO")

        import os
        db_url = os.environ.get("DATABASE_URL")
        
      • Next.js:

        • Las variables que comienzan con NEXT_PUBLIC_ se exponen automáticamente al navegador.
        • Use el archivo .env.local para variables específicas de su entorno local (¡no debe ser subido al repositorio!).
        • Ejemplo:
          • En .env.local:
            NEXT_PUBLIC_API_URL=http://localhost:3000/api
            
          • En su componente de Next.js:
            function MiComponente() {
              const apiUrl = process.env.NEXT_PUBLIC_API_URL;
              // ...
            }
            
  3. VIII. Logs (Registros): Trate los logs como flujos de eventos

    • Qué es: La aplicación debe escribir sus logs en stdout (salida estándar). El entorno de ejecución se encarga de recolectar y almacenar estos logs.
    • Cómo ayuda: Simplifica la recolección y el análisis de logs, permitiendo el uso de herramientas como ELK Stack (Elasticsearch, Logstash, Kibana) o Grafana Loki.

      • Niveles de Log:
        • DEBUG: Información detallada para depurar durante el desarrollo. (ej: valores de variables, llamadas a funciones)
        • INFO: Información general sobre el funcionamiento de la aplicación. (ej: "Servidor iniciado", "Solicitud recibida")
        • WARN: Indica un evento inesperado, pero que no impide que la aplicación siga funcionando. (ej: "Intento de acceso a recurso no autorizado")
        • ERROR: Indica un error que impide a la aplicación realizar una determinada acción. (ej: "Fallo al conectar con la base de datos")
        • FATAL: Indica un error crítico que impide que la aplicación siga funcionando. (ej: "Memoria insuficiente", generalmente causa la terminación de la aplicación)
    • En Nuestro Proyecto: Configuren sus aplicaciones para escribir logs estructurados en stdout. Usen los niveles de log de forma apropiada. Una buena práctica es usar un formato JSON para los logs, facilitando el análisis.

      • Información Esencial en los Logs:

        • Timestamp: Cuándo ocurrió el evento.
        • Nivel de Log: DEBUG, INFO, WARN, ERROR.
        • Mensaje: Descripción del evento.
        • Transaction ID: Un ID único para rastrear una transacción de extremo a extremo en varios microservicios.
        • IDs de Objetos/Datos Indexadores: IDs de entidades importantes (ej: ID del cliente, ID del pedido) para facilitar el filtrado y el análisis.
      • Logs de Eventos de Negocio:

        • Es crucial registrar eventos que representen acciones significativas del negocio (ej: "Nuevo pedido creado", "Pago recibido", "Producto enviado").
        • Estos logs pueden usarse para generar métricas de negocio (KPIs) y monitorear el rendimiento de la aplicación (ej: número de pedidos por día, tasa de conversión, tiempo promedio de entrega).
      • Ejemplo de Logging Estructurado (Spring Boot):

        import org.slf4j.Logger;
        import org.slf4j.LoggerFactory;
        import java.util.UUID;
        
        private static final Logger logger = LoggerFactory.getLogger(MiServicio.class);
        
        public void procesarSolicitud(String solicitud, UUID transactionId) {
            logger.info("Solicitud recibida", Map.of(
                "transactionId", transactionId,
                "solicitud", solicitud,
                "timestamp", System.currentTimeMillis()
            ));
        
            try {
               // ... lógica de procesamiento ...
               logger.info("Solicitud procesada con éxito", Map.of("transactionId", transactionId));
            } catch (Exception e) {
               logger.error("Error al procesar la solicitud", Map.of(
                   "transactionId", transactionId,
                   "error", e.getMessage()
               ), e);
            }
        }
        
        public void validarDatos(DatosCliente datos, UUID transactionId) {
            if (!datosValidos(datos)) {
                logger.warn("Datos del cliente inválidos", Map.of(
                    "transactionId", transactionId,
                    "clienteId", datos.getId(),
                    "nombre", datos.getNombre()
                ));
                throw new IllegalArgumentException("Datos inválidos");
            }
            logger.info("Datos validados", Map.of("transactionId", transactionId, "clienteId", datos.getId()));
        }
        
      • Ejemplo de Logging Estructurado (FastAPI):
        import logging
        import json
        import uuid
        
        logger = logging.getLogger(__name__)
        logger.setLevel(logging.INFO)
        
        def procesar_solicitud(solicitud: str, transaction_id: str):
            logger.info(json.dumps({
                "level": "INFO",
                "message": "Solicitud recibida",
                "solicitud": solicitud,
                "transaction_id": transaction_id
            }))
        
            try:
                # ... lógica de procesamiento ...
                logger.info(json.dumps({"level": "INFO", "message": "Solicitud procesada con éxito", "transaction_id": transaction_id}))
            except Exception as e:
                logger.error(json.dumps({
                    "level": "ERROR",
                    "message": "Error al procesar la solicitud",
                    "transaction_id": transaction_id,
                    "error": str(e)
                }), exc_info=True)
        
        def validar_datos(datos, transaction_id: str):
            if not datos_validos(datos):
                logger.warning(json.dumps({
                    "level": "WARN",
                    "message": "Datos del cliente inválidos",
                    "transaction_id": transaction_id,
                    "cliente_id": datos["id"],
                    "nombre": datos["nombre"]
                }))
                raise ValueError("Datos inválidos")
        
            logger.info(json.dumps({"level": "INFO", "message": "Datos validados", "transaction_id": transaction_id, "cliente_id": datos["id"]}))
        
  4. X. Paridad Dev/Prod: Mantenga los entornos lo más similares posible

    • Qué es: Mantener los entornos de desarrollo, staging y producción lo más similares posible. Esto incluye:
      • Código: Usar el mismo código en todos los entornos.
      • Configuración: Usar configuraciones diferentes, pero con la misma estructura.
      • Servicios: Usar los mismos servicios de apoyo (ej: base de datos, cola de mensajes).
    • Cómo ayuda: Reduce el riesgo de bugs y problemas que solo aparecen en producción.
    • En Nuestro Proyecto: Usen contenedores (Docker) para garantizar que la aplicación se ejecute de la misma forma en todos los entornos. Usen herramientas de aprovisionamiento de infraestructura (ej: Terraform) para automatizar la creación de entornos similares.
  5. XI. Procesos: Ejecute la aplicación como uno o más procesos sin estado (stateless)

    • Qué es: La aplicación debe ejecutarse como uno o más procesos sin estado (stateless). Esto significa que la aplicación no debe almacenar ningún estado localmente. El estado debe almacenarse en un servicio de apoyo (ej: una base de datos).
    • Cómo ayuda: Facilita la escalabilidad horizontal de la aplicación.
    • En Nuestro Proyecto: Eviten almacenar datos de sesión localmente. Usen JWT (JSON Web Tokens) para almacenar la información de la sesión de forma segura y sin estado.

      • Next.js y JWT con Cookies HttpOnly:
        • En Next.js, al recibir el JWT después del login, almacénenlo en una cookie HttpOnly.
        • Las cookies HttpOnly no pueden ser accedidas vía JavaScript en el navegador, lo que las hace más seguras contra ataques XSS.
        • En el backend (API), verifiquen el JWT presente en la cookie para autenticar al usuario.

Otros Factores Importantes

Aunque los factores anteriores son los más directamente relacionados con las brechas identificadas, los otros factores de la 12-Factor App también son importantes para construir aplicaciones robustas y escalables:

  • II. Dependencias: Declare y aísle explícitamente las dependencias
    • Ya está implementado en el proyecto base, con el uso de herramientas de gestión de dependencias como Maven, npm, poetry y, por supuesto, docker-compose.
  • IV. Servicios de Respaldo (Backing Services): Trate los servicios de respaldo como recursos adjuntos
    • Con el uso de contenedores y orquestación (Docker + Docker Compose), podemos gestionar la base de datos con facilidad. Además, reforzamos la necesidad de usar pruebas integradas, ya sea usando bases de datos en memoria u orquestando contenedores dentro de la suite de pruebas. Adicionalmente, el uso de ORMs como JPA/Hibernate y Peewee nos desacopla de la tecnología de base de datos SQL, dándonos más flexibilidad para tratarlos como recursos adjuntos y flexibles.
  • V. Build, Release, Run (Construir, Lanzar, Ejecutar): Separe estrictamente las fases de construir, lanzar y ejecutar
    • Tenemos una pipeline en Bitbucket que se encarga de estos pasos. También tenemos esta gestión en cada proyecto con sus respectivas herramientas que aíslan estos pasos, como Maven, Npm y Poetry. Aparte de eso, tenemos un docker-compose-dev.yaml que proporciona una forma de trabajar en el entorno de desarrollo con docker-compose.
  • VI. Vinculación de Puertos (Port Binding): Exporte servicios a través de la vinculación de puertos
    • En este aspecto, optamos por usar un API gateway, pero los servicios se exponen a este gateway a través de un puerto.
  • VII. Concurrencia: Escale mediante el modelo de procesos
    • Es posible configurar esta concurrencia con docker-compose, que permite un ajuste fino de cuántas instancias del contenedor vamos a ejecutar por servicio. Además, las herramientas como Spring, FastAPI y Express realizan esta gestión automáticamente a nivel del proceso del lenguaje.
  • IX. Desechabilidad (Disposability): Maximice la robustez con un inicio rápido y un apagado elegante
    • El uso de contenedores junto con la orquestación con docker-compose lo hace posible.
  • XII. Procesos de Administración: Ejecute tareas administrativas como procesos únicos (one-off)
    • Pedimos que la creación de usuarios administradores, por ejemplo, se haga a través de una línea de comandos que solo el equipo de TI pueda ejecutar. Este es un ejemplo clásico de este factor.

Conclusión 🎉

La 12-Factor App es una guía valiosa para construir aplicaciones modernas y escalables. ¡Al aplicar estos principios, estarán en el camino correcto para crear sistemas más robustos, fáciles de mantener y con menos dolores de cabeza! Recuerden que el objetivo es aprender y aplicar estos conceptos de forma gradual.

Si tienen dudas, ¡no duden en preguntar! Estamos aquí para ayudar. 💪