1import{j as e,f as r,S as a,_ as t}from"./index-BTLZfiYp.js";import{A as s,U as o}from"./user-CB8tciwA.js";import{C as i}from"./clock-xuYHtJnv.js";const n="/blog/blog-legacy-rds-migration.png",u=()=>e.jsxs(r,{children:[e.jsx(a,{title:"Migrating off Legacy RDS: Upgrade vs Replatform | Morfless",description:"When to upgrade Amazon RDS in place vs replatform to Aurora or Aurora Serverless v2 , Extended Support costs, pg_upgrade risks and a logical replication migration blueprint.",canonical:"/blog/migrating-off-legacy-rds",type:"article",article:{author:"Ian Corbally",publishedTime:"2026-07-21"}}),e.jsx("article",{className:"pt-10 pb-20 lg:pt-16 lg:pb-28",children:e.jsx("div",{className:"container mx-auto px-4 lg:px-8 max-w-3xl",children:e.jsxs("div",{children:[e.jsxs(t,{to:"/blog",className:"inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-primary transition-colors mb-8",children:[e.jsx(s,{size:16})," Back to blog"]}),e.jsx("div",{className:"mb-6",children:e.jsx("span",{className:"text-xs font-medium px-3 py-1 rounded-full bg-accent/10 text-accent",children:"AWS Optimisation"})}),e.jsx("h1",{className:"font-display text-3xl md:text-4xl lg:text-5xl font-bold text-foreground mb-6 leading-tight",children:"Migrating off Legacy RDS: When to Upgrade vs. Replatforming to Serverless or Aurora"}),e.jsxs("div",{className:"flex items-center gap-4 text-sm text-muted-foreground mb-8",children:[e.jsxs("span",{className:"flex items-center gap-1.5",children:[e.jsx(o,{size:14})," Ian Corbally"]}),e.jsxs("span",{className:"flex items-center gap-1.5",children:[e.jsx(i,{size:14})," 9 min read"]})]}),e.jsx("img",{src:n,alt:"Migrating off legacy Amazon RDS to Aurora or Aurora Serverless v2",width:1200,height:630,className:"w-full rounded-2xl mb-10"}),e.jsxs("div",{className:"prose prose-invert prose-lg max-w-none space-y-6 text-muted-foreground leading-relaxed",children:[e.jsx("p",{children:"Database lifecycle management is one of the more predictable friction points in infrastructure management. Major version end-of-life schedules force infrastructure teams into a recurring cycle of testing, tuning, and deployment. When an engine version reaches its official end of support lifecycle, enterprise groups face a sharp financial and technical transition."}),e.jsx("p",{children:"Faced with Amazon Web Services enforcing engine deprecation timelines, team leadership often views in-place major version updates as a default, low-risk fix. In practice, however, upgrading a production engine in-place requires almost identical testing overhead to standard application migration efforts. This transition point represents a strategic window to evaluate your platform choice. Instead of simply patching, we must decide whether to execute an RDS Postgres upgrade migration plan or move workloads onto modern serverless or clustered engines."}),e.jsx("h2",{className:"font-display text-2xl md:text-3xl font-bold text-foreground mt-10 mb-4",children:"The Junction Point: Upgrade in Place, or Replatform?"}),e.jsxs("p",{children:["Continuing to run older engines on Amazon RDS carries a direct RDS Extended Support pricing impact. Once an engine version exits the standard support tier, AWS applies an active surcharge on every active vCPU-hour. In Years 1 and 2, AWS charges $0.10 per vCPU-hour for Extended Support. On an on-demand Multi-AZ ",e.jsx("code",{children:"db.r6g.xlarge"})," instance (8 vCPUs total, priced at ~$1.426/hour in ",e.jsx("code",{children:"us-east-1"}),"), this surcharge is $0.80/hour â representing a ~56% increase on compute. The surcharge doubles in Year 3, when the rate scales up to $0.20 per vCPU-hour ($1.60/hour extra). SRE teams must model these stepped phases carefully to accurately calculate the financial impact of database maintenance delay."]}),e.jsxs("p",{children:["How do teams avoid RDS Extended Support charges effectively? The obvious path is a major version database upgrade. However, executing a standard upgrade can be complex. In-place major upgrades run a severe risk of introducing query plan regressions. The underlying mechanism for this post-upgrade degradation is that ",e.jsx("code",{children:"pg_upgrade"})," completely discards optimizer statistics stored in ",e.jsx("code",{children:"pg_statistic"}),". Post-upgrade, the cost-based optimizer makes decisions using flat defaults, causing massive, systemic performance regressions unless we immediately run a staged analyze (such as executing ",e.jsx("code",{children:"vacuumdb --all --analyze-in-stages"})," or manual low-threshold ",e.jsx("code",{children:"ANALYZE"})," routines) to rebuild the stats catalog before routing production load."]}),e.jsxs("p",{children:["Furthermore, high-write production setups require tuning autovacuum settings before attempting structural catalog migrations. Increasing concurrency helps reduce transaction ID wraparound risk and flushes dead tuples before executing major catalog upgrades. However, increasing ",e.jsx("code",{children:"autovacuum_max_workers"})," to 8 without scaling the global ",e.jsx("code",{children:"autovacuum_vacuum_cost_limit"})," is a dangerous anti-pattern. Under default behavior, PostgreSQL shares the cost limit (which defaults to 200 in older versions) among all active workers. Spreading this static budget over 8 workers instead of 3 means each worker gets throttled faster, perversely slowing down cleanup times and exacerbating wraparound risks. To prevent this, we must scale the performance budget alongside worker threads. We must also observe CPU capacity on active production hosts closely when scaling these workers to prevent resource starvation. If you want a detailed look at the mechanisms, AWS provides a clear ",e.jsx("a",{href:"https://aws.amazon.com/blogs/database/part-1-upgrade-your-amazon-rds-for-postgresql-database-comparing-upgrade-approaches/",target:"_blank",rel:"noopener noreferrer",className:"text-primary hover:underline",children:"breakdown of RDS for PostgreSQL upgrade methods"}
1)," that contrasts standard upgrade approaches."]}),e.jsx("pre",{className:"bg-card border border-border rounded-lg p-4 overflow-x-auto text-sm",children:e.jsx("code",{children:`# Important baseline parameters to check before an in-place upgrade 2shared_preload_libraries = 'pg_stat_statements,pg_hint_plan' 3# Scale the shared cost budget and reduce delay so workers can deploy their budget efficiently: 4autovacuum_max_workers = 8 5autovacuum_vacuum_cost_limit = 2000 6autovacuum_vacuum_cost_delay = 2`})}),e.jsx("p",{children:"When optimising operations, a modern database modernisation strategy for enterprise architects requires evaluating a fundamental question: is the engineering effort to test an in-place major version upgrade worth the output? If we must allocate engineering sprints to rebuild test schemas, audit queries, and run performance benchmarks, we should analyse if that effort is better spent moving to a modern database layer."}),e.jsxs("p",{children:["According to the standard ",e.jsx("a",{href:"https://docs.aws.amazon.com/prescriptive-guidance/latest/large-migration-guide/migration-strategies.html",target:"_blank",rel:"noopener noreferrer",className:"text-primary hover:underline",children:"AWS migration strategies framework"}),", the choice between a simple replatforming and more invasive modernisation projects depends on your long-term architecture design. Replatforming to modern engine alternatives like Amazon Aurora bypasses several administrative bottlenecks found in standard RDS instances. To plan this transition correctly, you can read more about planning your timeline around the official ",e.jsx("a",{href:"https://aws.amazon.com/blogs/database/upgrade-strategies-for-amazon-aurora-postgresql-and-amazon-rds-for-postgresql-12/",target:"_blank",rel:"noopener noreferrer",className:"text-primary hover:underline",children:"AWS RDS Extended Support and upgrade strategies"}),"."]}),e.jsx("p",{children:"Before deciding between an RDS PostgreSQL major version upgrade vs Aurora migration, we must analyse your workload profile and evaluate whether the targets will perform efficiently under the dynamic scaling profiles of modern platforms."}),e.jsx("h2",{className:"font-display text-2xl md:text-3xl font-bold text-foreground mt-10 mb-4",children:"Evaluating Amazon Aurora Serverless as a Long-term Haven"}),e.jsx("p",{children:"When reviewing the direct Amazon Aurora vs RDS cost equation, raw architecture patterns dictate the cost curve. Traditional RDS provisioning forces teams to size hardware setups for peak utilisation levels, which typically leaves a considerable amount of unused headroom during lower traffic windows."}),e.jsxs("p",{children:["Is Aurora Serverless cheaper than provisioned RDS? The answer is tied to your workload profile. Aurora Serverless v2 uses Aurora Capacity Units (ACUs) to scale compute, where 1 ACU provides approximately 2 GB of memory alongside proportional compute power and network capacity. If we run a cluster in ",e.jsx("code",{children:"us-east-1"}),", an ACU costs roughly $0.12 per hour."]}),e.jsx("p",{children:"Let us look at a practical cost comparison:"}),e.jsxs("ul",{className:"list-disc pl-6 space-y-2",children:[e.jsxs("li",{children:[e.jsxs("strong",{className:"text-foreground",children:["Provisioned RDS (",e.jsx("code",{children:"db.r6g.xlarge"}),"):"]})," Provides 4 vCPUs and 32 GB of RAM for roughly $0.713 per hour (approx. $520 per month, on-demand)."]}),e.jsxs("li",{children:[e.jsx("strong",{className:"text-foreground",children:"Aurora Serverless v2 equivalence:"})," Providing an equivalent 32 GB of RAM requires configuring 16 ACUs. Running at a constant 16 ACUs costs $1.92 per hour (approx. $1,401 per month)."]})]}),e.jsx("p",{children:"This comparison shows that running a steady, flat-line production database on Aurora Serverless v2 24/7 can be considerably more expensive than using standard provisioned resources. Workloads that experience unpredictable spikes, batch jobs, or significant periods of inactivity are ideal candidates."}),e.jsx("pre",{className:"bg-card border border-border rounded-lg p-4 overflow-x-auto text-sm",children:e.jsx("code",{children:`Compute Demand (ACU) 7 | 816| _/ 912| _/ _/ 10 8| _/ _/ 11 4|_/ _/ 12 0.5+---------------+----------> Time (Hours) 13 0 24`})}),e.jsx("p",{children:"When is Aurora Serverless v2 worth it? It is highly effective in dev, staging, or test environments that run only during business hours. A serverless staging host configured to scale between 0.5 ACU and 16 ACUs can drop to 0.5 ACU ($0.06 per hour) during idle times. This can lower the idle compute bill by 90% while still maintaining the capacity to handle heavy testing cycles on demand. It is also an excellent option for production services with highly variable, unpredictable spikes where over-provisioning for worst-case peaks would otherwise sit completely idle."}),e.jsx("p",{children:"If you want to migrate RDS to Aurora Serverless, this transition can help decouple storage from compute layers, eliminate manual resizing operations, and protect your workloads against unexpected traffic spikes."}),e.jsx("h2",{className:"font-display text-2xl md:text-3xl font-bold text-foreground mt-10 mb-4",children:"Migration Paths: DMS and Snapshot Restores Made Simple"}),e.jsx("p",{children:"Once you choose to transition to a modern database model, managing your replatforming RDS to Aurora downtime risk becomes a key priority. For staging setups or non-production backends that can tolerate maintenance windows, you can take a snapshot of your source database, restore it directly as an Aurora cluster, and execute target engine upgrades on the newly cloned database. This route provides a straightforward, low-risk upgrade path."}),e.jsxs("p",{children:["For production engines that need to remain highly available, we can use logical replication. Rather than setting up complex AWS Database Migration Service (DMS) tasks (which can introduce syntax translation overhead), utilising native database engine tools is often the cleanest pathway. This is demonstrated in ",e.jsx("a",{href:"https://www.revenuecat.com/blog/engineering/rds-to-aurora-migration-zero-downtime",target:"_blank",rel:"noopener noreferrer",className:"text-primary hover:underline",children:"RevenueCat's zero-downtime migration case study"}
13),", where the infrastructure engineering team bypassed DMS entirely to shift their primary Postgres database to Aurora with minimal impact under heavy traffic pipelines."]}),e.jsxs("p",{children:["For engineers running specialised Postgres workloads, follow this ",e.jsx("a",{href:"https://dev.to/garrett_yan/zero-downtime-rds-to-aurora-serverless-v2-migration-a-step-by-step-guide-202d",target:"_blank",rel:"noopener noreferrer",className:"text-primary hover:underline",children:"step-by-step migration guide to Aurora Serverless v2"}),", which outlines standard replication setups."]}),e.jsx("p",{children:"Below is a streamlined configuration blueprint to replicate a legacy RDS PostgreSQL source directly to an Aurora cluster utilising logical replication."}),e.jsx("h3",{className:"font-display text-xl md:text-2xl font-bold text-foreground mt-8 mb-3",children:"Step 1: Configure parameter groups on the legacy RDS source"}),e.jsx("p",{children:"Modify the custom DB parameter group for your active source database to support replication and log streaming:"}),e.jsx("pre",{className:"bg-card border border-border rounded-lg p-4 overflow-x-auto text-sm",children:e.jsx("code",{children:`# Ensure these parameters are active on the legacy database parameter group 14rds.logical_replication = 1 15wal_sender_timeout = 60000 16max_wal_senders = 10 17max_replication_slots = 10`})}),e.jsx("p",{children:e.jsx("em",{children:"Note: enabling these changes requires rebooting your source RDS database."})}),e.jsx("h3",{className:"font-display text-xl md:text-2xl font-bold text-foreground mt-8 mb-3",children:"Step 2: Establish the replication publication and logical replication slot"}),e.jsxs("p",{children:[e.jsx("strong",{className:"text-foreground",children:"Critical operational hazard:"})," calling ",e.jsx("code",{children:"pg_create_logical_replication_slot"})," on a high-throughput production legacy database poses a major operational hazard. If the target subscription halts due to schema errors, sequence replication errors, or network issues, the source database will retain Write-Ahead Logs (WAL) indefinitely to service the active replication slot. This can rapidly consume all available storage on the legacy RDS instance, triggering a critical production outage (disk full). SRE teams must closely monitor the logical replication slot lag and establish fast-acting storage volume alerts on the source instance before creating the subscription."]}),e.jsxs("p",{children:["Crucially, all tables targetable by the replication stream must have a Primary Key or a valid ",e.jsx("code",{children:"REPLICA IDENTITY"})," configured (such as ",e.jsx("code",{children:"REPLICA IDENTITY FULL"}),"). If any tables lacking primary keys receive ",e.jsx("code",{children:"UPDATE"})," or ",e.jsx("code",{children:"DELETE"})," calls on the source database, the subscription engine will halt immediately with replication failures, blocking the synchronisation pipeline."]}),e.jsxs("p",{children:["To identify tables lacking a primary key or configured replica identity, query ",e.jsx("code",{children:"pg_class"})," for its ",e.jsx("code",{children:"relreplident"})," configuration before creating the publication:"]}),e.jsx("pre",{className:"bg-card border border-border rounded-lg p-4 overflow-x-auto text-sm",children:e.jsx("code",{children:`-- Query pg_class to identify tables lacking a valid replica identity definition 18SELECT nspname AS schema_name, relname AS table_name, relreplident 19FROM pg_class c 20JOIN pg_namespace n ON n.oid = c.relnamespace 21WHERE c.relkind = 'r' 22 AND c.relreplident IN ('d', 'n') 23 AND NOT EXISTS ( 24 SELECT 1 FROM pg_index i 25 WHERE i.indrelid = c.oid AND i.indisprimary 26 );`})}),e.jsx("p",{children:"If any tables are returned, update their replica identity configuration on the source host:"}),e.jsx("pre",{className:"bg-card border border-border rounded-lg p-4 overflow-x-auto text-sm",children:e.jsx("code",{children:"ALTER TABLE table_name REPLICA IDENTITY FULL;"})}),e.jsx("p",{children:"Once completed, establish a publication on your source database to specify which tables will stream writes down the wire. You must also create the replication slot on the source manually so the target subscription can bind to it:"}),e.jsx("pre",{className:"bg-card border border-border rounded-lg p-4 overflow-x-auto text-sm",children:e.jsx("code",{children:`-- Connect to legacy source database as superuser or rds_superuser 27CREATE PUBLICATION app_migration_pub FOR ALL TABLES; 28 29-- Create the logical replication slot required by the target subscription 30SELECT pg_create_logical_replication_slot('app_migration_slot', 'pgoutput');`})}),e.jsx("h3",{className:"font-display text-xl md:text-2xl font-bold text-foreground mt-8 mb-3",children:"Step 3: Configure target schema and subscription on Aurora or Serverless v2"}),e.jsxs("p",{children:[e.jsx("strong",{className:"text-foreground",children:"Critical schema restriction:"})," native PostgreSQL logical replication does not replicate Data Definition Language (DDL) operations. We must enforce a strict schema freeze throughout the entire replication window. Any index modifications, migrations, or database structural changes made on the source while replication is active will crash the replication link or cause severe data desynchronisation. Ensure the schema is fully pre-applied on the target before executing the subscription step."]}),e.jsx("p",{children:"Before executing the subscription handshake, verify that the source database security group is explicitly configured to receive T
30CP port 5432 ingress from the destination Aurora VPC subnet or target Security Group. This ingress rule is mandatory for logical replication stream connectivity."}),e.jsxs("p",{children:["After carrying over your database schema definitions using tools like ",e.jsx("code",{children:"pg_dump --schema-only"}),", create the matching subscription on the target Aurora instance:"]}),e.jsx("pre",{className:"bg-card border border-border rounded-lg p-4 overflow-x-auto text-sm",children:e.jsx("code",{children:`-- Connect to new Aurora target instance 31CREATE SUBSCRIPTION app_migration_sub 32CONNECTION 'host=legacy-rds.c123456789.us-east-1.rds.amazonaws.com port=5432 dbname=prod_database user=replicator password=secure_password' 33PUBLICATION app_migration_pub 34WITH (copy_data = true, create_slot = false, slot_name = 'app_migration_slot');`})}),e.jsxs("p",{children:[e.jsx("strong",{className:"text-foreground",children:"Warning regarding sequence replication:"})," native PostgreSQL logical replication does NOT replicate database sequences. If you cut over write traffic to the new Aurora setup without manually synchronising sequences (calculating the current ",e.jsx("code",{children:"MAX"})," on the source and executing ",e.jsx("code",{children:"SELECT setval..."})," on the destination), subsequent database writes will fail with duplicate-key constraint violations on primary key columns."]}),e.jsx("p",{children:"To prevent sequence failures, run the following dynamic query on the source database immediately before performance cutover to generate the synchronisation commands, then execute the output statement block on the target database node:"}),e.jsx("pre",{className:"bg-card border border-border rounded-lg p-4 overflow-x-auto text-sm",children:e.jsx("code",{children:`-- Run on the source to dynamically generate SQL commands syncing sequence values 35SELECT 'SELECT setval(''' || quote_ident(schemaname) || '.' || quote_ident(sequencename) || ''', ' || last_value || ', true);' AS sync_statement 36FROM pg_sequences;`})}),e.jsx("p",{children:"Using this setup, the target engine will backfill existing data before continuously applying changes. Once the replication lag drops to zero and sequence values are verified, write operations can be shifted to the new Aurora setup with minimal interruption."}),e.jsx("h2",{className:"font-display text-2xl md:text-3xl font-bold text-foreground mt-10 mb-4",children:"Summary: Choosing Your Modernisation Roadmap"}),e.jsx("p",{children:"Upgrading legacy databases does not have to be a routine chore that simply patches over technical debt. When faced with the overhead of an RDS Extended Support deadline, teams should evaluate whether it is time to move to a more modern database architecture:"}),e.jsxs("ol",{className:"list-decimal pl-6 space-y-2",children:[e.jsxs("li",{children:[e.jsx("strong",{className:"text-foreground",children:"Assess the surcharge impact:"})," use your billing analytics to identify instances heading for Extended Support, and calculate the actual cost of doing nothing."]}),e.jsxs("li",{children:[e.jsx("strong",{className:"text-foreground",children:"Analyse your performance layout:"})," reserve Serverless v2 targets for workloads with variable traffic or environments that sit idle. Rely on provisioned instance classes with Reserved Instances for database workloads that have flat, predictable utilisation profiles."]}),e.jsxs("li",{children:[e.jsx("strong",{className:"text-foreground",children:"Optimise your migration strategy:"})," keep your migration simple. Use native engine tools and logical replication to migrate your data safely and minimise downtime risks."]})]}),e.jsxs("p",{children:[e.jsx("a",{href:"https://app.morfless.com/register",target:"_blank",rel:"noopener noreferrer",className:"text-primary hover:underline",children:"Get Morfless free"})," ","to see where your RDS estate is heading toward Extended Support surcharges and model the replatforming payoff."]})]})]})})})]});export{u as default};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.