9.4 KiB
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
localhosten el VPS (ej. QA en8081, Staging en8082, Prod en8083). - Reverse Proxy: El servidor Nginx principal del host escucha el puerto
80y443(públicos). Mediante bloquesserver_name, intercepta las peticiones a subdominios específicos (practicas.qa.kubistudio.cloud,practicas.staging.kubistudio.cloud,practicas.prod.kubistudio.cloud) y hace unproxy_passhacia 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:
- Variables en el Pipeline: El pipeline de Gitea Actions pasa variables de entorno (Commit SHA, Branch, App Env, Build Date) mediante la bandera
-een el comandodocker run. - Entrypoint Interceptor: Se utiliza un script personalizado
docker-entrypoint.shen el contenedor. - 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.jsinyectando el objetowindow.__ENV__. - Consumo en Cliente: El archivo
index.htmlcarga 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-argcon el Git SHA y Build Date) y la sube al Gitea Container Registry con las etiquetasqa-latestysha-<hash>. - Deploy (SSH): Se conecta por SSH al VPS, detiene y elimina el contenedor anterior (
cicd-qa), levanta el nuevo en el puerto8081conAPP_ENV=qay valida el despliegue con un health check.
- Triggers: Push a la rama
-
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 puerto8082conAPP_ENV=stagingy realiza smoke tests comprobando la variable de entorno expuesta en el de salud.
- Triggers: Push a la rama
-
Production (Entorno de Producción):
- Triggers: Push a la rama
main. - Build & Push: Genera la imagen inmutable de producción (
production-latest,stableysha-<hash>). - Deploy (SSH): Guarda la imagen actual para rollback, levanta el nuevo contenedor en el puerto
8083conAPP_ENV=production. Si el health check falla, realiza un rollback automático restaurando la imagen anterior.
- Triggers: Push a la rama
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