+1 (541) 238-9429

Moving MySQL to RDS or Aurora: the replication cutover, step by step

Most teams that ask us to move MySQL onto RDS or Aurora arrive with one of two plans. The first is mysqldump on Saturday night and a four-hour maintenance window. The second is AWS DMS, chosen because it is the service with "migration" in the name. Both can work. Neither is what we do by default.

A platform move is a replication problem. You build the target, make it a replica of production, let it catch up, verify it, and then move writes during a pause measured in seconds. Everything below is the mechanics of doing that safely, and the decisions you have to make before you start.

Decide what you are moving to, first

RDS for MySQL and Aurora MySQL are not interchangeable, and the difference changes your migration path.

  • RDS for MySQL runs upstream MySQL (8.0 and 8.4) on EBS with a standby in another AZ for Multi-AZ. Failover is a DNS swap to the standby, typically under a minute or two. Behaviour matches the MySQL you already run, including binlog-based replication in and out.
  • Aurora MySQL is a MySQL-compatible engine on a shared distributed storage layer. Replicas read the same storage, so replica lag is usually milliseconds rather than a single-threaded apply problem, and failover is faster. In exchange you give up some knobs, you pay for I/O per request (unless you move to the I/O-Optimized configuration), and a small set of behaviours differ from upstream — notably around innodb_flush_log_at_trx_commit, the buffer pool warm-up, and features that assume local storage.

A useful way to choose: if replica lag or read scaling is your actual pain, Aurora buys you something structural. If your pain is that nobody wants to patch servers at 2am, RDS gets you there with fewer behavioural surprises. If you are on 5.7 today, note that both platforms charge for extended support on out-of-date major versions, so plan the version upgrade and the platform move as one project — but not as one step. Upgrade version and change platform in two cutovers if you can afford two windows; debugging a regression is much easier when only one variable moved.

One more constraint people discover late: neither platform gives you SUPER. Anything your runbooks do with SET GLOBAL on restricted variables, CHANGE REPLICATION SOURCE TO, or filesystem access changes shape. On RDS you use stored procedures (mysql.rds_set_external_source, mysql.rds_start_replication, mysql.rds_skip_repl_error) and parameter groups instead. Read your own automation for SUPER-dependent calls before you commit to a date.

Step 1: make the source ready to be replicated from

On the source, you need binary logging in a shape the target can consume:

SHOW VARIABLES WHERE Variable_name IN
  ('log_bin','binlog_format','binlog_row_image','gtid_mode',
   'enforce_gtid_consistency','binlog_expire_logs_seconds','server_id');

What you want: log_bin = ON, binlog_format = ROW, binlog_row_image = FULL, and GTIDs on (gtid_mode = ON, enforce_gtid_consistency = ON) if the source and target versions both support them, because GTID-based positioning removes an entire class of "which coordinate was that again" errors during cutover and rollback.

The setting that ruins more migrations than any other is binlog_expire_logs_seconds. If your seed takes eleven hours and you retain binlogs for eight, the target can never catch up and you start over. Raise retention to comfortably exceed your worst-case seed-plus-verify time — a week is a reasonable default during a migration — and check you have the disk for it. Note that this is also why the very first thing to check on a stalled migration is whether the required binlog file still exists on the source.

Also audit the schema for things the copy will trip on: tables with no primary key (row-based apply on the target degrades to full scans per row and lag becomes unbounded), DEFINER-bearing views, triggers and routines that reference accounts you will not recreate, and any remaining MyISAM tables. Fix those on the source, ahead of time, as ordinary work. They are cheaper before the clock is running.

Step 2: seed the target

The target must start from a consistent snapshot with a known binlog position or GTID set. Three options in ascending order of size:

  1. mysqldump --single-transaction --source-data=2 (--master-data on older versions). Simple, writes the coordinates into the dump header, fine up to a few dozen GB. Add --set-gtid-purged=ON when using GTIDs. Load time, not dump time, is what hurts: a single-threaded restore into the target is often the longest step of the whole project.
  2. mydumper/myloader. The same logical approach, parallelised. Usually several times faster to load, and the tool most of our mid-size migrations use.
  3. Percona XtraBackup into S3, restored by RDS. RDS for MySQL can create an instance directly from a physical backup in S3, which is by far the fastest path for large datasets. Aurora MySQL supports a similar restore-from-S3 flow. Physical restore is the right answer above roughly a terabyte, where logical load times stop being tolerable.

While the seed loads, keep the target's replication switched off, and consider relaxing durability on the target only (innodb_flush_log_at_trx_commit = 2, larger redo) to shorten the load. Put it back before the verification pass, not after cutover.

Step 3: run the target as a replica

On RDS for MySQL, replication from an external source is configured by stored procedure:

CALL mysql.rds_set_external_source (
  'source.internal.example.com', 3306,
  'repl_user', 'password',
  'mysql-bin.001234', 4, 0);
CALL mysql.rds_start_replication;

On Aurora MySQL the equivalents are mysql.rds_set_external_master / mysql.rds_set_external_source on the writer, or GTID-based variants where supported. Either way, from then on it is ordinary MySQL replication: watch SHOW REPLICA STATUS for Seconds_Behind_Source, Replica_SQL_Running, and the retrieved-versus-executed GTID sets.

Two things to get right here:

  • Network and credentials. The replication user needs REPLICATION SLAVE and REPLICATION CLIENT, reachable over a route that will still exist at 3am — VPN, Direct Connect, or peering, with a security group you have actually tested from the target subnet. Use TLS; the replication stream carries your data across whatever that path is.
  • Lag behaviour under load. If the target is smaller than the source, it will fall behind during batch jobs and never look ready. Size the target for the source's write rate during migration even if you intend to shrink it later. This is also where Aurora's storage model helps: applies that queue on RDS often keep up on Aurora.

Then let it run for days, not hours. A replica that has survived a full weekly cycle — the Monday morning peak, the nightly batch, the monthly report — has told you something a two-hour test cannot.

Step 4: verify before you believe

Run pt-table-checksum against source and target if the path allows it; it is the only mechanical answer to "is the data identical". Where the tool cannot run, checksum the tables that matter with explicit queries (row counts plus SUM(CRC32(...)) over business keys) and accept that you are sampling.

Beyond data equality, verify the things the platform changed underneath you:

  • Query plans. Dump your top 50 slow-log queries and EXPLAIN every one on the target. Optimizer statistics on a freshly loaded table are new, and a plan that flipped from a range scan to a full scan will find you in production otherwise. Run ANALYZE TABLE on the big tables after the seed.
  • Parameter drift. Diff your source my.cnf against the target parameter group, variable by variable. sql_mode, character_set_server, transaction_isolation, innodb_buffer_pool_size, max_connections and the timeout family are where defaults bite. Anything you cannot set on the target is a finding, not a footnote.
  • Accounts and grants. Users, hosts, authentication plugins, and any caching_sha2_password clients that need driver updates.
  • Everything that is not the database. Backups, point-in-time restore window, monitoring, alerting, and — the one most often forgotten — the replication out of this database into your analytics pipeline or CDC consumers, which will need new coordinates after cutover.

Step 5: the cutover

The write pause should be short and scripted. Our standard sequence, run from a checklist with a named owner per line:

  1. Freeze schema changes and batch jobs. Announce the window.
  2. Put the application into a brief read-only or maintenance state, or stop the writers.
  3. Wait for the target to reach the source's final GTID or binlog coordinate — zero lag, retrieved equals executed.
  4. Restore full durability on the target, and recheck parameters if you relaxed any.
  5. Set up replication back the other way, target to source, before you accept writes. This is the step that makes rollback real rather than aspirational.
  6. Repoint the application. Use a DNS CNAME or proxy endpoint with a low TTL that you changed to a low TTL the day before; a 3600-second TTL discovered at cutover time is how a 30-second pause becomes an hour.
  7. Release writes. Watch error rates, connection counts, replication back to the old source, and p99 latency for the first hour.

Seconds of write pause, not hours, is an ordinary outcome for this shape of migration. The long pole is the verification, which happens while production carries on untouched.

The rollback you have to test

Rollback is not "we still have the old server". It is: the old server is a replica of the new one, so writes taken after cutover exist in both places, and repointing the application back loses nothing. That reverse replication is why we insist on GTIDs where possible, and it is the thing to rehearse — in staging, end to end, with the actual DNS or proxy change — before the real date. Decide the abort criteria in advance (error rate above X, p99 above Y, replication broken), write them down, and give one named person the authority to call it. An untested rollback plan is a paragraph, not a plan.

Keep reverse replication running for at least a business cycle after cutover, then stop it deliberately and record the date. That is the moment the migration is actually over.

Where RDS Blue/Green Deployments fit

If you are already on RDS or Aurora and the move is a version upgrade rather than a platform change, Blue/Green Deployments do most of this for you: AWS builds a green environment as a managed replica, keeps it in sync, and switches over with the endpoints renamed for you, typically in under a minute. It is a good tool, and on managed-to-managed upgrades it is usually the right one.

Its limits are worth knowing before you plan around it. The green environment is read-only while it syncs; switchover blocks writes briefly and will refuse to proceed if lag or health checks fail; and rollback after switchover is not automatic — the blue environment remains, but it has not been receiving writes, so going back means losing what landed on green. For moves onto AWS from self-managed MySQL, Blue/Green does not apply at all, and you are doing the replication cutover above.

What about DMS

AWS DMS earns its place on heterogeneous migrations — Oracle or SQL Server to MySQL — where it does real translation work. For MySQL to MySQL it adds a moving part on top of binlog replication you could run yourself, along with its own failure modes around LOB handling, unsupported DDL during full load, and tables without primary keys. If your source and target are both MySQL, native replication is simpler to reason about and easier to debug at 3am. That is not a criticism of the service; it is a preference for the smallest number of components between two databases you already understand.

The short version

Seed from a consistent snapshot, replicate, verify against real traffic, cut over in seconds, and keep replication running backwards until you are sure. The work is in the preparation, which is exactly where you want it — the parts that happen during the window should be the parts you have already done twice.

If you have a date on the calendar and a migration plan you would like a second opinion on, send us the source topology, dataset size, and the target you have picked. Most of the risk in these projects is visible before anything is copied.