Subchapter 28.21
references/phases/generate/generate-docs.mdMarkdown60 KBView on GitHub
Self-contained sub-file for generating migration documentation and database migration scripts. Produces
MIGRATION_GUIDE.md, , and database migration scripts in . Only generates procedures for data stores actually present in the design — omits absent types entirely.
README.md$MIGRATION_DIRExecute ALL steps in order. Do not skip or optimize.
Scan aws-design.json.services[] to determine which compute and data store types exist in the design:
| Check | Condition | Flag |
|---|---|---|
| Beanstalk present | Any service with aws_service == "Elastic Beanstalk" | has_beanstalk = true |
| Beanstalk web present | Any service with aws_service == "Elastic Beanstalk" and aws_config.process_type == "web" | has_beanstalk_web = true |
| Fargate present | Any service with aws_service == "Fargate" or aws_service == "ALB" | has_fargate = true |
| Postgres present | Any service with aws_service containing "RDS PostgreSQL" or "Aurora PostgreSQL" | has_postgres = true |
| Redis present | Any service with aws_service == "ElastiCache Redis" | has_redis = true |
| Kafka present | Any service with aws_service == "Amazon MSK" | has_kafka = true |
Also extract:
deferred_addons[] — entries from aws-design.json.deferred[]all_services[] — full list of designed services for README generationtarget_region — from preferences.json.global.target_region (default: us-east-1)heroku_apps[] — list of unique app names from design servicesbeanstalk_web_apps[] — unique app names from Elastic Beanstalk web services, plus each name sanitized by replacing - with _migration_approach — from preferences.json.global.migration_approach ("full_cutover" or "interim_cutover_data_first")migration_method — from preferences.json.data.migration_method ("pg_dump_restore", "dms", "bucardo", "wal_g")containerization_status — from preferences.json.operational.containerization_status ("containerized", "buildpack_only", "partial")target_exit_date — from preferences.json.global.target_exit_date (ISO date or null)Write the migration guide to $MIGRATION_DIR/MIGRATION_GUIDE.md using the template below.
Critical rules:
true.# Migration Guide: Heroku to AWS
This guide provides step-by-step instructions for migrating your Heroku application(s) to AWS.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Phase 1: Infrastructure Provisioning](#phase-1-infrastructure-provisioning)
- [Phase 2: Data Migration](#phase-2-data-migration)
- [Phase 3: Application Deployment](#phase-3-application-deployment)
- [Phase 4: Verification
Verify all resources are created successfully:
terraform outputRecord the output values — they are needed for data migration and application deployment.
{{IF has_postgres}}
Strategy: Use pg_dump / pg_restore for a full database migration with minimal downtime.
Enable maintenance mode on Heroku to prevent writes during migration:
heroku maintenance:on -a {{app_name}}Verify source database size and estimate transfer time:
heroku pg:info -a {{app_name}}Run the database migration script:
./scripts/migrate-postgres.shOr execute manually:
# Export from Heroku Postgres
PGPASSWORD="{{SOURCE_DB_PASSWORD}}" pg_dump \
-h {{SOURCE_DB_HOST}} \
-p {{SOURCE_DB_PORT}} \
-U {{SOURCE_DB_USER}} \
-d {{SOURCE_DB_NAME}} \
-Fc \
--no-owner \
--no-acl \
--verbose \
> heroku_backup.dump
# Import to AWS RDS/Aurora
PGPASSWORD="{{TARGET_DB_PASSWORD}}" pg_restore \
-h {{TARGET_DB_HOST}} \
-p {{TARGET_DB_PORT}} \
-U {{TARGET_DB_USER}} \
-d {{TARGET_DB_NAME}} \
--no-owner \
--no-acl \
--verbose \
heroku_backup.dump# Connect to target and verify row counts
PGPASSWORD="{{TARGET_DB_PASSWORD}}" psql \
-h {{TARGET_DB_HOST}} \
-p {{TARGET_DB_PORT}} \
-U {{TARGET_DB_USER}} \
-d {{TARGET_DB_NAME}} \
-c "SELECT schemaname, relname, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESC;"Compare row counts between source and target to confirm data integrity.
{{IF migration_approach == “interim_cutover_data_first”}}
During the interim period your Heroku app connects to the AWS database. Work through the steps below in the order given — Step 1 is a prerequisite gate that must pass before any network path is opened in Step 2. The database stays private by default; Step 2 is about picking the narrowest path that works for your Heroku runtime.
⚠️ Never open port 5432 to
0.0.0.0/0. TLS protects the traffic, not the listener — a world-reachable database port is still exposed to internet-wide port scanning, credential stuffing, and protocol-level exploitation, and Heroku’s “dynamic dyno IPs” are not a reason to accept it. Every path below ends in a bounded, enumerated allowlist.
Complete every part of Step 1 and pass its verification gate before touching Step 2.
Require TLS on the database. The generated Terraform already sets rds.force_ssl = 1 for this database, so terraform apply puts it in place — there is no console step and no way to skip it. Where the parameter lives, and whether it needs a reboot, depends on the engine:
RDS for PostgreSQL — set in the instance-level aws_db_parameter_group. The parameter is static, so reboot the instance once so it takes effect:
aws rds reboot-db-instance --db-instance-identifier <db_identifier>Aurora PostgreSQL — set in the cluster-level aws_rds_cluster_parameter_group (a cluster parameter, so an instance-level group cannot carry it). The parameter is dynamic here, so no reboot is needed and reboot-db-instance does not apply to a cluster. terraform apply is sufficient.
Defaults differ by engine, which is why the generated Terraform always sets this explicitly rather than relying on them. RDS for PostgreSQL:
rds.force_ssldefaults to1(on) on major version 15 and later; on 14 and earlier the default is0(off). Aurora PostgreSQL: the default is0(off) on version 16 and older, and1(on) only from version 17 — so on the Aurora version this guide pins, TLS is not enforced by default.
Either way, do not treat this as done because the code exists — step 4 below is the gate that proves it.
Ship the RDS CA bundle with your Heroku app so the client can verify the server certificate:
curl -o config/rds-ca-bundle.pem https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
git add config/rds-ca-bundle.pem && git commit -m "Add RDS CA bundle" && git push heroku mainPoint DATABASE_URL at AWS with full certificate verification (verify-full, not require — require encrypts but does not authenticate the server):
heroku config:set DATABASE_URL="postgres://{{TARGET_DB_USER}}:{{TARGET_DB_PASSWORD}}@{{TARGET_DB_HOST}}:{{TARGET_DB_PORT}}/{{TARGET_DB_NAME}}?sslmode=verify-full&sslrootcert=config/rds-ca-bundle.pem" -a {{app_name}}Verification gate. From a host that can already reach the database (for example the workstation or bastion you ran pg_restore from), confirm enforcement is real rather than assumed:
# (a) pg_hba rules must be hostssl, not host
psql "$ADMIN_DATABASE_URL" -c "SELECT type, database, auth_method FROM pg_hba_file_rules;"
# (b) a plaintext connection must be REJECTED
psql "postgres://{{TARGET_DB_USER}}@{{TARGET_DB_HOST}}:{{TARGET_DB_PORT}}/{{TARGET_DB_NAME}}?sslmode=disable"
# Expected: FATAL: no pg_hba.conf entry for host "...", user "...", database "...", SSL offIf (a) shows host instead of hostssl, or (b) succeeds, stop here — rds.force_ssl is not in effect (most often a missed reboot or a parameter group that was never attached). Do not open any network path until both checks pass.
Determine your runtime first: heroku spaces:info --space <space_name> succeeds only if the app runs in a Private Space; otherwise the app is on the Common Runtime. Then take the first path that applies.
Path A — Private Space peered to your AWS VPC (preferred: no public exposure at all).
Available on Cedar-generation Private Spaces. Private Space Peering establishes a private network connection between your dynos and an AWS VPC you control that does not traverse the public internet, so the database stays Not publicly accessible in private subnets.
heroku spaces:peering:info <space_name> # Heroku-side account/VPC/CIDRs
# then request peering FROM your AWS account, and accept it on the Heroku side:
aws ec2 create-vpc-peering-connection --vpc-id <your_vpc_id> \
--peer-vpc-id <heroku_vpc_id> --peer-owner-id <heroku_account_id>
heroku spaces:peerings:accept <pcx_id> --space <space_name>Requirements and consequences:
10.0.0.0/16, 10.1.0.0/16, 172.17.0.0/16); check with heroku spaces:peering:info before requesting.heroku spaces:peering:info (e.g. 10.0.128.0/20, 10.0.144.0/20) — private ranges, a handful of entries.Path B — Private Space stable outbound IPs (no peering available).
Every Private Space, Cedar or Fir, egresses through a NAT gateway from “a small, stable list of IP addresses dedicated to the space.” List them and allowlist those /32s:
heroku spaces:info --space <space_name> # read the "Outbound IPs" lineAllowlist: each outbound IP as a /32 (typically 4 addresses).
Path C — Common Runtime plus a static-egress add-on. Common Runtime dynos have no controllable egress address — Heroku states you cannot control the originating IP address of outbound dyno requests, so there is no set of dyno IPs to allowlist. Do not compensate by widening the allowlist. Instead route the database connection through a SOCKS5 proxy add-on that owns static IPs, then allowlist the proxy:
heroku addons:create quotaguardstatic. Provides a load-balanced pair of static IPs and a SOCKS5 tunnel (QGTunnel) that carries arbitrary TCP, PostgreSQL included, without application code changes.heroku addons:create fixie-socks. A standard SOCKS V5 proxy giving static outbound IPs for database and other TCP traffic; fixie-wrench forwards a local port for runtimes with no native SOCKS5 support.Read the add-on’s own dashboard for its current IP list — the addresses are per-plan and can change when you change plans.
Allowlist: the proxy’s static IPs as /32s (typically 2).
Paths B and C send traffic over the public internet, so they additionally require the database to be publicly accessible and its DB subnet group to sit in subnets with an internet gateway route. The generated Terraform places the database in private subnets, so a Path B/C interim period means moving the DB subnet group as well — one more reason to prefer Path A and to keep the interim period short.
There is no fourth path. If you cannot enumerate a bounded allowlist by any of the routes above, the data-first interim approach is not viable for your setup: switch to full_cutover and migrate the database and application in one window instead.
Make every interim change in the generated Terraform and commit it, not through the RDS or EC2 console:
terraform apply, so the exposure exists only in the running account and never in Git — you cannot audit when it opened or whether it closed.In terraform/terraform.tfvars, set the interim variables the generator emits, then plan and apply:
interim_heroku_ingress_cidrs = ["203.0.113.10/32", "203.0.113.11/32"] # from Step 2
interim_db_public_access = false # true only on Path B or Cterraform plan -out=tfplan # review the single added ingress rule before applying
terraform apply tfplanBoth variables default to closed ([] and false). interim_heroku_ingress_cidrs = [] emits no ingress rule at all, and the variable rejects 0.0.0.0/0.
Once the application runs on AWS and no longer connects from Heroku:
interim_heroku_ingress_cidrs = [], interim_db_public_access = false), then terraform apply. Confirm the plan removes the ingress rule.heroku spaces:peerings:destroy <pcx_id> --space <space_name>.Not publicly accessible.
{{ENDIF}}{{IF migration_method == “dms”}}
For databases over ~10GB, AWS DMS can provide a faster migration with less downtime:
⚠️ Important limitation: AWS DMS cannot perform continuous replication (CDC) with Heroku Postgres. Heroku does not grant the REPLICATION role required for logical replication slots. DMS is for one-time bulk data migration with a final cutover window only.
DMS Setup Steps:
pg_dump --schema-only from Heroku → pg_restore to target{{IF migration_method == “bucardo”}}
For near-zero downtime migration using trigger-based replication:
Requirements:
Setup overview: Bucardo performs an initial full copy, then switches to delta-push mode for continuous replication until cutover. See the detailed Bucardo setup procedure in your migration reference documentation.
Note: Bucardo does not support LOB migration. Stored functions/procedures must be migrated separately via pg_dump --schema-only.
{{ENDIF}}
{{IF migration_method == “wal_g”}}
For large databases requiring minimal downtime via WAL-based replication:
Requirements:
Setup overview: WAL-G captures write-ahead logs from the source database and replays them on the target, allowing continuous catch-up with minimal final cutover window. See the detailed WAL-G setup procedure in your migration reference documentation. {{ENDIF}}
Regardless of migration method, the final cutover follows this sequence:
# 1. Enable maintenance mode (prevents new writes)
heroku maintenance:on -a {{app_name}}
# 2. Final backup (safety net)
heroku pg:backups:capture -a {{app_name}}
# 3. If using pg_dump: run final migration now
# If using DMS/Bucardo/WAL-G: wait for final sync, then stop replication
# 4. Verify data in target database
# 5. Detach Heroku database (or point to new URL)
heroku config:set DATABASE_URL="postgres://{{TARGET_DB_USER}}:{{TARGET_DB_PASSWORD}}@{{TARGET_DB_HOST}}:{{TARGET_DB_PORT}}/{{TARGET_DB_NAME}}?sslmode=verify-full&sslrootcert=config/rds-ca-bundle.pem" -a {{app_name}}
# 6. Disable maintenance mode
heroku maintenance:off -a {{app_name}}
# 7. Verify application is working with new databaseAfter full application migration to AWS (no longer on Heroku):
# Detach and optionally destroy Heroku Postgres
heroku addons:detach DATABASE -a {{app_name}}
# WARNING: Only destroy after confirming all data is accessible in AWS
# heroku addons:destroy heroku-postgresql -a {{app_name}}{{ENDIF}} {{IF has_redis}}
Strategy: Export Redis data using DUMP/RESTORE or redis-cli --rdb depending on dataset size.
Check current Redis memory usage and key count:
heroku redis:info -a {{app_name}}Determine migration approach:
DUMP/RESTORE./scripts/migrate-redis.shOr execute manually using redis-cli:
# Connect to source and dump keys
redis-cli -h {{SOURCE_REDIS_HOST}} -p {{SOURCE_REDIS_PORT}} \
-a "{{SOURCE_REDIS_PASSWORD}}" --tls \
--scan --pattern '*' | while read key; do
redis-cli -h {{SOURCE_REDIS_HOST}} -p {{SOURCE_REDIS_PORT}} \
-a "{{SOURCE_REDIS_PASSWORD}}" --tls \
DUMP "$key" | redis-cli -h {{TARGET_REDIS_HOST}} -p {{TARGET_REDIS_PORT}} \
-a "{{TARGET_REDIS_PASSWORD}}" --tls \
RESTORE "$key" 0 -
done# Generate RDB snapshot from source
redis-cli -h {{SOURCE_REDIS_HOST}} -p {{SOURCE_REDIS_PORT}} \
-a "{{SOURCE_REDIS_PASSWORD}}" --tls \
--rdb heroku_redis.rdb
# Import to ElastiCache (use S3 as intermediary)
aws s3 cp heroku_redis.rdb s3://{{MIGRATION_BUCKET}}/redis/heroku_redis.rdb
# Then use ElastiCache seed-from-S3 or restore from backup# Compare key counts
echo "Source keys:" && redis-cli -h {{SOURCE_REDIS_HOST}} -p {{SOURCE_REDIS_PORT}} \
-a "{{SOURCE_REDIS_PASSWORD}}" --tls DBSIZE
echo "Target keys:" && redis-cli -h {{TARGET_REDIS_HOST}} -p {{TARGET_REDIS_PORT}} \
-a "{{TARGET_REDIS_PASSWORD}}" --tls DBSIZE{{ENDIF}} {{IF has_kafka}}
Strategy: Use MirrorMaker 2 or topic recreation with producer replay for migration.
Document current topic configuration:
heroku kafka:topics -a {{app_name}}Record consumer group offsets for replay:
heroku kafka:consumer-groups -a {{app_name}}Create topics on MSK matching source configuration:
# For each topic, create with matching partitions and replication
aws kafka create-topic \
--cluster-arn {{MSK_CLUSTER_ARN}} \
--topic-name {{TOPIC_NAME}} \
--partitions {{PARTITION_COUNT}} \
--replication-factor {{REPLICATION_FACTOR}}Configure producers to write to MSK endpoint.
Replay historical data if needed using consumer offset reset.
# Configure MirrorMaker 2 to replicate from Heroku Kafka to MSK
# mm2.properties template:
clusters = source, target
source.bootstrap.servers = {{SOURCE_KAFKA_BROKERS}}
target.bootstrap.servers = {{TARGET_MSK_BROKERS}}
source->target.enabled = true
source->target.topics = .*# Verify topic list on MSK
aws kafka list-topics --cluster-arn {{MSK_CLUSTER_ARN}}
# Verify message counts per topic/partition
kafka-consumer-groups.sh --bootstrap-server {{TARGET_MSK_BROKERS}} \
--describe --all-groups{{ENDIF}}
{{IF has_beanstalk}}
The generated EB path deploys a source bundle containing your Dockerfile. GitHub Actions is the default deploy mechanism; CodePipeline is available only when selected during Clarify.
{{IF eb_deploy_method == “github_actions”}}
The generated .github/workflows/deploy-eb.yml workflow uses GitHub OIDC role assumption, packages the source bundle, creates an Elastic Beanstalk application version, and updates each generated EB environment.
Before first run, create or provide a GitHub OIDC IAM role with permissions to call elasticbeanstalk create-storage-location, elasticbeanstalk create-application-version, elasticbeanstalk update-environment, and upload the source bundle to the EB storage bucket. Store the role ARN as the repository secret AWS_ROLE_ARN.
{{ENDIF}} {{IF eb_deploy_method == “codepipeline”}}
The generated terraform/pipeline.tf creates an AWS-managed CodePipeline path from GitHub to Elastic Beanstalk. Complete the one-time GitHub connection authorization in the AWS console before expecting push-triggered deployments to run.
{{ENDIF}} {{IF eb_deploy_method == “manual”}}
No automated deploy artifact was generated. Package and deploy manually:
VERSION_LABEL="v$(date +%Y%m%d%H%M%S)"
BUCKET="$(aws elasticbeanstalk create-storage-location --query S3Bucket --output text --region {{target_region}})"
zip -r app.zip . -x '.git/*' 'node_modules/*'
aws s3 cp app.zip "s3://${BUCKET}/{{app_name}}/${VERSION_LABEL}.zip" --region {{target_region}}
aws elasticbeanstalk create-application-version \
--application-name {{app_name}} \
--version-label "${VERSION_LABEL}" \
--source-bundle "S3Bucket=${BUCKET},S3Key={{app_name}}/${VERSION_LABEL}.zip" \
--region {{target_region}}
for ENVIRONMENT in {{EB_ENVIRONMENT_NAMES}}; do
aws elasticbeanstalk update-environment \
--environment-name "${ENVIRONMENT}" \
--version-label "${VERSION_LABEL}" \
--region {{target_region}}
done{{ENDIF}}
Export all Heroku config vars and import sensitive values to AWS Secrets Manager or SSM Parameter Store. Reference secrets in EB via the environmentsecrets namespace configured in beanstalk.tf; set non-sensitive config directly as EB environment properties.
{{ENDIF}} {{IF has_fargate}}
# Build Docker image
docker build -t {{app_name}}:latest .
# Tag for ECR
docker tag {{app_name}}:latest {{AWS_ACCOUNT_ID}}.dkr.ecr.{{target_region}}.amazonaws.com/{{app_name}}:latest
# Push to ECR
aws ecr get-login-password --region {{target_region}} | docker login --username AWS --password-stdin {{AWS_ACCOUNT_ID}}.dkr.ecr.{{target_region}}.amazonaws.com
docker push {{AWS_ACCOUNT_ID}}.dkr.ecr.{{target_region}}.amazonaws.com/{{app_name}}:latestThe Terraform configuration creates ECS services automatically. After pushing the image, force a new deployment:
aws ecs update-service \
--cluster {{app_name}}-cluster \
--service {{app_name}}-web \
--force-new-deployment \
--region {{target_region}}Export all Heroku config vars and import to AWS Secrets Manager / Parameter Store, then reference them in your ECS task definition.
This generated path uses standard ECS/Fargate Terraform. If an ECS Express Mode path becomes available in this skill, treat it as an optional simplification for the Fargate override path, not as a replacement for the Elastic Beanstalk default without explicit user choice.
{{ENDIF}}
{{IF has_beanstalk}}
http://{{EB_ENVIRONMENT_URL}}/
{{IF has_beanstalk_web}}eb_health_check_path_<app_sanitized>_web returns a successful response on its app’s EB environment URL
{{ENDIF}}
{{ENDIF}}
{{IF has_fargate}}https://{{ALB_DNS_NAME}}/https://{{ALB_DNS_NAME}}/health
{{ENDIF}}
{{IF has_postgres}}{{IF has_beanstalk}}
{{app_domain}} → CNAME → {{EB_ENVIRONMENT_URL}}{{ENDIF}} {{IF has_fargate}}
{{app_domain}} → CNAME → {{ALB_DNS_NAME}}{{ENDIF}}
After successful verification (recommend 48–72 hours of parallel running):
Once your application is fully running on AWS (no longer connecting from Heroku):
0.0.0.0/0 inbound rules remain; allow only VPC-internal traffic on database ports{{IF migration_approach == “interim_cutover_data_first”}}
interim_heroku_ingress_cidrs = [] and interim_db_public_access = false, terraform apply, then delete the interim ingress block — full procedure in “Interim Database Exposure” Step 4 above{{ENDIF}}
After successful verification (recommend 48–72 hours of parallel running):
Scale Heroku dynos to 0:
heroku ps:scale web=0 worker=0 -a {{app_name}}Disable Heroku maintenance mode (if still on):
heroku maintenance:off -a {{app_name}}Remove add-ons and delete app when confident:
heroku addons:destroy --confirm {{app_name}} <addon_name>
heroku apps:destroy --confirm {{app_name}}{{IF deferred_addons.length > 0}}
The following add-ons could not be automatically mapped to AWS equivalents and require manual migration:
| Add-On | Plan | Provider | Reason | Recommendation |
|---|
{{FOR addon IN deferred_addons}} | {{addon.addon_name}} | {{addon.addon_plan}} | {{addon.provider}} | {{addon.reason}} | {{addon.recommendation}} | {{ENDFOR}}
For each deferred add-on above:
{{ENDIF}}
### Template Variable Resolution
Replace template variables using these sources:
| Variable | Source |
|----------|--------|
| `{{target_region}}` | `preferences.json` → `global.target_region` |
| `{{app_name}}` | First app from `heroku-resource-inventory.json`.apps[] (repeat per-app for multi-app) |
| `{{heroku_apps_comma_separated}}` | All app names from design services, comma-separated |
| `{{migration_approach}}` | `preferences.json` → `global.migration_approach` |
| `{{migration_method}}` | `preferences.json` → `data.migration_method` |
| `{{containerization_status}}` | `preferences.json` → `operational.containerization_status` |
| `{{target_exit_date}}` | `preferences.json` → `global.target_exit_date` (or "not set") |
| `{{SOURCE_DB_*}}` | Placeholder — user fills from `heroku pg:credentials:url` output |
| `{{TARGET_DB_*}}` | Placeholder — user fills from Terraform output |
| `{{SOURCE_REDIS_*}}` | Placeholder — user fills from `heroku redis:credentials` output |
| `{{TARGET_REDIS_*}}` | Placeholder — user fills from Terraform output |
| `{{SOURCE_KAFKA_*}}` | Placeholder — user fills from `heroku kafka:info` output |
| `{{TARGET_MSK_*}}` | Placeholder — user fills from Terraform output |
| `{{AWS_ACCOUNT_ID}}` | Placeholder — user fills with their AWS account ID |
| `{{ALB_DNS_NAME}}` | Placeholder — user fills from Terraform output |
| `{{EB_ENVIRONMENT_URL}}` | Elastic Beanstalk web environment CNAME output |
| `{{EB_ENVIRONMENT_NAMES}}` | Space-separated generated Elastic Beanstalk environment names for all EB process types |
| `{{MSK_CLUSTER_ARN}}` | Placeholder — user fills from Terraform output |
| `{{MIGRATION_BUCKET}}` | Placeholder — user creates an S3 bucket for migration artifacts |
{{IF has_beanstalk_web}}
The generated per-app Elastic Beanstalk web variables have no defaults. A non-interactive plan fails and identifies any missing or invalid value.
{{ENDIF}}
cd terraform/
# Initialize providers and modules
terraform init
# Preview changes
terraform plan -out=tfplan
# Apply infrastructure
terraform apply tfplan
# Record outputs for data migration
terraform output > ../terraform-outputs.txt{{IF has_postgres}}
# Migrate PostgreSQL database
./scripts/migrate-postgres.sh{{ENDIF}} {{IF has_redis}}
# Migrate Redis data
./scripts/migrate-redis.sh{{ENDIF}}
{{IF has_beanstalk}}
Deploy through the selected Elastic Beanstalk deploy method from MIGRATION_GUIDE.md Phase 3. The default is the generated GitHub Actions workflow.
{{ENDIF}}
{{IF has_fargate}}
Build and push your container image, then update ECS services. See MIGRATION_GUIDE.md Phase 3 for details.
{{ENDIF}}
Follow the verification checklist in MIGRATION_GUIDE.md Phase 4, then perform DNS cutover per Phase 5.
{{PLACEHOLDER}} format. Replace with actual values from Heroku credentials and Terraform outputs.MIGRATION_GUIDE.md.
{{ENDIF}}### Template Variable Resolution
| Variable | Source |
|----------|--------|
| `{{generation_timestamp}}` | Current ISO 8601 timestamp |
| `{{heroku_apps_comma_separated}}` | All app names from design services |
| `{{target_region}}` | `preferences.json` → `global.target_region` |
| `{{estimated_monthly_total}}` | `estimation-infra.json` → total projected monthly cost |
| `{{environment_name}}` | `preferences.json` → `global.environment_naming` |
| `{{deferred_addons.length}}` | Count of entries in `aws-design.json`.deferred[] |
### Conditional Section Rules
- `has_beanstalk`: True if any service in design has `aws_service == "Elastic Beanstalk"`
- `eb_deploy_method`: `preferences.design_constraints.eb_deploy_method.value`; default to `"github_actions"` when absent and `has_beanstalk` is true
- `has_fargate`: True if any service in design has `aws_service == "Fargate"`
- `has_postgres`: True if any service has `aws_service` containing `"RDS PostgreSQL"` or `"Aurora PostgreSQL"`
- `has_redis`: True if any service has `aws_service == "ElastiCache Redis"`
- `has_kafka`: True if any service has `aws_service == "Amazon MSK"`
- `generation_warnings_exist`: True if `generation-warnings.json` has a NON-EMPTY `warnings` array (the file is always written, so test its contents, not its existence)
---
## Step 3: Generate Database Migration Scripts
Generate migration scripts ONLY for data stores present in the design. Place scripts in `$MIGRATION_DIR/scripts/`.
### 3A: PostgreSQL Migration Script
Trigger: has_redis == true
Write to $MIGRATION_DIR/scripts/migrate-redis.sh:
#!/usr/bin/env bash
set -euo pipefail
###############################################################################
# Redis Migration Script
# Migrates data from Heroku Redis to AWS ElastiCache Redis
#
# Prerequisites:
# - redis-cli installed (Redis client tools)
# - Network access to both source and target Redis instances
# - TLS support enabled in redis-cli (if source/target use TLS)
#
# Usage:
# 1. Fill in connection parameters below
# 2. Run: chmod +x migrate-redis.sh && ./migrate-redis.sh
###############################################################################
# ─── Source Connection (Heroku Redis) ────────────────────────────────────────
# Retrieve via: heroku redis:credentials -a <app_name>
SOURCE_REDIS_HOST="{{SOURCE_REDIS_HOST}}"
SOURCE_REDIS_PORT=
Kafka migration does NOT generate a standalone script because MirrorMaker 2 configuration is environment-specific and requires running infrastructure. The MIGRATION_GUIDE.md provides the procedure and configuration templates instead.
After writing scripts, ensure they are executable:
chmod +x $MIGRATION_DIR/scripts/migrate-postgres.sh # (if generated)
chmod +x $MIGRATION_DIR/scripts/migrate-redis.sh # (if generated)Verify all generated files:
MIGRATION_GUIDE.md exists and:
has_postgres: Contains “PostgreSQL Migration” subsectionhas_redis: Contains “Redis Migration” subsectionhas_kafka: Contains “Kafka Migration” subsectionhas_postgres: Does NOT contain “PostgreSQL Migration” subsectionhas_redis: Does NOT contain “Redis Migration” subsectionhas_kafka: Does NOT contain “Kafka Migration” subsectionhas_postgres: Contains “Heroku CLI Cutover Sequence” subsectionmigration_method == "dms": Contains DMS limitation warning about CDC/continuous replicationmigration_approach == "interim_cutover_data_first": Contains “Interim Database Exposure” section whose Step 1 is the TLS prerequisite gate and whose Step 2 offers only bounded-allowlist connectivity pathsmigration_approach == "interim_cutover_data_first": Contains “Platform Risk Advisory” section0.0.0.0/0 anywhere — interim access must be a scoped CIDR allowlist applied through Terraformcontainerization_status != "containerized": Contains “Containerization Prerequisites” sectionhas_beanstalk_web: Explains that each web app’s eb_application_port_<app>_web and eb_health_check_path_<app>_web are required before planninghas_beanstalk: Contains selected EB deploy method instructions and the EB DNS cutover target, and emits no CodePipeline artifact unless eb_deploy_method is "codepipeline"eb_health_check_path_<app>_web insteaddeferred_addons.length > 0: Contains “Manual Migration Items” sectionREADME.md exists and:
$MIGRATION_DIRScripts (if generated):
scripts/migrate-postgres.sh exists if has_postgresscripts/migrate-redis.sh exists if has_redischmod +x applied)| Error | Behavior | Impact |
|---|---|---|
| Template variable unresolvable | Use placeholder with {{VARIABLE_NAME}} format | User fills manually |
| No data stores in design | Omit Phase 2 entirely from guide | Valid — compute-only migration |
| No deferred add-ons | Omit Manual Migration Items section | Valid — all add-ons mapped |
| All three data stores absent | MIGRATION_GUIDE still generated (compute-only) | Valid migration path |
| Script write failure | Log warning, continue with remaining files | Parent captures in generation-warnings |
This file