Skip to main content

Migrate AWS → Sprintsail Runtime

This is the workflow Sprintsail is built around. Take an app you've deployed to AWS, run sail migrate, and the same app comes up on a Kubernetes runtime — Contour ingress, cert-manager TLS, a sealed Secret, CloudNativePG — with the same data and the identical handler code.

This walkthrough is verbatim what was shipped: see commit 49848e7 and the sail-poc example.

What you'll end up with

SideReality
AWSRDS Postgres (4 rows), Secrets Manager api-key, ECS Express WebApp
Sprintsail Runtime (K3s on EC2)Contour ingress with TLS via sail-issuer, CNPG Postgres with the same 4 rows, SealedSecret with the same api-key value, WebApp built+pushed to ECR

A curl to the runtime URL returns the migrated data — proof that the SAME src/server.ts runs on either target.

Prerequisites

  • The app from Quickstart deployed to AWS with seed data.

  • A Sprintsail Runtime cluster. The cheapest real one: K3s on a single EC2 instance (~$60/mo running, ~$3/mo stopped). Recipe:

    # Launch a t3.large EC2 (8 GB RAM is the minimum for the full operator stack)
    # Open ports 22 (your IP), 80 + 443 (world), 6443 (your IP)
    curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC='--disable traefik --tls-san <node-ip>' sh -
    sudo cat /etc/rancher/k3s/k3s.yaml # pull this to your laptop, rewrite 127.0.0.1 → <node-ip>

    Then run the operator bootstrap:

    cd sprintsail/runtime-bootstrap
    ./install.sh sail-runtime-k3s

    See Sprintsail Runtime target for the full setup.

  • ECR registry wired so the SDK's image push lands somewhere the K3s node can pull from:

    aws ecr create-repository --repository-name sprintsail/orders-web
    aws ecr get-login-password | docker login --username AWS \
    --password-stdin <account>.dkr.ecr.us-east-1.amazonaws.com
    ./refresh-ecr-auth.sh ubuntu@<node-ip> ~/.ssh/<your-key>.pem # writes /etc/rancher/k3s/registries.yaml

The migrate command

From your project root:

SAIL_IMAGE_REGISTRY=<account>.dkr.ecr.us-east-1.amazonaws.com \
SAIL_INGRESS_DOMAIN=<node-ip-with-dashes>.sslip.io \
npx sail migrate \
--from aws:us-east-1 \
--to sprintsail-runtime:sail-runtime-k3s \
--yes

What you'll see:

Migration plan: aws:us-east-1 → sprintsail-runtime:sail-runtime-k3s
Project: orders

✓ database orders
✓ secret api-key
✓ webapp web

Provisioning equivalents on destination...
[k8s/database] created Cluster sail-orders/orders-orders (postgres 16, 1 instance, 1Gi)
[k8s/secret] created SealedSecret sail-orders/orders-api-key (encrypted at rest)
[k8s/webapp] preparing sail-orders/orders-web
[k8s/image] docker build (linux/amd64) <ecr>/sprintsail/orders-web:...
[k8s/image] docker push <ecr>/sprintsail/orders-web:...
[k8s/webapp] created Deployment sail-orders/orders-web
[k8s/webapp] TLS via cert-manager issuer "sail-issuer"
[k8s/webapp] created Ingress sail-orders/orders-web

Copying data...
[k8s/migrate] opened RDS SG sg-c68d33fe to cluster egress <node-ip>/32:5432
[k8s/migrate] running pg_dump|psql Job sail-orders/sail-dbmig-orders-orders-rw-... (AWS RDS orders-orders.<rds-host> → orders-orders-rw.sail-orders.svc.cluster.local)
[k8s/migrate] database copied AWS RDS orders-orders.<rds-host> → orders-orders-rw.sail-orders.svc.cluster.local/app
[k8s/migrate] secret value copied aws:us-east-1/orders/api-key → sail-orders/orders-api-key

Migration complete.

Then hit the runtime URL:

curl -sk https://orders-web.<node-ip-with-dashes>.sslip.io/

You get back the same JSON the AWS WebApp served — same rows, same api-key prefix — but it's coming from CNPG on Kubernetes, with a sealed Secret, behind cert-manager TLS.

How the data copy works

This is the bit AWS punts on (its migrateData() for Database is a manual stub). The runtime provider automates it with a Kubernetes Job.

  1. Auto-open the source SG. The provider reads securityGroupId from the source RDS attributes (set by the AWS provider on provision) and authorizes 5432 inbound from the destination cluster's external egress IP. Idempotent. For the same-VPC case (intra-VPC traffic uses the private IP, which a CIDR rule can't cover), the failure message includes the exact SG-to-SG authorize-security-group-ingress command.

  2. Resolve source creds. The provider calls GetSecretValueCommand on the AWS Secrets Manager secret named in the RDS attributes. Builds postgresql://<user>:<pass>@<rds>:5432/postgres.

  3. Spawn an in-cluster Job. Image is the CNPG-tagged Postgres so pg_dump is the right version. The Job runs:

    set -e
    pg_dump --clean --if-exists --no-owner --no-privileges -f /tmp/dump.sql "$SRC_URL"
    psql -v ON_ERROR_STOP=1 -f /tmp/dump.sql "$DST_URL"

    File + && rather than a pipe — sh/dash has no pipefail, so a piped pg_dump | psql would mask a dump failure (psql gets empty input, exits 0, Job falsely succeeds). Caught during the live POC; permanent fix.

  4. Secret value copy. AWS GetSecretValueCommand → read-modify-replace on the destination materialized Secret (the K8s client's PATCH defaults to JSON-Patch, hence replace not patch).

The cutover script

After migration, .sail/cutover-<timestamp>.sh lists every URL/endpoint that changed:

Resource: orders.database.orders
endpoint: orders-orders.<rds-host>
→ orders-orders-rw.sail-orders.svc.cluster.local
Resource: orders.webapp.web
url: https://sa-<id>.ecs.us-east-1.on.aws
→ https://orders-web.<node-ip>.sslip.io

Use this as the checklist for downstream consumers (DNS records, webhook URLs, etc.) before running sail destroy --target aws:us-east-1.

Gotchas the POC surfaced

  • Image architecture. Apple Silicon builds arm64 by default; cloud K8s nodes are amd64. The provider builds linux/amd64 when pushing to a registry (vs. kind-load, which preserves host arch). Override with SAIL_IMAGE_PLATFORM=linux/arm64 for arm64 clusters (Graviton).
  • Same-VPC RDS reachability. If the destination cluster is in the same VPC as the source RDS, DNS resolves the RDS endpoint to its private IP, and CIDR-based SG rules don't cover that path. Add an SG-to-SG rule.
  • ECR token expiry. 12 hours. refresh-ecr-auth.sh re-issues + restarts K3s. The "real" answer is an IAM instance profile + the kubelet credential provider — v1.1.

What this proves

sail migrate aws → sprintsail-runtime works for real, on real infrastructure, with real data. The headline isn't a kind-cluster demo; it's a Postgres dump from an actual RDS over the internet into an actual CNPG cluster on an EC2 you'd happily run a workload on.

The handler code didn't change. The architectural moat — "we own both halves and we don't lock either" — is structurally sound.