|
| 1 | +--- |
| 2 | +title: 'Database Migrations' |
| 3 | +description: 'Running migrations for a database deployed with Suga' |
| 4 | +--- |
| 5 | + |
| 6 | +## With public database (Neon, Supabase, Planetscale etc.) |
| 7 | + |
| 8 | +Running database migrations against services like Neon, Supabase or Planetscale is simple with Suga. |
| 9 | + |
| 10 | +### Prerequisites |
| 11 | + |
| 12 | +1. **Ensure your application build is up to date** by running `suga build` to generate the necessary Terraform configuration |
| 13 | + |
| 14 | +2. **Identify your database module name** from your `suga.yaml` file: |
| 15 | + |
| 16 | +```yaml |
| 17 | +databases: |
| 18 | + database: # <-- 👀 This will be the terraform module name |
| 19 | + env_var_key: DATABASE_URL |
| 20 | +``` |
| 21 | +
|
| 22 | +3. **Ensure your migration tool is available** in your CI/CD environment (e.g., golang-migrate, flyway, liquibase, or a custom migration script) |
| 23 | +
|
| 24 | +### Migration Strategy |
| 25 | +
|
| 26 | +The recommended approach follows a four-phase deployment pattern: |
| 27 | +
|
| 28 | +1. **Deploy database infrastructure first** - Create or update database resources |
| 29 | +2. **Extract connection details** - Get the database connection string from Terraform state |
| 30 | +3. **Run migrations** - Apply schema changes before deploying application code |
| 31 | +4. **Deploy application** - Roll out the updated application that depends on the new schema |
| 32 | +
|
| 33 | +### Implementation Steps |
| 34 | +
|
| 35 | +<Steps> |
| 36 | + <Step title="Deploy Database Module"> |
| 37 | + First, apply only the database module using Terraform's `-target` flag: |
| 38 | + |
| 39 | + ```bash |
| 40 | + # Initialize Terraform |
| 41 | + terraform init |
| 42 | +
|
| 43 | + # Plan database module deployment |
| 44 | + terraform plan -target=module.database -out=tfplan-database |
| 45 | +
|
| 46 | + # Apply database module |
| 47 | + terraform apply tfplan-database |
| 48 | + ``` |
| 49 | + </Step> |
| 50 | + |
| 51 | + <Step title="Extract Database Connection String"> |
| 52 | + After the database module is deployed, extract the connection details from Terraform state: |
| 53 | + |
| 54 | + ```bash |
| 55 | + # Extract database connection components |
| 56 | + terraform output -json database_connection_string |
| 57 | +
|
| 58 | + # Or extract from state directly |
| 59 | + terraform state show module.database |
| 60 | + ``` |
| 61 | + |
| 62 | + For Neon databases specifically, you'll typically need: |
| 63 | + - Role name and password |
| 64 | + - Endpoint host |
| 65 | + - Database name |
| 66 | + - SSL mode (usually `require`) |
| 67 | + </Step> |
| 68 | + |
| 69 | + <Step title="Run Migrations"> |
| 70 | + With the connection string available, run your migrations: |
| 71 | + |
| 72 | + ```bash |
| 73 | + # Example using golang-migrate |
| 74 | + migrate -database "$DATABASE_URL" -path ./migrations up |
| 75 | +
|
| 76 | + # Example using a custom migration tool |
| 77 | + ./run-migrations.sh --database-url "$DATABASE_URL" |
| 78 | +
|
| 79 | + # Example using Node.js migration tool |
| 80 | + npm run migrate:up |
| 81 | + ``` |
| 82 | + </Step> |
| 83 | + |
| 84 | + <Step title="Deploy Application"> |
| 85 | + After migrations succeed, deploy the complete application stack: |
| 86 | + |
| 87 | + ```bash |
| 88 | + # Plan full deployment |
| 89 | + terraform plan -out=tfplan-app |
| 90 | +
|
| 91 | + # Apply all modules |
| 92 | + terraform apply tfplan-app |
| 93 | + ``` |
| 94 | + </Step> |
| 95 | +</Steps> |
| 96 | + |
| 97 | +### Best Practices |
| 98 | + |
| 99 | +1. **Use environment-specific workspaces** to isolate different stages (dev, staging, production) |
| 100 | +2. **Implement proper concurrency control** to prevent simultaneous migrations |
| 101 | +3. **Always backup data** before running migrations in production |
| 102 | +4. **Test migrations** in lower environments first |
| 103 | +5. **Use transactional migrations** when possible to enable rollback on failure |
| 104 | +6. **Version control your migration files** alongside your application code |
| 105 | + |
| 106 | +### Full Example: Platform-Agnostic Script |
| 107 | + |
| 108 | +Here's a general approach that can be adapted to any CI/CD platform: |
| 109 | + |
| 110 | +```bash |
| 111 | +#!/bin/bash |
| 112 | +set -e |
| 113 | +
|
| 114 | +# Configuration |
| 115 | +WORKSPACE="${ENVIRONMENT:-dev}" |
| 116 | +TERRAFORM_DIR="./terraform/stacks/suga_platform" |
| 117 | +
|
| 118 | +# Step 1: Initialize and select workspace |
| 119 | +cd "$TERRAFORM_DIR" |
| 120 | +terraform init |
| 121 | +terraform workspace select "$WORKSPACE" || terraform workspace new "$WORKSPACE" |
| 122 | +
|
| 123 | +# Step 2: Deploy database module |
| 124 | +terraform plan -target=module.database -out=tfplan-database |
| 125 | +terraform apply tfplan-database |
| 126 | +
|
| 127 | +# Step 3: Extract connection string |
| 128 | +DATABASE_URL=$(terraform output -raw database_connection_string) |
| 129 | +export DATABASE_URL |
| 130 | +
|
| 131 | +# Step 4: Run migrations |
| 132 | +cd ../../../backend # Adjust path as needed |
| 133 | +./migrate up # Replace with your migration command |
| 134 | +
|
| 135 | +# Step 5: Deploy application |
| 136 | +cd "$TERRAFORM_DIR" |
| 137 | +terraform plan -out=tfplan-app |
| 138 | +terraform apply tfplan-app |
| 139 | +``` |
| 140 | + |
| 141 | +### Example: GitHub Actions Implementation |
| 142 | + |
| 143 | +Here's a complete GitHub Actions workflow that implements the migration pattern: |
| 144 | +> This is a copy of the workflow used to deploy the Suga platform itself 😃 |
| 145 | + |
| 146 | +```yaml |
| 147 | +name: Deploy with Database Migrations |
| 148 | +
|
| 149 | +on: |
| 150 | + push: |
| 151 | + branches: [main] |
| 152 | + tags: ['v[0-9]+.[0-9]+.[0-9]+'] |
| 153 | +
|
| 154 | +jobs: |
| 155 | + terraform-apply: |
| 156 | + name: Deploy Terraform Stack |
| 157 | + runs-on: ubuntu-latest |
| 158 | + steps: |
| 159 | + - name: Checkout code |
| 160 | + uses: actions/checkout@v4 |
| 161 | +
|
| 162 | + - name: Configure AWS credentials |
| 163 | + uses: aws-actions/configure-aws-credentials@v4 |
| 164 | + with: |
| 165 | + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} |
| 166 | + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} |
| 167 | + aws-region: us-east-2 |
| 168 | +
|
| 169 | + - name: Setup Terraform |
| 170 | + uses: hashicorp/setup-terraform@v3 |
| 171 | +
|
| 172 | + - name: Set Terraform Workspace |
| 173 | + id: workspace |
| 174 | + run: | |
| 175 | + if [[ "${{ github.ref }}" =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+ ]]; then |
| 176 | + WORKSPACE="production" |
| 177 | + elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then |
| 178 | + WORKSPACE="staging" |
| 179 | + else |
| 180 | + WORKSPACE="dev" |
| 181 | + fi |
| 182 | + echo "workspace=$WORKSPACE" >> $GITHUB_OUTPUT |
| 183 | +
|
| 184 | + - name: Terraform Init |
| 185 | + working-directory: ./terraform/stacks/suga_platform |
| 186 | + run: terraform init |
| 187 | +
|
| 188 | + - name: Create or Select Terraform Workspace |
| 189 | + working-directory: ./terraform/stacks/suga_platform |
| 190 | + run: | |
| 191 | + terraform workspace select ${{ steps.workspace.outputs.workspace }} || \ |
| 192 | + terraform workspace new ${{ steps.workspace.outputs.workspace }} |
| 193 | +
|
| 194 | + # Step 1: Deploy Database Module First |
| 195 | + - name: Deploy Database Module |
| 196 | + working-directory: ./terraform/stacks/suga_platform |
| 197 | + run: | |
| 198 | + terraform plan -target=module.database -out=tfplan-database |
| 199 | + terraform apply -auto-approve tfplan-database |
| 200 | +
|
| 201 | + # Step 2: Extract Database Connection String |
| 202 | + - name: Extract Database Connection String |
| 203 | + id: db-connection |
| 204 | + working-directory: ./terraform/stacks/suga_platform |
| 205 | + run: | |
| 206 | + # Extract from Terraform state (example for Neon) |
| 207 | + STATE_JSON=$(terraform state pull) |
| 208 | + |
| 209 | + ROLE_NAME=$(echo "$STATE_JSON" | jq -r '.resources[] | |
| 210 | + select(.module == "module.database" and .type == "neon_role") | |
| 211 | + .instances[0].attributes.name') |
| 212 | + ROLE_PASSWORD=$(echo "$STATE_JSON" | jq -r '.resources[] | |
| 213 | + select(.module == "module.database" and .type == "neon_role") | |
| 214 | + .instances[0].attributes.password') |
| 215 | + ENDPOINT_HOST=$(echo "$STATE_JSON" | jq -r '.resources[] | |
| 216 | + select(.module == "module.database" and .type == "neon_endpoint") | |
| 217 | + .instances[0].attributes.host') |
| 218 | + DATABASE_NAME=$(echo "$STATE_JSON" | jq -r '.resources[] | |
| 219 | + select(.module == "module.database" and .type == "neon_database") | |
| 220 | + .instances[0].attributes.name') |
| 221 | + |
| 222 | + DATABASE_URL="postgresql://${ROLE_NAME}:${ROLE_PASSWORD}@${ENDPOINT_HOST}/${DATABASE_NAME}?sslmode=require" |
| 223 | + echo "DATABASE_URL=$DATABASE_URL" >> $GITHUB_OUTPUT |
| 224 | +
|
| 225 | + # Step 3: Run Database Migrations |
| 226 | + - name: Setup Go # Or your migration tool runtime |
| 227 | + uses: actions/setup-go@v5 |
| 228 | + with: |
| 229 | + go-version: "1.23" |
| 230 | +
|
| 231 | + - name: Run Database Migrations |
| 232 | + working-directory: ./backend |
| 233 | + env: |
| 234 | + DATABASE_URL: ${{ steps.db-connection.outputs.DATABASE_URL }} |
| 235 | + run: | |
| 236 | + go mod download |
| 237 | + go run cmd/migrate/main.go -action=up |
| 238 | +
|
| 239 | + # Step 4: Deploy Complete Application |
| 240 | + - name: Deploy Application |
| 241 | + working-directory: ./terraform/stacks/suga_platform |
| 242 | + run: | |
| 243 | + terraform plan -out=tfplan-app |
| 244 | + terraform apply -auto-approve tfplan-app |
| 245 | +``` |
| 246 | + |
| 247 | +### Security Considerations |
| 248 | + |
| 249 | +- **Never log database credentials** - Use masked outputs in CI/CD logs |
| 250 | +- **Use secure secret management** - Store credentials in CI/CD secret stores |
| 251 | +- **Rotate credentials regularly** - Implement credential rotation policies |
| 252 | +- **Limit network access** - Use private endpoints or IP allowlisting where possible |
| 253 | + |
| 254 | +## With private databases (Cloud SQL, RDS, etc.) |
| 255 | + |
| 256 | +Coming soon... |
0 commit comments