Skip to main content

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:

  1. Plan. Read the source target's state. Print every primitive that will be migrated.
  2. Provision. Walk the source state's primitives. For each one, call the destination provider's provision() to create an equivalent. Save the new state.
  3. 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