# CI/CD Multi-Environment Pipeline POC Prueba de concepto de un pipeline CI/CD multi-ambiente con Gitea Actions, Docker y Nginx. ## 📌 Resumen de la Arquitectura Implementación de una estrategia de Integración y Despliegue Continuo (CI/CD) para múltiples entornos (QA, Staging, Producción) alojada en un único servidor VPS (Debian/Ubuntu). La arquitectura utiliza **Gitea** (como control de versiones, Registry de contenedores y Runner de acciones), **Docker** para la contenerización de la aplicación estática, y un servidor **Nginx** en el host (Host Nginx) que actúa como Reverse Proxy y terminador SSL. ## 🏗️ Estrategia de Infraestructura y Red Para aislar los entornos en el mismo servidor sin conflictos de red, se implementó el siguiente flujo: * **Puertos Internos Dockerizados:** Cada entorno corre en un contenedor Docker independiente, exponiendo un puerto único a nivel de `localhost` en el VPS (ej. QA en `8081`, Staging en `8082`, Prod en `8083`). * **Reverse Proxy:** El servidor Nginx principal del host escucha el puerto `80` y `443` (públicos). Mediante bloques `server_name`, intercepta las peticiones a subdominios específicos (`practicas.qa.kubistudio.cloud`, `practicas.staging.kubistudio.cloud`, `practicas.prod.kubistudio.cloud`) y hace un `proxy_pass` hacia el puerto interno correspondiente. * **Terminación SSL:** La seguridad HTTPS se maneja exclusivamente en el Nginx del host utilizando **Certbot (Let's Encrypt)**. Los contenedores de Docker solo manejan tráfico HTTP interno, evitando colisiones en el puerto `443`. ## ⚙️ Inyección Dinámica de Variables (Frontend Estático) Para cumplir con la premisa de *Build Once, Deploy Anywhere* en una aplicación estática (HTML/JS/CSS servida por Nginx), se solucionó el problema de las variables de entorno de la siguiente manera: 1. **Variables en el Pipeline:** El pipeline de Gitea Actions pasa variables de entorno (Commit SHA, Branch, App Env, Build Date) mediante la bandera `-e` en el comando `docker run`. 2. **Entrypoint Interceptor:** Se utiliza un script personalizado `docker-entrypoint.sh` en el contenedor. 3. **Generación Runtime:** Antes de que el Nginx del contenedor arranque, el script lee las variables de entorno de Linux y genera dinámicamente un archivo estático `/usr/share/nginx/html/env-config.js` inyectando el objeto `window.__ENV__`. 4. **Consumo en Cliente:** El archivo `index.html` carga este script de configuración antes de ejecutar la lógica de la aplicación, permitiendo que la interfaz cambie de aspecto y muestre la metadata correcta según el entorno (QA, Staging o Prod). ## 🚀 Flujo de las Pipelines de CD y Ramas El despliegue está dividido en tres flujos principales basados en ramas: ``` feature/* | PR | ┌───┴───┐ │ dev │ ──(push)──► CI Pipeline ──► Deploy QA └───┬───┘ (lint, build, (puerto 8081) │ security scan) PR docker registry │ │ ┌───┴───┐ │ │staging│ ──(push)────────┼──► Deploy Staging └───┬───┘ │ (puerto 8082) │ │ PR │ │ │ ┌───┴───┐ │ │ main │ ──(push)────────┴──► Deploy Production └───────┘ (aprobación) (puerto 8083) ``` ### Detalle por Entorno * **QA (Entorno de Pruebas):** * **Triggers:** Push a la rama `dev`. * **Build & Push:** Construye la imagen Docker inyectando argumentos de compilación (`--build-arg` con el Git SHA y Build Date) y la sube al Gitea Container Registry con las etiquetas `qa-latest` y `sha-`. * **Deploy (SSH):** Se conecta por SSH al VPS, detiene y elimina el contenedor anterior (`cicd-qa`), levanta el nuevo en el puerto `8081` con `APP_ENV=qa` y valida el despliegue con un health check. * **Staging (Entorno de Pre-producción):** * **Triggers:** Push a la rama `staging`. * **Build & Push:** Genera la imagen etiquetada para staging (`staging-latest`). * **Deploy (SSH):** Detiene el contenedor anterior (`cicd-staging`), levanta el nuevo en el puerto `8082` con `APP_ENV=staging` y realiza smoke tests comprobando la variable de entorno expuesta en el de salud. * **Production (Entorno de Producción):** * **Triggers:** Push a la rama `main`. * **Build & Push:** Genera la imagen inmutable de producción (`production-latest`, `stable` y `sha-`). * **Deploy (SSH):** Guarda la imagen actual para rollback, levanta el nuevo contenedor en el puerto `8083` con `APP_ENV=production`. Si el health check falla, realiza un **rollback automático** restaurando la imagen anterior. ## Ejecución Local ### Con docker-compose ```yaml version: '3.8' services: cicd-qa: build: . ports: - "8081:80" environment: APP_ENV: qa APP_VERSION: dev-local GIT_COMMIT: local GIT_BRANCH: dev BUILD_DATE: ${BUILD_DATE:-unknown} DEPLOY_TIME: ${DEPLOY_TIME:-unknown} BUILD_NUMBER: "0" cicd-staging: build: . ports: - "8082:80" environment: APP_ENV: staging APP_VERSION: staging-local GIT_COMMIT: local GIT_BRANCH: staging BUILD_DATE: ${BUILD_DATE:-unknown} DEPLOY_TIME: ${DEPLOY_TIME:-unknown} BUILD_NUMBER: "0" cicd-prod: build: . ports: - "8083:80" environment: APP_ENV: production APP_VERSION: 1.0.0 DEPLOY_TIME: ${DEPLOY_TIME:-unknown} ``` ### Manual ```bash # Build docker build \ --build-arg APP_VERSION=1.0.0 \ --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ --build-arg GIT_COMMIT=$(git rev-parse --short HEAD) \ --build-arg GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) \ -t cicd-poc:latest . # Run docker run -d \ --name cicd-qa \ -p 8081:80 \ -e APP_ENV=qa \ -e APP_VERSION=dev-local \ -e GIT_COMMIT=$(git rev-parse --short HEAD) \ -e GIT_BRANCH=dev \ -e BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ -e BUILD_NUMBER="1" \ cicd-poc:latest # Test curl http://localhost:8081/health ``` ## Variables de Entorno del Contenedor | Variable | Obligatorio | Default | Descripción | |-----------------|-------------|---------------|---------------------------------------| | `APP_ENV` | No | `development` | Ambiente: qa, staging, production | | `APP_VERSION` | No | `0.0.0` | Versión de la aplicación | | `BUILD_DATE` | No | *(vacío)* | Fecha ISO del build | | `GIT_COMMIT` | No | *(vacío)* | SHA corto del commit | | `GIT_BRANCH` | No | *(vacío)* | Rama de git | | `DEPLOY_TIME` | No | *(vacío)* | Timestamp del deploy | | `BUILD_NUMBER` | No | *(vacío)* | Número de build de la pipeline | ## Rollback Manual ### Production ```bash ssh user@deploy-host # Rollback a stable tag docker stop cicd-prod && docker rm cicd-prod docker run -d \ --name cicd-prod \ --restart unless-stopped \ -p 8083:80 \ -e APP_ENV=production \ -e APP_VERSION=rollback \ -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ registry-url/image-name:stable # O con un SHA específico docker run -d \ --name cicd-prod \ --restart unless-stopped \ -p 8083:80 \ -e APP_ENV=production \ -e APP_VERSION=rollback \ -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ registry-url/image-name:sha-SHA_DEL_COMMIT ``` ### QA / Staging ```bash # QA docker stop cicd-qa && docker rm cicd-qa docker run -d --name cicd-qa -p 8081:80 ... registry-url/image-name:sha-SHA_ANTERIOR # Staging docker stop cicd-staging && docker rm cicd-staging docker run -d --name cicd-staging -p 8082:80 ... registry-url/image-name:sha-SHA_ANTERIOR ``` ## Endpoints | Endpoint | Descripción | |-----------|--------------------------------------| | `/` | Aplicación web | | `/health` | Health check (JSON) | ### Ejemplo `/health` ```json { "status": "ok", "env": "production", "version": "release-a1b2c3d", "timestamp": "2024-01-15T14:23:00+00:00" } ``` ## Estructura del Proyecto ``` . ├── src/ │ └── index.html # App web (UI completa self-contained) ├── .gitea/workflows/ │ ├── ci.yml # CI: lint + build + security scan │ ├── deploy-qa.yml # Deploy a QA (push a dev) │ ├── deploy-staging.yml # Deploy a Staging (push a staging) │ └── deploy-prod.yml # Deploy a Producción (push a main + aprobación) ├── docker-entrypoint.sh # Entrypoint que genera env-config.js en runtime ├── Dockerfile # Multi-stage build ├── healthcheck.sh # Script de health check ├── nginx.conf # Configuración de nginx └── README.md # Este archivo ```