How to create a database migration
Aucert uses Flyway for database migrations. Migrations run as temporary pods inside AKS because the PostgreSQL instances are on a private VNet (no public access).
Database instances
| Instance | Databases | Migration path |
|---|---|---|
aucert-internal-pg | internal_shared_db, astra_db, plane_db | infra/migrations/internal-shared/, infra/migrations/astra/ |
aucertdev-product-pg | aucert (dev) | infra/migrations/product/ (when created) |
Steps
Step 1: Create the migration file
Migration files follow the Flyway naming convention: V{NNN}__{description}.sql
# For internal_shared_db
ls infra/migrations/internal-shared/
# V001__create_platforms.sql
# V002__create_audit_log.sql
# V003__create_user_preferences.sql
# V004__seed_platforms.sql
# Create the next migration
touch infra/migrations/internal-shared/V005__add_user_roles.sql
caution
- Version numbers must be sequential and unique
- Use double underscores (
__) between version and description - Use snake_case for descriptions
- Never modify an already-applied migration — create a new one instead
Step 2: Write the SQL
-- infra/migrations/internal-shared/V005__add_user_roles.sql
CREATE TABLE IF NOT EXISTS user_roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES user_preferences(id),
role TEXT NOT NULL CHECK (role IN ('admin', 'member', 'viewer')),
granted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
granted_by TEXT NOT NULL
);
CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);
Best practices:
- Always use
IF NOT EXISTSforCREATE TABLE/CREATE INDEX - Include rollback comments at the top if the migration is complex
- Keep migrations small and focused — one logical change per file
- Use explicit types (e.g.,
TIMESTAMPTZnotTIMESTAMP)
Step 3: Test locally (if accessible)
If you have a local PostgreSQL or can port-forward:
# Port-forward the internal PG
kubectl port-forward -n internal-platform svc/internal-pg 5433:5432
# Run Flyway locally
docker run --rm \
-v $(pwd)/infra/migrations/internal-shared:/flyway/sql \
flyway/flyway:11 \
-url="jdbc:postgresql://host.docker.internal:5433/internal_shared_db" \
-user="aucert_admin" \
-password="<from-key-vault>" \
info
Step 4: Deploy via CI
Migrations run automatically in the deploy-astra.yml workflow when files in infra/migrations/** change. The workflow:
- Creates a ConfigMap from the SQL files
- Runs a temporary Flyway pod inside AKS (same VNet as PG)
- Credentials injected via
secretKeyReffromastra-db-credentialsK8s secret - Cleans up the ConfigMap after completion
# Manual trigger if needed
gh workflow run "Deploy Astra" --field skip_migrations=false
Step 5: Verify the migration
# Check Flyway history table
kubectl exec -n internal-platform -it <pg-pod> -- \
psql -U aucert_admin -d internal_shared_db \
-c "SELECT * FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 5;"
Rollback strategy
danger
Flyway Community Edition does not support automatic rollback. Plan your rollback strategy before applying migrations in production.
For additive changes (new tables, columns, indexes): Create a new forward migration that reverses the change.
For destructive changes (drops, renames): Don't. Instead:
- Add the new structure in migration N
- Migrate data in migration N+1
- Drop the old structure in migration N+2 (after verifying)
What's next
- How to deploy to dev — Full deployment process
- How to debug AKS pods — Troubleshoot migration pods
- Secrets management — Database credential flow