nietzshn d4c3affa2f
CI Pipeline / HTML Lint (push) Successful in 8s
Deploy QA / Build and Push (push) Successful in 16s
CI Pipeline / Build Docker Image (push) Successful in 1m7s
Deploy QA / Deploy to QA (push) Successful in 9s
CI Pipeline / Security Scan (push) Successful in 12s
CI Pipeline / HTML Lint (pull_request) Successful in 8s
CI Pipeline / Build Docker Image (pull_request) Successful in 58s
CI Pipeline / Security Scan (pull_request) Successful in 12s
feat: implement dynamic runtime environment configuration via env-config.js injection and update project documentation
2026-06-02 21:59:20 -06:00
2026-05-29 20:26:01 -06:00

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-<hash>.
    • 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-<hash>).
    • 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

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

# 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

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

# 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

{
  "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
S
Description
CI/CD Multi-Environment Strategy (PoC)
Readme Unlicense 93 KiB
Languages
HTML 84.9%
Dockerfile 8.9%
Shell 6.2%