DevOps

Automatización del esquema de base de datos: migraciones de PostgreSQL en el pipeline CI/CD con Flyway y GitHub Actions

Ruslan Ismailov Publicado 14 min de lectura
A

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 EXISTS para idempotencia donde sea aplicable.
  • Nunca modifiques archivos de migración ya aplicados: Flyway verifica los checksums.
  • Evita DROP TABLE sin una copia de seguridad o una etapa de despliegue separada.
  • Para modificar la estructura de tablas grandes, usa ALTER TABLE ... ADD COLUMN con 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 V5 añadió una columna con un error, escribimos V6 que lo corrige.
  • Si V5 eliminó una columna necesaria, V6 la 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 environment en 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_history para detectar cambios inesperados.
  • Usa conexiones SSL: añade ?sslmode=require a 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

  1. Directorio db/migrations/ creado con archivos SQL versionados.
  2. flyway.conf configurado sin credenciales hardcodeadas.
  3. Workflow de GitHub Actions creado: las migraciones se ejecutan en el job migrate antes del job deploy.
  4. El pipeline de PRs prueba las migraciones contra un servicio PostgreSQL efímero.
  5. Todas las credenciales almacenadas en GitHub Secrets con separación por entorno.
  6. Usuario de PostgreSQL dedicado para Flyway creado con permisos mínimos.
  7. La URL JDBC usa sslmode=require en producción.
  8. Docker Compose configurado para reproducir el pipeline de migraciones en local.
  9. Estrategia forward-only adoptada para los cambios de esquema.
  10. 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í →