Automatización del esquema de base de datos: migraciones de PostgreSQL en el pipeline CI/CD con Flyway y GitHub Actions
Introducción: por qué las migraciones manuales son peligrosas en 2026
Hace unos años, "ejecutar el script manualmente antes del despliegue" era la norma para muchos equipos. Hoy, en 2026, es una fuente de incidentes. Los equipos trabajan más rápido, los despliegues ocurren varias veces al día y la infraestructura se reproduce de forma automática. Aplicar scripts SQL a la base de datos de manera manual en este entorno implica:
- Riesgo de error humano: script equivocado, base de datos incorrecta o aplicado en el orden equivocado.
- Falta de auditoría: es imposible saber quién cambió el esquema y cuándo.
- Desincronización entre entornos: staging y producción evolucionan de forma diferente.
- Imposibilidad de rollback automático cuando falla un despliegue.
- Problemas al escalar horizontalmente y cuando varios equipos trabajan sobre la misma base de datos.
La solución es integrar la gestión del esquema de PostgreSQL directamente en el pipeline CI/CD. La herramienta Flyway junto con GitHub Actions permite que las migraciones sean reproducibles, versionadas y seguras. En este artículo recorremos el ciclo completo: desde la instalación hasta una configuración lista para producción.
Flyway vs Liquibase vs migraciones integradas de frameworks
Antes de profundizar en Flyway, vale la pena entender por qué elegirlo sobre las alternativas.
Flyway
- Modelo sencillo: archivos SQL con nombres según una convención y la tabla de historial de migraciones
flyway_schema_history. - Soporte para PostgreSQL, MySQL, Oracle y otros motores de bases de datos de forma nativa.
- Configuración mínima para empezar y amplias capacidades para entornos enterprise.
- Se integra con Maven, Gradle, Docker, CLI y GitHub Actions.
- Flyway Teams/Enterprise (versión de pago) añade dry-run, migraciones undo y más opciones de rollback.
Liquibase
- Formato más flexible: XML, YAML, JSON o SQL para describir los cambios.
- Soporta generación de diff entre esquemas.
- Mayor curva de aprendizaje y más configuración requerida.
- Ideal para equipos que necesitan una abstracción independiente del motor de base de datos.
Migraciones integradas de frameworks
- Django migrations, Laravel migrations, Alembic (SQLAlchemy): cómodos dentro de su propio ecosistema.
- Están atados al lenguaje y al framework, lo que dificulta su uso en sistemas polyglot.
- No tienen integración directa con CI/CD sin wrappers adicionales.
Para equipos que trabajan con PostgreSQL en producción y tienen CI/CD en GitHub Actions, Flyway es la elección óptima: mínimas dependencias, comportamiento predecible y soporte nativo de Docker.
Instalación y configuración básica de Flyway para PostgreSQL
Flyway puede ejecutarse de varias formas. La más versátil para CI/CD es la imagen Docker oficial flyway/flyway.
Estructura del proyecto
Estructura de directorios recomendada en el repositorio:
project-root/
├── db/
│ ├── migrations/
│ │ ├── V1__init_schema.sql
│ │ ├── V2__add_users_table.sql
│ │ └── V3__add_index_on_email.sql
│ └── flyway.conf
├── docker-compose.yml
└── .github/
└── workflows/
└── deploy.yml
Archivo de configuración flyway.conf
Configuración básica para conectarse a PostgreSQL:
flyway.url=jdbc:postgresql://localhost:5432/mydb
flyway.user=${DB_USER}
flyway.password=${DB_PASSWORD}
flyway.schemas=public
flyway.locations=filesystem:./db/migrations
flyway.baselineOnMigrate=false
flyway.validateOnMigrate=true
flyway.outOfOrder=false
Los valores ${DB_USER} y ${DB_PASSWORD} son sustituidos por Flyway a partir de variables de entorno, lo cual es fundamental para la seguridad en CI.
Escritura de migraciones SQL versionadas: convención de nombres y buenas prácticas
Convención de nombres de archivos
Flyway sigue estrictamente un patrón de nombres de archivos:
V{version}__{description}.sql— migraciones versionadas (se aplican una sola vez).R__{description}.sql— migraciones repetibles (views, stored procedures, triggers).U{version}__{description}.sql— migraciones undo (solo en la versión de pago).
Ejemplos de nombres correctos:
V1__init_schema.sql
V2__add_users_table.sql
V2_1__add_users_email_index.sql
V3__add_orders_table.sql
R__refresh_analytics_view.sql
Ejemplos de migraciones reales
Inicialización del esquema (V1__init_schema.sql):
-- V1__init_schema.sql
CREATE TABLE IF NOT EXISTS schema_meta (
id SERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Creación de la tabla de usuarios (V2__add_users_table.sql):
-- V2__add_users_table.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL,
username VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_users_email ON users(email);
Adición de clave foránea (V3__add_orders_table.sql):
-- V3__add_orders_table.sql
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
total NUMERIC(12, 2) NOT NULL DEFAULT 0,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
Buenas prácticas para escribir migraciones
- Cada migración debe ser atómica: un solo cambio lógico por archivo.
- Usa
IF NOT EXISTS/IF EXISTSpara idempotencia donde sea aplicable. - Nunca modifiques archivos de migración ya aplicados: Flyway verifica los checksums.
- Evita
DROP TABLEsin una copia de seguridad o una etapa de despliegue separada. - Para modificar la estructura de tablas grandes, usa
ALTER TABLE ... ADD COLUMNcon valores por defecto en lugar de recrear la tabla. - Documenta el propósito de cada migración en un comentario al inicio del archivo.
Integración de Flyway en GitHub Actions: ejecutar migraciones antes del despliegue
Principio clave: las migraciones deben ejecutarse antes del despliegue del nuevo código de la aplicación. Esto garantiza que el nuevo código siempre encuentre el esquema actualizado.
Workflow completo de GitHub Actions
# .github/workflows/deploy.yml
name: Deploy with DB Migrations
on:
push:
branches:
- main
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
migrate:
name: Run PostgreSQL Migrations
runs-on: ubuntu-22.04
environment: production
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run Flyway migrations
uses: docker://flyway/flyway:10-alpine
with:
args: migrate
env:
FLYWAY_URL: jdbc:postgresql://${{ secrets.DB_HOST }}:5432/${{ secrets.DB_NAME }}
FLYWAY_USER: ${{ secrets.DB_USER }}
FLYWAY_PASSWORD: ${{ secrets.DB_PASSWORD }}
FLYWAY_LOCATIONS: filesystem:/github/workspace/db/migrations
FLYWAY_VALIDATE_ON_MIGRATE: "true"
FLYWAY_OUT_OF_ORDER: "false"
FLYWAY_BASELINE_ON_MIGRATE: "false"
- name: Verify migration status
uses: docker://flyway/flyway:10-alpine
with:
args: info
env:
FLYWAY_URL: jdbc:postgresql://${{ secrets.DB_HOST }}:5432/${{ secrets.DB_NAME }}
FLYWAY_USER: ${{ secrets.DB_USER }}
FLYWAY_PASSWORD: ${{ secrets.DB_PASSWORD }}
FLYWAY_LOCATIONS: filesystem:/github/workspace/db/migrations
deploy:
name: Deploy Application
runs-on: ubuntu-22.04
needs: migrate
environment: production
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Deploy to production
run: |
echo "Deploying application after successful migrations..."
# Tu script de despliegue: kubectl apply, docker stack deploy, etc.
Ejecución de pruebas de migración en PRs
En el pipeline de pull requests es útil ejecutar las migraciones contra una base de datos de prueba temporal:
# .github/workflows/pr-migration-test.yml
name: Test Migrations on PR
on:
pull_request:
paths:
- 'db/migrations/**'
jobs:
test-migrations:
runs-on: ubuntu-22.04
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpassword
ports:
- 5432:5432
options: >
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run Flyway migrate on test DB
uses: docker://flyway/flyway:10-alpine
with:
args: migrate
env:
FLYWAY_URL: jdbc:postgresql://localhost:5432/testdb
FLYWAY_USER: testuser
FLYWAY_PASSWORD: testpassword
FLYWAY_LOCATIONS: filesystem:/github/workspace/db/migrations
- name: Validate migration info
uses: docker://flyway/flyway:10-alpine
with:
args: validate
env:
FLYWAY_URL: jdbc:postgresql://localhost:5432/testdb
FLYWAY_USER: testuser
FLYWAY_PASSWORD: testpassword
FLYWAY_LOCATIONS: filesystem:/github/workspace/db/migrations
Estrategias de rollback y baseline para bases de datos existentes
El problema del rollback en Flyway
Flyway Community Edition no soporta migraciones undo automáticas. Es una decisión de diseño deliberada: revertir el esquema de una base de datos es una operación peligrosa, especialmente si ya se han escrito datos bajo el nuevo esquema.
Estrategia «forward-only»
El enfoque recomendado para la mayoría de los sistemas en producción: nunca revertir el esquema hacia atrás, sino corregir los errores con una nueva migración hacia adelante.
- Si
V5añadió una columna con un error, escribimosV6que lo corrige. - Si
V5eliminó una columna necesaria,V6la recrea.
Blue-green deployment como estrategia de seguridad
En un despliegue blue-green, la migración se ejecuta antes de cambiar el tráfico. Si la migración falla, el cambio no se produce y la versión anterior sigue funcionando con el esquema intacto.
Baseline para una base de datos existente
Si conectas Flyway a una base de datos de producción ya existente, utiliza el comando baseline:
# Establecer baseline en la versión 1 para una BD existente
flyway -url=jdbc:postgresql://localhost:5432/mydb \
-user=myuser \
-password=mypassword \
-baselineVersion=1 \
-baselineDescription="Initial baseline" \
baseline
Después de esto, Flyway marcará el estado actual como versión 1 y comenzará a aplicar solo las nuevas migraciones a partir de V2. En la configuración establece flyway.baselineOnMigrate=false: el baseline solo debe ejecutarse una vez de forma manual.
Trabajo con migraciones en entorno Docker
Docker Compose para desarrollo local
El siguiente docker-compose.yml levanta PostgreSQL y ejecuta Flyway automáticamente al iniciar:
version: '3.9'
services:
postgres:
image: postgres:16-alpine
container_name: app_postgres
restart: unless-stopped
environment:
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD: appsecret
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
interval: 10s
timeout: 5s
retries: 5
flyway:
image: flyway/flyway:10-alpine
container_name: app_flyway
command: migrate
depends_on:
postgres:
condition: service_healthy
environment:
FLYWAY_URL: jdbc:postgresql://postgres:5432/appdb
FLYWAY_USER: appuser
FLYWAY_PASSWORD: appsecret
FLYWAY_LOCATIONS: filesystem:/flyway/sql
FLYWAY_VALIDATE_ON_MIGRATE: "true"
volumes:
- ./db/migrations:/flyway/sql
restart: on-failure
volumes:
postgres_data:
Ejecución: docker compose up flyway — aplica todas las migraciones pendientes a la base de datos local.
Flyway en un Dockerfile para producción
Alternativa: incluir Flyway en un init container de Kubernetes o en un Job independiente:
FROM flyway/flyway:10-alpine
COPY db/migrations /flyway/sql
# CMD se define al lanzar el contenedor mediante args
Seguridad: gestión de credenciales y secretos en el pipeline
La seguridad de las credenciales de la base de datos en CI/CD es un aspecto crítico. Una filtración de la contraseña de PostgreSQL en producción puede tener consecuencias catastróficas.
GitHub Actions Secrets
Nunca almacenes credenciales en el repositorio ni en variables de entorno sin cifrado. Usa GitHub Secrets:
- Ve a Settings → Secrets and variables → Actions en tu repositorio.
- Crea los secretos:
DB_HOST,DB_NAME,DB_USER,DB_PASSWORD. - Usa
environmenten el workflow para separar los secretos por entorno (staging, producción).
Uso de HashiCorp Vault o AWS Secrets Manager
Para entornos enterprise se recomienda obtener las credenciales de forma dinámica desde Vault:
# Paso para obtener credenciales desde Vault
- name: Import secrets from Vault
uses: hashicorp/vault-action@v3
with:
url: ${{ secrets.VAULT_ADDR }}
token: ${{ secrets.VAULT_TOKEN }}
secrets: |
secret/data/production/postgres username | DB_USER ;
secret/data/production/postgres password | DB_PASSWORD
- name: Run Flyway migrations
uses: docker://flyway/flyway:10-alpine
with:
args: migrate
env:
FLYWAY_URL: jdbc:postgresql://${{ secrets.DB_HOST }}:5432/${{ secrets.DB_NAME }}
FLYWAY_USER: ${{ env.DB_USER }}
FLYWAY_PASSWORD: ${{ env.DB_PASSWORD }}
FLYWAY_LOCATIONS: filesystem:/github/workspace/db/migrations
Principio de mínimos privilegios
Crea un usuario de PostgreSQL dedicado para Flyway con los permisos mínimos necesarios:
-- Creación del usuario para migraciones
CREATE USER flyway_runner WITH PASSWORD 'strong_random_password';
-- Permisos solo sobre la base de datos y el esquema necesarios
GRANT CONNECT ON DATABASE appdb TO flyway_runner;
GRANT USAGE, CREATE ON SCHEMA public TO flyway_runner;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO flyway_runner;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO flyway_runner;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL ON TABLES TO flyway_runner;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL ON SEQUENCES TO flyway_runner;
Rotación de contraseñas y auditoría
- Rota regularmente las contraseñas de las cuentas de servicio (como mínimo, cada trimestre).
- Activa el registro de conexiones a PostgreSQL mediante
log_connections = on. - Monitoriza la tabla
flyway_schema_historypara detectar cambios inesperados. - Usa conexiones SSL: añade
?sslmode=requirea la URL JDBC.
Conclusión y checklist
Automatizar las migraciones de PostgreSQL con Flyway en un pipeline CI/CD sobre GitHub Actions no es una complicación, sino la base de una gestión confiable del esquema de base de datos. Un pipeline correctamente configurado garantiza que el esquema siempre esté sincronizado con el código, que los cambios estén versionados y auditables, y que el factor humano quede eliminado de un proceso crítico.
Tu base de datos no es un archivo de configuración. Requiere el mismo control de versiones y automatización que el código fuente de tu aplicación.
Checklist de implementación
- Directorio
db/migrations/creado con archivos SQL versionados. flyway.confconfigurado sin credenciales hardcodeadas.- Workflow de GitHub Actions creado: las migraciones se ejecutan en el job
migrateantes del jobdeploy. - El pipeline de PRs prueba las migraciones contra un servicio PostgreSQL efímero.
- Todas las credenciales almacenadas en GitHub Secrets con separación por entorno.
- Usuario de PostgreSQL dedicado para Flyway creado con permisos mínimos.
- La URL JDBC usa
sslmode=requireen producción. - Docker Compose configurado para reproducir el pipeline de migraciones en local.
- Estrategia forward-only adoptada para los cambios de esquema.
- El equipo conoce la convención de nombres para los archivos de migración.
Este enfoque escala desde una pequeña startup hasta sistemas de producción de alta carga, garantizando previsibilidad y seguridad en cada etapa del ciclo de vida de la aplicación.
Tecnologías
Etiquetas
Ruslan Ismailov
Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →