Subchapter 27.46
references/phases/generate/generate-artifacts-scripts.mdMarkdown24 KBView on GitHub
Loaded by generate.md after generate-artifacts-infra.md completes (terraform files generated).
Execute ALL steps in order. Do not skip or optimize.
Transform the migration plan (generation-infra.json) into numbered migration scripts for data, container, secrets, and validation tasks.
Outputs:
scripts/ directory — Numbered migration scripts for data and service migrationRead the following artifacts from $MIGRATION_DIR/:
aws-design.json (REQUIRED) — AWS architecture design with cluster-level resource mappingsgeneration-infra.json (REQUIRED) — Migration plan with timeline and service assignmentspreferences.json (REQUIRED) — User preferences including target region, sizing, complianceIf any REQUIRED file is missing: STOP. Output: “Missing required artifact: [filename]. Complete the prior phase that produces it.”
Scan aws-design.json clusters[].resources[] to determine which resource categories exist.
Set boolean flags for downstream script generation:
aws_service containing “RDS”, “Aurora”, “DynamoDB”,
“ElastiCache”, “Redshift” OR gcp_type starting with google_sql_, google_firestore_,
google_bigtable_, google_redis_ (NOT google_bigquery_ — BigQuery is specialist-deferred
and carries no automated data-migration steps; see has_bigquery)gcp_type starting with google_bigquery_
OR aws_service = “Deferred — specialist engagement”aws_service = “S3” OR gcp_type = google_storage_bucketaws_service containing “Fargate”, “ECS”, “EKS”, or “Elastic Beanstalk”
OR gcp_type starting with google_cloud_run_, google_container_clusteraws_service containing “Secrets Manager”
OR gcp_type starting with google_secret_manager_Report detected categories to user: “Resource categories detected: [list active flags]”
Scripts 02-04 are generated only when the corresponding resource categories are detected:
$MIGRATION_DIR/
├── scripts/
│ ├── 01-validate-prerequisites.sh # Always
│ ├── 02-migrate-data.sh # Only if has_data_migration
│ ├── 03-migrate-containers.sh # Only if has_containers
│ ├── 04-migrate-secrets.sh # Only if has_secrets
│ └── 05-validate-migration.sh # Always (adapts checks)Every script defaults to dry-run mode — requires --execute flag to make changes
Every script includes a verification step after execution
Scripts are numbered for execution order
Scripts use set -euo pipefail for safety
Scripts log all actions to $MIGRATION_DIR/logs/
Fail fast on unset fill-ins at --execute: every script that reads user-supplied env vars (SOURCE_HOST, TARGET_DB_PASSWORD, ALB_DNS, …) checks them at the top of the execute path and exits listing ALL missing ones at once — never fails midway on the first empty var after work has started. Dry-run mode runs without them (printing which are unset). Pattern (adjust the for v in … list to match this script’s required env vars — it is a template, not a universal constant):
if [ "$DRY_RUN" = false ]; then
missing=()
# Adjust the list below to match this script's required env vars
for v in SOURCE_HOST TARGET_HOST TARGET_DB_PASSWORD; do
[ -z "${!v:-}" ] && missing+=("$v")
done
if [ ${#missing[@]} -gt 0 ]; then
echo "Cannot execute — set these first (see MIGRATION_GUIDE.md fill-in checklist): ${missing[*]}"
exit 1
fi
fiVerify all prerequisites before migration:
Skip this script entirely if has_data_migration is false.
Based on database and storage resources in aws-design.json:
Script preamble — emit FIRST, whenever this script is generated (any subsection below
active). The shebang, failure mode, and dry-run plumbing are not gated on any single resource
flag: a BigQuery-only project generates only the deferral notice below, and that script still
has to satisfy Script Quality Rule 1 (set -euo pipefail):
#!/usr/bin/env bash
set -euo pipefail
# Data migration
# Usage: ./02-migrate-data.sh [--execute]
DRY_RUN=true
[[ "${1:-}" == "--execute" ]] && DRY_RUN=false
echo "Mode: $([ "$DRY_RUN" = true ] && echo 'DRY RUN' || echo 'EXECUTE')"Cloud SQL to RDS/Aurora — include only if has_databases:
Read preferences.json → design_constraints.db_size.value to select the migration tool:
Inventory cross-check (before selecting the tool): If gcp-resource-inventory.json has a google_sql_database_instance with a disk size (config.disk_size_gb or legacy variants), map it to the Q13b band and compare against db_size.value. Terraform disk size is allocated capacity — an upper bound on actual data, so the comparison is asymmetric:
db_size.value band is LARGER than the allocated band — data cannot exceed its allocation, UNLESS the disk can autoresize. Branch on config.disk_autoresize (extracted by Discover only when explicitly set in HCL):
true — the allocation is a starting point, not a cap; the disk may have grown since the Terraform was written. Keep the user’s answer; no warning. Note it in the script header comment (# db_size source: user answer [value]; terraform disk_size_gb=[N] is autoresize-enabled, not a cap).false, or absent — treat the allocation as the effective bound. Warn and use the allocated band:Database size inconsistency: Clarify recorded
db_size: [value]but Terraform only allocatesdisk_size_gb: [N]([band]). Using the allocated band for tool selection — say “use my answer instead” if the disk has grown since this Terraform was written (e.g. autoresize is enabled outside this Terraform).
db_size.value band is SMALLER than the allocated band — plausible (actual data below allocation is the normal case). Keep the user’s answer; no warning.
Record any override in the script header comment (# db_size source: terraform disk_size_gb=[N] (allocated), overrode preferences value [value]).
Runtime size measurement (always emit, PostgreSQL path): Because neither the Q13b answer nor the allocated disk measures actual data, the generated script must measure it before migrating. Emit this block in the shared preamble (after connection variables), tailoring the advisory to the tool the script was generated with:
# Allocated disk is an upper bound, not a measurement — check actual data size first.
if [ -n "$SOURCE_HOST" ] && [ -n "${SOURCE_DB_PASSWORD:-}" ]; then
ACTUAL_GB=$(PGPASSWORD="$SOURCE_DB_PASSWORD" psql -h "$SOURCE_HOST" -U postgres -d "$DATABASE_NAME" -At \
-c "SELECT ceil(pg_database_size(current_database()) / 1024.0^3)")
echo "Actual database size: ${ACTUAL_GB} GB (this script was generated for the [band] band)"
else
ACTUAL_GB=""
echo "SKIP: actual-size check needs SOURCE_HOST and SOURCE_DB_PASSWORD set."
fiFollow it with ONE tool-appropriate advisory branch:
pgcopydb script: if [ -n "$ACTUAL_GB" ] && [ "$ACTUAL_GB" -lt 10 ]; then echo "NOTE: actual data is under 10 GB — plain pg_dump/pg_restore would be simpler (no pgcopydb install, no wal_level change). Consider regenerating."; fi
pg_dump script: if [ -n "$ACTUAL_GB" ] && [ "$ACTUAL_GB" -ge 10 ]; then echo "WARNING: actual data is ${ACTUAL_GB} GB — pg_dump may exceed your maintenance window above 10 GB. Regenerate with pgcopydb before proceeding."; exit 1; fi (hard stop: undersized tooling is the dangerous direction; oversized is merely inconvenient. The guards run in both dry-run and execute mode; when connection variables are unset — typical first dry run — the check is skipped with a notice, and the operator must fill them in before --execute anyway)
"<10GB" → use pg_dump/pg_restore
"10-100GB" or "100-500GB" → use pgcopydb (parallel copy; requires wal_level=logical on Cloud SQL)
">500GB" → use AWS DMS (continuous replication; generate DMS task config instead of a shell export script)
"unknown" → default to pgcopydb with a TODO comment to verify size before running
Generate the script with conditional branches based on db_size:
# Cloud SQL → RDS data migration
# Tool selection based on database size (preferences.json design_constraints.db_size.value):
# <10GB: pg_dump/pg_restore
# 10-500GB: pgcopydb (parallel copy, 3-5x faster than pg_dump)
# >500GB: AWS DMS recommended — see README-DMS.md if generated
# unknown: pgcopydb (safer default at unknown scale)
# Note: band comes from Q13b / allocated disk (an upper bound) — the preamble below
# measures ACTUAL data size and warns (or stops) if this tool choice doesn't fit it.
echo "=== Database Migration: Cloud SQL → RDS ==="
SOURCE_HOST="" # TODO: Set Cloud SQL IP
TARGET_HOST="" # From terraform output database_endpoint
DATABASE_NAME="" # TODO: Set database nameThen emit ONE of the following tool-specific blocks based on db_size:
If db_size is "<10GB" — emit pg_dump block:
# Tool: pg_dump/pg_restore (database < 10GB)
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] Would export from Cloud SQL using pg_dump: $DATABASE_NAME"
echo "[DRY RUN] Would import to RDS using pg_restore: $TARGET_HOST"
else
echo "Exporting from Cloud SQL with pg_dump..."
PGPASSWORD="$SOURCE_DB_PASSWORD" pg_dump \
-h "$SOURCE_HOST" -U postgres -d "$DATABASE_NAME" \
-Fc -f /tmp/migration_dump.pgc
echo "Importing to RDS with pg_restore..."
PGPASSWORD="$TARGET_DB_PASSWORD" pg_restore \
-h "$TARGET_HOST" -U postgres -d "$DATABASE_NAME" \
-Fc /tmp/migration_dump.pgc
rm -f /tmp/migration_dump.pgc
fiIf db_size is "10-100GB" or "100-500GB" — emit pgcopydb block:
# Tool: pgcopydb (database 10GB–500GB — parallel copy, 3-5x faster than pg_dump)
# Requires: pgcopydb installed, wal_level=logical on Cloud SQL source
# See: https://pgcopydb.readthedocs.io/
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] Would copy database using pgcopydb: $DATABASE_NAME"
echo "[DRY RUN] Source: $SOURCE_HOST → Target: $TARGET_HOST"
else
echo "Copying database with pgcopydb (parallel)..."
pgcopydb copy db \
--source "postgresql://postgres:${SOURCE_DB_PASSWORD}@${SOURCE_HOST}/${DATABASE_NAME}" \
--target "postgresql://postgres:${TARGET_DB_PASSWORD}@${TARGET_HOST}/${DATABASE_NAME}" \
--table-jobs 4 \
--index-jobs 4
fiIf db_size is ">500GB" — emit DMS guidance block instead of a shell script:
# Tool: AWS DMS (database > 500GB — continuous replication recommended)
# Single-pass export/import at this scale is high-risk and likely to exceed any maintenance window.
# AWS DMS provides continuous replication with a minimal cutover window (minutes, not hours).
#
# Steps:
# 1. Create a DMS replication instance in the target VPC
# 2. Create source endpoint (Cloud SQL PostgreSQL) and target endpoint (Aurora PostgreSQL)
# 3. Create a full-load + CDC replication task
# 4. Run full load, then let CDC catch up to < 1 second lag
# 5. Cut over DNS during the minimal lag window
#
# TODO: Configure DMS endpoints and task — see AWS DMS documentation:
# https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Source.PostgreSQL.html
echo "Database size > 500GB: AWS DMS is strongly recommended."
echo "See README-DMS.md for setup instructions."
echo "This script does not perform the migration directly at this scale."If db_size is "unknown" — emit pgcopydb block with prominent TODO:
# Tool: pgcopydb (database size unknown — safer default than pg_dump at unknown scale)
# TODO: Verify your database size before running. If > 500GB, use AWS DMS instead.
# Requires: pgcopydb installed, wal_level=logical on Cloud SQL source
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] Would copy database using pgcopydb: $DATABASE_NAME"
echo "[DRY RUN] TODO: Verify database size before executing"
else
echo "Copying database with pgcopydb..."
pgcopydb copy db \
--source "postgresql://postgres:${SOURCE_DB_PASSWORD}@${SOURCE_HOST}/${DATABASE_NAME}" \
--target "postgresql://postgres:${TARGET_DB_PASSWORD}@${TARGET_HOST}/${DATABASE_NAME}" \
--table-jobs 4 \
--index-jobs 4
fi# Verification
echo "=== Verification ==="
echo "TODO: Compare row counts between source and target"BigQuery — specialist-deferred (no automated AWS target) — include only if has_bigquery:
Design marked every google_bigquery_* resource as Deferred — specialist engagement with
no_automated_aws_target: true (see references/phases/design/design-infra.md → BigQuery specialist gate).
Therefore do not generate BigQuery export, copy, or load steps, and do not name an AWS analytics or
warehouse target (no Athena, Redshift, Glue, EMR, Lake Formation, or a prescribed “data lake on S3”).
Emit only the deferral notice below:
# BigQuery — AWS target deliberately NOT selected
# This plugin does not choose an AWS analytics or warehouse target for BigQuery
# (no Athena/Redshift/Glue/EMR recommendation, and no prescribed data lake on S3).
# Engage your AWS account team and/or a data analytics migration partner before
# data warehouse, lake, SQL analytics, or BI cutover planning — query patterns,
# data volumes, ETL/ELT, and downstream consumers must be assessed by specialists.
# No BigQuery migration steps are generated here by design.
echo "BigQuery: AWS target deferred — specialist engagement required."
echo " Next step: engage your AWS account team and/or a data analytics migration partner."
echo " No BigQuery export or load steps are generated by this skill."Firestore to DynamoDB — include only if has_databases:
# Firestore → DynamoDB migration
# TODO: Use AWS DMS or custom export/import scriptSkip this script entirely if has_containers is false.
This script has two independent sections; emit each only if its services are present in aws-design.json:
DRY_RUN, account/region). Both sections depend on it, so it must not live inside either one.Dockerfile (plus a Dockerrun.aws.json only for the pre-built-image case, which this skill does not use) and EB builds the image during deployment; for the language platforms (Python/Node/etc.) you ship the app source. So there is no ECR push for EB — do not create an ECR repo for an EB service. The EB section bundles the app source, uploads it to the EB-managed S3 bucket, creates an application version, and deploys it to the environment Terraform provisioned.Shared preamble (always emitted):
#!/usr/bin/env bash
set -euo pipefail
# App migration: container images (→ ECR) and/or Elastic Beanstalk source deploy
# Usage: ./03-migrate-containers.sh [--execute]
DRY_RUN=true
[[ "${1:-}" == "--execute" ]] && DRY_RUN=false
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
AWS_REGION="us-east-1" # From preferences.json target_regionECR image migration — GCR/Artifact Registry → ECR (include ONLY if Fargate/ECS/EKS present):
ECR_REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
# TODO: List container images from aws-design.json compute resources
IMAGES=(
"gcr.io/project/image1:latest"
# Add more images from your GCP container registry
)
for IMAGE in "${IMAGES[@]}"; do
IMAGE_NAME=$(echo "$IMAGE" | rev | cut -d'/' -f1 | rev | cut -d':' -f1)
IMAGE_TAG=$(echo "$IMAGE" | rev | cut -d':' -f1 | rev)
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] Would migrate: $IMAGE → $ECR_REGISTRY/$IMAGE_NAME:$IMAGE_TAG"
else
echo "Creating ECR repository: $IMAGE_NAME"
aws ecr create-repository --repository-name "$IMAGE_NAME" --region "$AWS_REGION" 2>/dev/null || true
echo "Pulling from GCR: $IMAGE"
docker pull "$IMAGE"
echo "Tagging for ECR: $ECR_REGISTRY/$IMAGE_NAME:$IMAGE_TAG"
docker tag "$IMAGE" "$ECR_REGISTRY/$IMAGE_NAME:$IMAGE_TAG"
echo "Pushing to ECR..."
aws ecr get-login-password --region "$AWS_REGION" | docker login --username AWS --password-stdin "$ECR_REGISTRY"
docker push "$ECR_REGISTRY/$IMAGE_NAME:$IMAGE_TAG"
fi
doneElastic Beanstalk source deploy (include ONLY if an EB environment is in aws-design.json):
# EB builds the image from your Dockerfile at deploy time (Docker platform) or runs the app
# source directly (language platforms). No ECR push. One deploy per App Engine service → EB env.
# TODO: one entry per EB environment from aws-design.json (eb_application, eb_environment, source_service)
EB_APP="gcp-migration" # aws_config.eb_application
EB_ENVS=(
"default" # aws_config.eb_environment (one per source_service)
# "worker"
)
EB_BUCKET="elasticbeanstalk-${AWS_REGION}-${AWS_ACCOUNT_ID}"
for EB_ENV in "${EB_ENVS[@]}"; do
VERSION_LABEL="${EB_ENV}-$(date -u +%Y%m%d%H%M%S)" # stamp at run time
BUNDLE="/tmp/${EB_APP}-${EB_ENV}.zip"
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] Would bundle ./${EB_ENV} (incl. Dockerfile), upload to s3://${EB_BUCKET}, and deploy version ${VERSION_LABEL} to ${EB_APP}/${EB_ENV}"
else
echo "Bundling source for ${EB_ENV} (must include Dockerfile for the Docker platform)..."
( cd "./${EB_ENV}" && zip -r "$BUNDLE" . -x '*.git*' )
aws s3 cp "$BUNDLE" "s3://${EB_BUCKET}/${VERSION_LABEL}.zip" --region "$AWS_REGION"
aws elasticbeanstalk create-application-version --application-name "$EB_APP" \
--version-label "$VERSION_LABEL" \
--source-bundle "S3Bucket=${EB_BUCKET},S3Key=${VERSION_LABEL}.zip" --region "$AWS_REGION"
aws elasticbeanstalk update-environment --application-name "$EB_APP" \
--environment-name "$EB_ENV" --version-label "$VERSION_LABEL" --region "$AWS_REGION"
fi
doneVerification (always emitted; each line only reports on the sections you included):
echo "=== Verification ==="
# Include the next two lines only if the ECR section was emitted:
echo "Listing ECR repositories (Fargate/ECS/EKS)..."
aws ecr describe-repositories --region "$AWS_REGION" --query 'repositories[].repositoryName' --output table 2>/dev/null || echo "No ECR repositories (expected if EB-only)"
# Include the next two lines only if the EB section was emitted:
echo "Listing Elastic Beanstalk environments..."
aws elasticbeanstalk describe-environments --region "$AWS_REGION" --query 'Environments[].{Env:EnvironmentName,Status:Status,Health:Health}' --output table 2>/dev/null || echo "No EB environments"Skip this script entirely if has_secrets is false.
Migrate secrets from GCP Secret Manager to AWS Secrets Manager:
#!/usr/bin/env bash
set -euo pipefail
# Secrets migration: GCP Secret Manager → AWS Secrets Manager
# Usage: ./04-migrate-secrets.sh [--execute]
DRY_RUN=true
[[ "${1:-}" == "--execute" ]] && DRY_RUN=false
# TODO: List secrets to migrate
SECRETS=(
"database-password"
"api-key"
# Add more secrets from your GCP project
)
for SECRET_NAME in "${SECRETS[@]}"; do
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] Would migrate secret: $SECRET_NAME"
else
echo "Reading secret from GCP: $SECRET_NAME"
# Write to a restricted temp file — avoids secret values appearing in shell
# variables, process lists (ps aux), or shell history. Cleaned up on exit.
TMPFILE=$(mktemp)
chmod 600 "$TMPFILE"
trap 'rm -f "$TMPFILE"' EXIT
gcloud secrets versions access latest --secret="$SECRET_NAME" > "$TMPFILE"
echo "Creating/updating secret in AWS: $SECRET_NAME"
aws secretsmanager create-secret \
--name "$SECRET_NAME" \
--secret-string "file://$TMPFILE" \
--tags Key=MigrationSource,Value=gcp-secret-manager 2>/dev/null || \
aws secretsmanager put-secret-value \
--secret-id "$SECRET_NAME" \
--secret-string "file://$TMPFILE"
rm -f "$TMPFILE"
trap - EXIT
fi
done
# Verification
echo "=== Verification ==="
aws secretsmanager list-secrets --query 'SecretList[].Name' --output tablePost-migration validation script. Always generated, but adapt checks based on which resource categories were detected in Step 1. Only include validation sections for resources that exist.
#!/usr/bin/env bash
set -euo pipefail
# Post-migration validation
# Usage: ./05-validate-migration.sh
echo "=== Migration Validation ==="
# Check Terraform state (always included)
echo "--- Terraform Resources ---"
cd terraform/
terraform state list | wc -l
echo "resources in Terraform state"
# --- Include ONLY if Fargate/ECS/EKS present ---
# Check ECS services
echo "--- ECS Services ---"
aws ecs list-services --cluster "${PROJECT_NAME:-gcp-migration}" --query 'serviceArns' --output table 2>/dev/null || echo "No ECS cluster found"
# --- Include ONLY if Elastic Beanstalk present ---
echo "--- Elastic Beanstalk Environments ---"
aws elasticbeanstalk describe-environments --query 'Environments[].{Env:EnvironmentName,Status:Status,Health:Health}' --output table 2>/dev/null || echo "No EB environments found"
# --- Include ONLY if has_databases ---
# Check RDS instances
echo "--- RDS Instances ---"
aws rds describe-db-instances --query 'DBInstances[].{ID:DBInstanceIdentifier,Status:DBInstanceStatus,Endpoint:Endpoint.Address}' --output table 2>/dev/null || echo "No RDS instances found"
# --- Include ONLY if has_storage ---
# Check S3 buckets
echo "--- S3 Buckets ---"
aws s3 ls | grep "${PROJECT_NAME:-gcp-migration}" || echo "No matching S3 buckets found"
# --- Include ONLY if has_secrets ---
# Check secrets
echo "--- Secrets Manager ---"
aws secretsmanager list-secrets --query 'SecretList[].Name' --output table 2>/dev/null || echo "No secrets found"
echo "=== Validation Complete ==="
echo "Review the output above. All resources should show healthy status."
echo "TODO: Run application-level health checks"
echo "TODO: Compare performance metrics against GCP baseline"After generating all scripts, verify the following quality rules:
set -euo pipefailbq extract / export steps) — BigQuery resources
carry only the specialist-engagement deferral notice, consistent with no_automated_aws_target: trueReport the list of generated script files to the parent orchestrator. Do NOT update .phase-status.json — the parent generate.md handles phase completion.
Only list scripts that were actually generated (based on Step 1 resource detection flags):
Resource categories detected: [list active flags from Step 1]
Generated migration scripts:
- scripts/01-validate-prerequisites.sh
- scripts/02-migrate-data.sh # only if has_data_migration
- scripts/03-migrate-containers.sh # only if has_containers
- scripts/04-migrate-secrets.sh # only if has_secrets
- scripts/05-validate-migration.sh
Total: [N] migration scripts
TODO markers: [N] items requiring manual configuration
Skipped scripts: [list any scripts not generated, with reason]