MIGRATION PLAYBOOK · DATA
A practical data migration playbook for STACKIT
Moving data to STACKIT is one repeatable sequence per store: preflight, rehearsal, copy, write freeze, final sync, reconcile, cleanup. What changes between a PostgreSQL database, a bucket, a volume and a cache is the method. StackToIT never moves data itself; it lists the stateful units of your estate, offers the methods that are documented for each, and writes the checks that must pass before you accept the move. This playbook walks that sequence on a four-store estate.
The sequence, store by store
1. List the stateful units
Start from migration/data-movement.md. It lists every unit that holds data of its own: a database, a cache, a block volume, an object store. Queues and the system on a server's disk are not read as stateful. Provisioned capacity shown beside a unit is what was provisioned, not the size of the data.
2. State the numbers the source cannot
Data size, change rate, bandwidth, recovery point, recovery time and tolerated downtime are never in the Terraform. The run marks each UNKNOWN until you supply it, and a duration it prints is only a lower bound of the transfer. Without these numbers no method can be shown to fit a window.
3. Choose one method per store
The plan lists candidate methods for each unit and marks others unsupported, with the reason. The choice is a recorded decision, not a default. The table below shows the four stores of this run.
4. Preflight
Close the prerequisites the preflight checks before anything is copied: extensions, roles and TLS on the database, versions and archived objects in the bucket, the file system and free space on the volume, the commands the copy needs and the client TLS settings on the cache. A few, such as object metadata, are closed by the rehearsal instead.
5. Rehearse
Copy once into the empty target while the source stays in service, and measure the real throughput. The rehearsal is where a window estimate becomes a number you can defend.
6. Freeze writes, final sync
Stop the writers, copy what changed since the last pass, and keep the source untouched. For object stores the plan sets out an initial copy and a delta sync before the freeze, so the frozen part is short.
7. Reconcile
Compare both sides with the writers still stopped: row counts, checksums and sequences for the database, object counts and bytes for the bucket, key counts and key lists for the cache. Accept the move only on a recorded GO.
8. Switch, then clean up
Point the clients at the target, keep the source for the period the owner records, and delete temporary keys, dump files and the move host afterwards. The Object Storage key matters most: a credential created in a project has access to every bucket in that project (docs).
Four stateful units, listed
An estate written for this page and assessed by StackToIT on 4 November 2026: a PostgreSQL database, a versioned bucket, a block volume and a cache. First two panes: the data-bearing blocks of the input as two verbatim line ranges of main.tf, the database to the volume and then the cache (the network, server and attachment blocks around them are left out). Last pane: the table at the top of migration/data-movement.md, unchanged; on a narrow screen it scrolls sideways.
resource "aws_db_instance" "orders" {
identifier = "orders-db"
engine = "postgres"
engine_version = "15.7"
instance_class = "db.m5.large"
allocated_storage = 200
storage_type = "gp3"
username = "orders"
password = var.db_password
db_subnet_group_name = aws_db_subnet_group.main.name
multi_az = true
backup_retention_period = 7
skip_final_snapshot = true
}
resource "aws_s3_bucket" "uploads" {
bucket = "acme-orders-uploads"
}
resource "aws_s3_bucket_versioning" "uploads" {
bucket = aws_s3_bucket.uploads.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_ebs_volume" "reports" {
availability_zone = "eu-central-1a"
size = 500
type = "gp3"
}resource "aws_elasticache_cluster" "sessions" {
cluster_id = "sessions"
engine = "redis"
engine_version = "7.0"
node_type = "cache.t3.small"
num_cache_nodes = 1
subnet_group_name = "sessions"
}| Unit | Source service | Target service | Application | Mechanism | State | Data class | RPO | Downtime tolerance |
|---|---|---|---|---|---|---|---|---|
| aws_db_instance.orders | aws_db_instance | STACKIT PostgreSQL Flex | | UNKNOWN | DECISION_REQUIRED | NOT_PROVIDED | NOT_PROVIDED | NOT_PROVIDED |
| aws_ebs_volume.reports | aws_ebs_volume | STACKIT Block Storage | | UNKNOWN | DECISION_REQUIRED | NOT_PROVIDED | NOT_PROVIDED | NOT_PROVIDED |
| aws_elasticache_cluster.sessions | aws_elasticache_cluster | STACKIT Key Value Store (Valkey) | | UNKNOWN | DECISION_REQUIRED | NOT_PROVIDED | NOT_PROVIDED | NOT_PROVIDED |
| aws_s3_bucket.uploads | aws_s3_bucket | STACKIT Object Storage | | UNKNOWN | DECISION_REQUIRED | NOT_PROVIDED | NOT_PROVIDED | NOT_PROVIDED |Every mechanism starts as UNKNOWN
Four units, four DECISION_REQUIRED states. The data class, recovery point and downtime tolerance come from the customer's applications file, word for word; without one they stay NOT_PROVIDED and no default is assumed.
The volume has one open method
For the block volume only a file-level copy between servers is offered, so the execution plan takes it. The other three units wait for your answer.
Nothing is executed
The file says it in its first lines: StackToIT never moves data. Every step in the plan is for a person.
Which method for which store
As the run lists them. Tools named in a method are the operator's own to install and run; the plan does not run or verify them.
| Store and target | Methods the plan lists | Not offered, and why |
|---|---|---|
| PostgreSQL 15.7 database → PostgreSQL Flex (versions 14–18 offered) | Export and import with the engine clients (dump and restore); logical replication; a tool of your own. Logical replication needs the source's logical replication setting switched on, which means a parameter change and a reboot there, and an unread replication slot fills the source's storage. | Nothing is struck out. Schema, later DDL, sequences and large objects are not replicated by logical replication: the schema is copied first and the sequences at the freeze. |
| Versioned bucket → Object Storage | Copy with rclone, or sync with the AWS command line; a tool of your own. The plan sets out an initial copy, a delta sync, a freeze and a final sync. | S3 replication: the AWS documentation names only S3 buckets as destinations, so the plan offers a copy. A copy of current objects carries no older versions, delete markers, retention, ACLs or policies; the versions prerequisite is marked FAILS until you decide the history is wanted. |
| 500 GiB block volume → Block Storage | File-level copy between servers, for example with rsync, with a pass while the services run and a last pass inside the freeze. | Block-level image copy: no read page says a data-disk image can be the source of a STACKIT volume, and an AWS snapshot has no export path the plan can name. |
| Redis 7.0 cache → Key Value Store (Valkey) | A client that copies keys (scan, dump, restore) inside the freeze, as in the STACKIT tutorial, which uses the MIGRATE command and a script as an alternative. | Loading an exported backup file and replicating from the old primary: no STACKIT page or API read documents either, and the source cache does not offer replication commands. |
The cache method is inferred from the tutorial's procedure and has not been run by StackToIT against a Key Value Store, as the plan itself states. The tutorial quotes about 50 seconds per gigabyte on average and warns that keys with line breaks can break its command-line pipe.
The steps and the checks of one move
From migration/data-movement-plan.json, for the dump-and-restore method of the database. Left: the six steps. Right: the row-count, checksum and sequence checks, with the exact command each one names.
"steps": [
{
"kind": "preflight",
"id": "MAN/data/aws_db_instance.orders/preflight",
"in_plan": true
},
{
"kind": "rehearsal",
"id": "MAN/data/aws_db_instance.orders/rehearsal",
"in_plan": true
},
{
"kind": "write-freeze",
"id": "MAN/data/aws_db_instance.orders/write-freeze/dump-restore",
"in_plan": true
},
{
"kind": "final-sync",
"id": "MAN/data/aws_db_instance.orders/final-sync/dump-restore",
"in_plan": true
},
{
"kind": "reconcile",
"id": "MAN/data/aws_db_instance.orders/reconcile/dump-restore",
"in_plan": true
},
{
"kind": "cleanup",
"id": "MAN/data/aws_db_instance.orders/cleanup/dump-restore",
"in_plan": true
}
], {
"id": "row-counts",
"kind": "COUNT_COMPARISON",
"mode": "COMMAND",
"what": "Count the rows of every table on both sides, with the writers stopped (an exact count of each table; the estimates of pg_stat_user_tables are not counts).",
"expected": "the same tables with the same number of rows on both sides",
"command": "SELECT format('SELECT %L AS tbl, count(*) FROM %I.%I;', schemaname || '.' || tablename, schemaname, tablename) FROM pg_tables WHERE schemaname NOT IN ('pg_catalog', 'information_schema') \\gexec"
},
{
"id": "table-checksums",
"kind": "CONTENT_CHECK",
"mode": "COMMAND",
"what": "Take the md5 of the rows of every keyed table that matters on both sides, ordered by its key (a key range for a large table; DateStyle, TimeZone and extra_float_digits equal in both sessions).",
"expected": "the row count and the md5 are equal on both sides for every table checked",
"command": "SELECT count(*), md5(string_agg(t::text, ',' ORDER BY t.<key-column>)) FROM <schema>.<table> t;",
"not_provided": [
"the key column of each table (<key-column>)",
"the tables that matter"
]
},
{
"id": "sequences",
"kind": "CONTENT_CHECK",
"mode": "COMMAND",
"what": "Compare the position of every sequence on both sides, so that the first insert on the target does not repeat a key.",
"expected": "the positions are equal, or the target's is higher",
"command": "SELECT sequencename, last_value FROM pg_sequences ORDER BY 1;"
},Checks compare, they do not trust
Each check states what it compares and what result counts as equal. Row counts use an exact count of every table, not the planner's estimates; checksums need a key column per table that you name.
Some checks are yours by design
The application check and the rollback limit are marked manual: the read and the write belong to the application, and how long the source is kept is the owner's decision. The plan names the limit and no date.
Rollback is the untouched source
On a NO-GO the clients still point at the source, which is current to the freeze. Until the target accepts its first write after the switch, a rollback is only the connection settings put back. After that, PostgreSQL Flex's own daily backups with transaction logs allow a point-in-time restore, but only into a new instance in the same project; there is no in-place restore, so clients are repointed again (docs).
Decisions the architect owns
Method per store
Dump and restore against logical replication is a trade between a longer freeze and a change to the source. Decide it once the size and the tolerated downtime are stated.
Version history of the bucket
Copy only current objects, or plan the procedure for versions and delete markers. The default copy does not carry history.
Permissions and roles
Roles, grants and extensions of the database are re-created on the target; the plan lists the queries that read them from the source.
Sessions in the cache
Whether a session cache is worth copying at all, or whether users sign in again, is a business call. That framing is practice, not a finding of the run.
How long the source stays
The date until which the source is kept, and whether writes made on the target are copied back or accepted as lost.
Data migration questions
Does StackToIT copy my data?
No. It lists the stateful units, the candidate methods, the prerequisites and the checks, and it opens no connection to your data. A person or your own tooling performs every step.
How long will the copy take?
The run cannot say. A duration is a lower bound computed from the data size and bandwidth you state, and it only tells you whether that bound already exceeds the accepted downtime. Measure the throughput in the rehearsal.
Can I keep the databases online during the move?
Logical replication keeps the source in service until the freeze, at the cost of a source reboot and a replication slot you must watch. Dump and restore runs inside the freeze, so the freeze lasts as long as the dump, the restore and the checks.
What happens to bucket versions and ACLs?
A copy of current objects carries neither. The plan flags the versions prerequisite and points to the checks that show whether the source has them.
How do I back out?
Before the first write on the target, point the clients back at the source. Afterwards the owner's recorded rollback limit decides whether writes are copied back or lost.
Sources
Your next move: STACKIT.
Tell us about your estate. We show you what StackToIT delivers for it: target architecture, Terraform artifacts and a structured migration plan.