The sail migrate model
sail migrate is what makes the SDK's "no lock-in" claim load-bearing. It moves a deployed application between any two targets: AWS region → AWS region, or AWS → Sprintsail Runtime, or eventually GCP → Azure.
The contract
sail migrate --from <source-target> --to <destination-target> [--yes]
sail migrate does three things in order:
- Plan. Read the source target's state. Print every primitive that will be migrated.
- Provision. Walk the source state's primitives. For each one, call the destination provider's
provision()to create an equivalent. Save the new state. - Data copy. Walk them again. For each one with stateful payload (database rows, bucket objects, secret values), call the destination provider's
migrateData(). Save a cutover script.
Stateless primitives (Function, WebApp, etc.) are simply re-provisioned. Their state is in their container image, which the destination provider rebuilds + pushes.
sail migrate does not delete the source. You explicitly run sail destroy --target <source> once you've cut traffic over and verified.
Why the destination provider does the data copy
When source and destination providers differ — say AWS source, Sprintsail Runtime destination — only one provider's migrateData() runs: the destination's. It's responsible for reading from the source using whatever cross-provider mechanism makes sense.
This means the runtime provider knows how to read AWS RDS endpoints + Secrets Manager. The AWS provider, when used as a destination, knows how to read CloudNativePG endpoints + sealed secrets. The contract scales: a future GCP provider has to know how to read source state from each of its predecessors. It's a quadratic concern, but the matrix is small (4 providers × 4 sources max) and each pair is concrete.
How data copy actually happens
Bucket → Bucket
The destination provider lists the source bucket and copies each object. AWS → AWS uses server-side CopyObject (no traffic through your machine). Runtime → Runtime uses the S3 SDK against MinIO.
Secret → Secret
The destination provider fetches the source value with the source's SDK (GetSecretValueCommand for AWS, a K8s Secret read for the runtime) and writes it into the destination's storage (a fresh Secrets Manager value, or a re-sealed SealedSecret).
Database → Database
The destination provider creates an in-cluster Job (when destination is Sprintsail Runtime) that runs pg_dump | psql. The Job is given the source connection URL (built from RDS endpoint + Secrets Manager creds, or from another CNPG cluster) and the destination URL, and it streams the dump through. No host pg_dump, no manual download/upload.
(Aside: this avoids the sh/dash pipefail gotcha by dumping to a file first and chaining with &&. See DB auto-migration for the gory detail.)
Queue
No data is copied. In-flight messages in SQS or RabbitMQ are ephemeral and not part of the migration contract. Plan your cutover to drain the source queue first.
Function / WebApp / API / CronJob / Worker
Stateless. The destination provider rebuilds the container image and provisions a fresh equivalent. The handler code is identical because primitive contracts are stable.
The cutover script
After data copy, sail migrate writes .sail/cutover-<timestamp>.sh. It's a bash script listing every primitive whose URL / endpoint / ARN changed between source and destination, with echo lines showing the old → new mapping.
This is intentionally not automated — you decide when to flip DNS, update downstream consumers, change webhooks, etc. The script is a checklist, not an executor.
What happens if I run it twice?
Idempotent, like sail deploy. The destination provider adopts existing resources. The data copy step also re-runs cleanly: pg_dump --clean --if-exists drops + recreates schemas on the destination; bucket copy is overwrite-safe; secret value overwrite is the desired behavior on re-run (you're refreshing).
Tutorials
- AWS → Sprintsail Runtime (the canonical walkthrough) — same demo app, same data, two targets.