<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Prasan Bora</title>
    <description>The latest articles on DEV Community by Prasan Bora (@prasan_bora).</description>
    <link>https://dev.to/prasan_bora</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3732468%2Fce0114c3-a970-444c-abc5-d2a429d3c53c.jpeg</url>
      <title>DEV Community: Prasan Bora</title>
      <link>https://dev.to/prasan_bora</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/prasan_bora"/>
    <language>en</language>
    <item>
      <title>Upgrading PostgreSQL 13 to 17 on AWS RDS with Minimal Downtime</title>
      <dc:creator>Prasan Bora</dc:creator>
      <pubDate>Mon, 26 Jan 2026 08:48:20 +0000</pubDate>
      <link>https://dev.to/prasan_bora/upgrading-postgresql-13-to-17-on-aws-rds-with-minimal-downtime-29eo</link>
      <guid>https://dev.to/prasan_bora/upgrading-postgresql-13-to-17-on-aws-rds-with-minimal-downtime-29eo</guid>
      <description>&lt;p&gt;I recently upgraded a production PostgreSQL database from version 13.20 to 17.7 on AWS RDS. This post documents the entire journey - including the mistakes I made, the approaches I considered, and how I achieved a switchover with minimal downtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;My application runs on AWS with PostgreSQL 13.20 on RDS. PostgreSQL 13 reaches end of standard support in November 2025, and I wanted to get ahead of forced upgrades. More importantly, PostgreSQL 17 brings significant performance improvements and features I wanted to leverage.&lt;/p&gt;

&lt;p&gt;The challenge: my database serves a production application with active users. Traditional major version upgrades on RDS can take 10-30 minutes of downtime depending on database size - unacceptable for my use case.&lt;/p&gt;

&lt;p&gt;I needed to answer one question: &lt;strong&gt;How do I upgrade PostgreSQL by 4 major versions with minimal disruption?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Understanding the Upgrade Options
&lt;/h2&gt;

&lt;p&gt;Before diving into implementation, I evaluated three approaches:&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: In-Place Upgrade (Native RDS)
&lt;/h3&gt;

&lt;p&gt;The simplest approach - modify the &lt;code&gt;engine_version&lt;/code&gt; in Terraform and apply.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_instance"&lt;/span&gt; &lt;span class="s2"&gt;"postgres_rds"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;engine_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"17.7"&lt;/span&gt;  &lt;span class="c1"&gt;# Changed from 13.20&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: RDS performs a full &lt;code&gt;pg_upgrade&lt;/code&gt; which:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Takes the database offline during the entire upgrade&lt;/li&gt;
&lt;li&gt;Duration depends on database size (10-30+ minutes typically)&lt;/li&gt;
&lt;li&gt;No rollback if something goes wrong post-upgrade&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ruled this out immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Manual Blue-Green with Read Replica
&lt;/h3&gt;

&lt;p&gt;Create a read replica, upgrade it, then promote and switch DNS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL read replicas cannot be upgraded independently on RDS&lt;/li&gt;
&lt;li&gt;The replica must match the primary's major version&lt;/li&gt;
&lt;li&gt;This approach works for MySQL on RDS, not PostgreSQL&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Option 3: AWS Blue-Green Deployments (My Choice)
&lt;/h3&gt;

&lt;p&gt;AWS introduced Blue-Green Deployments for RDS in late 2022. This creates a synchronized staging environment (Green) that mirrors your production database (Blue), performs the upgrade on Green, then switches over with minimal downtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I chose this&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS handles replication automatically using logical replication&lt;/li&gt;
&lt;li&gt;Switchover is fast (typically under 1 minute)&lt;/li&gt;
&lt;li&gt;Built-in rollback capability before switchover&lt;/li&gt;
&lt;li&gt;Supported for PostgreSQL major version upgrades&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Mistake: RDS Proxy Doesn't Mix with Blue-Green
&lt;/h2&gt;

&lt;p&gt;My initial plan included RDS Proxy to minimize connection disruption during switchover. You might be wondering - why would I need a proxy for a database upgrade? I'll explain the reasoning later, but for now, just know that I expected it to help during the critical switchover moment.&lt;/p&gt;

&lt;p&gt;I added RDS Proxy to my Terraform configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_proxy"&lt;/span&gt; &lt;span class="s2"&gt;"rds_proxy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"staging-proxy"&lt;/span&gt;
  &lt;span class="nx"&gt;engine_family&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"POSTGRESQL"&lt;/span&gt;
  &lt;span class="nx"&gt;require_tls&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_security_group_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proxy_sg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_subnet_ids&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_subnets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;

  &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;auth_scheme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SECRETS"&lt;/span&gt;
    &lt;span class="nx"&gt;secret_arn&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proxy_credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
    &lt;span class="nx"&gt;iam_auth&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DISABLED"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_proxy_default_target_group"&lt;/span&gt; &lt;span class="s2"&gt;"proxy_target"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;db_proxy_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rds_proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_proxy_target"&lt;/span&gt; &lt;span class="s2"&gt;"proxy_target"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;db_proxy_name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rds_proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;target_group_name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_proxy_default_target_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proxy_target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;db_instance_identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;postgres_rds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identifier&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I attempted to create the Blue-Green deployment, AWS returned this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Databases using RDS Proxy are not currently supported for Blue Green Deployments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a hard limitation. You cannot create a Blue-Green deployment for any RDS instance that has an RDS Proxy attached to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned&lt;/strong&gt;: Always verify service compatibility before architecting a solution. I assumed these two features would work together because both aim to reduce downtime. They don't.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites for Blue-Green Deployments
&lt;/h2&gt;

&lt;p&gt;Before you can create a Blue-Green deployment for PostgreSQL, your source database needs specific configuration:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Automated Backups Enabled
&lt;/h3&gt;

&lt;p&gt;Blue-Green uses logical replication which requires point-in-time recovery capability.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_instance"&lt;/span&gt; &lt;span class="s2"&gt;"postgres_rds"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backup_retention_period&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;          &lt;span class="c1"&gt;# Must be &amp;gt; 0&lt;/span&gt;
  &lt;span class="nx"&gt;backup_window&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"03:00-04:00"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Logical Replication Enabled
&lt;/h3&gt;

&lt;p&gt;The source database needs a parameter group with logical replication enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_parameter_group"&lt;/span&gt; &lt;span class="s2"&gt;"postgres13_blue_green"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;family&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"postgres13"&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"postgres13-blue-green"&lt;/span&gt;

  &lt;span class="nx"&gt;parameter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"rds.logical_replication"&lt;/span&gt;
    &lt;span class="nx"&gt;value&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;
    &lt;span class="nx"&gt;apply_method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pending-reboot"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;parameter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"max_replication_slots"&lt;/span&gt;
    &lt;span class="nx"&gt;value&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10"&lt;/span&gt;
    &lt;span class="nx"&gt;apply_method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pending-reboot"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;parameter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"max_wal_senders"&lt;/span&gt;
    &lt;span class="nx"&gt;value&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10"&lt;/span&gt;
    &lt;span class="nx"&gt;apply_method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pending-reboot"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: Enabling &lt;code&gt;rds.logical_replication&lt;/code&gt; requires a database reboot. Plan for this before your upgrade window.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Valid Upgrade Path
&lt;/h3&gt;

&lt;p&gt;Not all version combinations are valid. Check available upgrade paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws rds describe-db-engine-versions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--engine&lt;/span&gt; postgres &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--engine-version&lt;/span&gt; 13.20 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'DBEngineVersions[0].ValidUpgradeTarget[?MajorEngineVersion==`17`]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-southeast-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I initially targeted 17.2 and got this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cannot find upgrade path from 13.20 to 17.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The valid target for me was 17.7.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Upgrade Process
&lt;/h2&gt;

&lt;p&gt;Here's the step-by-step process I followed:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Detach RDS Proxy (if attached)
&lt;/h3&gt;

&lt;p&gt;Since I had already deployed RDS Proxy, I had to detach it first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Remove the proxy target&lt;/span&gt;
aws rds deregister-db-proxy-targets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--db-proxy-name&lt;/span&gt; staging-proxy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--db-instance-identifiers&lt;/span&gt; dev-postgres-rds &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-southeast-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Create the Blue-Green Deployment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws rds create-blue-green-deployment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--blue-green-deployment-name&lt;/span&gt; &lt;span class="s2"&gt;"pg13-to-pg17-upgrade"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:rds:ap-southeast-2:123456789:db:dev-postgres-rds"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-engine-version&lt;/span&gt; &lt;span class="s2"&gt;"17.7"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-db-parameter-group-name&lt;/span&gt; &lt;span class="s2"&gt;"default.postgres17"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-southeast-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kicks off the following process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS creates a new RDS instance (Green) with PostgreSQL 17.7&lt;/li&gt;
&lt;li&gt;Takes a snapshot of your Blue database&lt;/li&gt;
&lt;li&gt;Restores it to the Green instance&lt;/li&gt;
&lt;li&gt;Sets up logical replication from Blue to Green&lt;/li&gt;
&lt;li&gt;Syncs all changes&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3: Wait for Provisioning
&lt;/h3&gt;

&lt;p&gt;The Green environment takes time to provision. Monitor the status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws rds describe-blue-green-deployments &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--blue-green-deployment-identifier&lt;/span&gt; &lt;span class="s2"&gt;"pg13-to-pg17-upgrade"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'BlueGreenDeployments[0].Status'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-southeast-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Status progression:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PROVISIONING&lt;/code&gt; - Creating the Green environment&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AVAILABLE&lt;/code&gt; - Ready for switchover&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For my 5GB database, this took approximately 35 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Verify Green Environment
&lt;/h3&gt;

&lt;p&gt;Before switching, verify the Green instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check the Green instance details&lt;/span&gt;
aws rds describe-blue-green-deployments &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--blue-green-deployment-identifier&lt;/span&gt; &lt;span class="s2"&gt;"pg13-to-pg17-upgrade"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'BlueGreenDeployments[0].SwitchoverDetails'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-southeast-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Engine version shows 17.7&lt;/li&gt;
&lt;li&gt;Replication lag is minimal&lt;/li&gt;
&lt;li&gt;Status is "AVAILABLE"&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 5: Execute Switchover
&lt;/h3&gt;

&lt;p&gt;This is the critical moment. The switchover:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stops writes to the Blue database&lt;/li&gt;
&lt;li&gt;Waits for replication to catch up&lt;/li&gt;
&lt;li&gt;Promotes Green to primary&lt;/li&gt;
&lt;li&gt;Renames instances (Blue becomes &lt;code&gt;-old&lt;/code&gt;, Green takes the original name)&lt;/li&gt;
&lt;li&gt;Updates the endpoint
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws rds switchover-blue-green-deployment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--blue-green-deployment-identifier&lt;/span&gt; &lt;span class="s2"&gt;"pg13-to-pg17-upgrade"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--switchover-timeout&lt;/span&gt; 300 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-southeast-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;My measured downtime: 20 seconds&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The switchover started at 16:26:42 and completed at 16:27:02 (UTC).&lt;/p&gt;




&lt;h2&gt;
  
  
  What Happens During Those 20 Seconds?
&lt;/h2&gt;

&lt;p&gt;This is the part I promised to explain earlier - why I wanted RDS Proxy in the first place.&lt;/p&gt;

&lt;p&gt;During the switchover window, your database is essentially unreachable. Any write operation attempted during this time will &lt;strong&gt;fail&lt;/strong&gt; - not queue, not wait, just fail. Your application will receive errors like &lt;code&gt;connection refused&lt;/code&gt; or &lt;code&gt;connection reset&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is "minimal downtime," not "zero downtime." The difference matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I Wanted RDS Proxy
&lt;/h3&gt;

&lt;p&gt;RDS Proxy sits between your application and database, maintaining connection pools. The theory was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Proxy holds active connections during the switchover&lt;/li&gt;
&lt;li&gt;Buffers requests briefly while the endpoint changes&lt;/li&gt;
&lt;li&gt;Redirects connections to the new (Green) instance seamlessly&lt;/li&gt;
&lt;li&gt;Writes would wait instead of fail outright&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This would have turned that 20-second failure window into a 20-second "slow response" window - much more graceful.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Unfortunate Reality
&lt;/h3&gt;

&lt;p&gt;AWS doesn't allow this. Blue-Green deployments and RDS Proxy are mutually exclusive. You must choose:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blue-Green (no proxy)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Downtime: ~20 seconds&lt;/li&gt;
&lt;li&gt;Write behavior: writes fail during switchover, retry logic required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;RDS Proxy (no Blue-Green)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Downtime: 10–30 minutes (in-place upgrade)&lt;/li&gt;
&lt;li&gt;Write behavior: connections handled gracefully during upgrade&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I chose Blue-Green because 20 seconds of failed writes is better than 30 minutes of total unavailability. But your application needs to handle those transient failures - implement retry logic or return appropriate errors to users.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 6: Cleanup
&lt;/h3&gt;

&lt;p&gt;After verifying the upgrade:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Delete the old instance&lt;/span&gt;
aws rds delete-db-instance &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--db-instance-identifier&lt;/span&gt; dev-postgres-rds-old1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--skip-final-snapshot&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-southeast-2

&lt;span class="c"&gt;# Delete the Blue-Green deployment&lt;/span&gt;
aws rds delete-blue-green-deployment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--blue-green-deployment-identifier&lt;/span&gt; &lt;span class="s2"&gt;"pg13-to-pg17-upgrade"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-southeast-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What About Terraform State?
&lt;/h2&gt;

&lt;p&gt;If you manage your RDS instance with Terraform, the Blue-Green switchover creates a state mismatch. The instance identifier remains the same, but AWS has essentially replaced the instance.&lt;/p&gt;

&lt;p&gt;After switchover, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll likely see Terraform wanting to modify parameters to match your configuration. Review the plan carefully - most changes should be no-ops or minor parameter adjustments.&lt;/p&gt;

&lt;p&gt;I also cleaned up my Terraform config post-upgrade:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Removed the postgres13 parameter group (no longer needed)&lt;/li&gt;
&lt;li&gt;Updated &lt;code&gt;engine_version&lt;/code&gt; to "17.7"&lt;/li&gt;
&lt;li&gt;Removed Blue-Green specific configuration
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_instance"&lt;/span&gt; &lt;span class="s2"&gt;"postgres_rds"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;identifier&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dev-postgres-rds"&lt;/span&gt;
  &lt;span class="nx"&gt;engine&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"postgres"&lt;/span&gt;
  &lt;span class="nx"&gt;engine_version&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"17.7"&lt;/span&gt;
  &lt;span class="nx"&gt;instance_class&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"db.t3.micro"&lt;/span&gt;
  &lt;span class="nx"&gt;allocated_storage&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;

  &lt;span class="c1"&gt;# Backups - good practice to keep enabled&lt;/span&gt;
  &lt;span class="nx"&gt;backup_retention_period&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
  &lt;span class="nx"&gt;backup_window&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"03:00-04:00"&lt;/span&gt;

  &lt;span class="c1"&gt;# ... rest of config&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Trade-offs and Limitations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Blue-Green Deployments Handle Well
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Major version upgrades with minimal downtime&lt;/li&gt;
&lt;li&gt;Safe rollback option (before switchover)&lt;/li&gt;
&lt;li&gt;Automatic data synchronization&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What They Don't Handle
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Databases with RDS Proxy attached&lt;/li&gt;
&lt;li&gt;Multi-AZ DB clusters (as of early 2024)&lt;/li&gt;
&lt;li&gt;Databases with more than 100 databases inside the instance&lt;/li&gt;
&lt;li&gt;Cross-region scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Hidden Costs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You pay for two RDS instances during the Blue-Green provisioning period&lt;/li&gt;
&lt;li&gt;My upgrade window cost approximately 35 minutes of double billing&lt;/li&gt;
&lt;li&gt;For larger databases, this could be significant&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verify service compatibility before architecting&lt;/strong&gt; - RDS Proxy and Blue-Green Deployments don't work together. I wasted time setting up proxy infrastructure I had to tear down.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check valid upgrade paths early&lt;/strong&gt; - Not every version combination is valid. &lt;code&gt;aws rds describe-db-engine-versions&lt;/code&gt; is your friend.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Logical replication requires a reboot&lt;/strong&gt; - Enabling &lt;code&gt;rds.logical_replication&lt;/code&gt; needs a database restart. Factor this into your planning.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Minimal downtime is achievable&lt;/strong&gt; - For a 5GB database, the actual switchover was remarkably fast. Your application should handle brief connection interruptions gracefully.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Blue-Green works via CLI better than Terraform&lt;/strong&gt; - The Terraform &lt;code&gt;blue_green_update&lt;/code&gt; block exists but using AWS CLI gives you more control over timing and verification steps.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clean up immediately&lt;/strong&gt; - The old instance keeps running and billing you. Delete it promptly after verification.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AWS Blue-Green Deployments are the best option for PostgreSQL major version upgrades with minimal downtime&lt;/li&gt;
&lt;li&gt;RDS Proxy is incompatible with Blue-Green Deployments - choose one approach&lt;/li&gt;
&lt;li&gt;Expect ~30-45 minutes of provisioning time, but only seconds of actual downtime&lt;/li&gt;
&lt;li&gt;Always verify upgrade paths and prerequisites before starting&lt;/li&gt;
&lt;li&gt;Have a rollback plan, even though I didn't need mine&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  References I Followed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/blue-green-deployments.html" rel="noopener noreferrer"&gt;AWS RDS Blue-Green Deployments Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/blue-green-deployments-switching.html" rel="noopener noreferrer"&gt;Switching Over a Blue-Green Deployment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/blue-green-deployments.html#blue-green-deployments-limitations" rel="noopener noreferrer"&gt;Blue-Green Deployment Limitations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html" rel="noopener noreferrer"&gt;RDS Proxy Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_UpgradeDBInstance.PostgreSQL.html" rel="noopener noreferrer"&gt;Upgrading PostgreSQL DB Engine Versions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This post documents a real production upgrade performed in January 2026. Your mileage may vary based on database size, AWS region, and specific configuration.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you're planning a similar upgrade, feel free to ask questions in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>aws</category>
      <category>devops</category>
      <category>database</category>
    </item>
  </channel>
</rss>
