Prerequisites #
Install AWS CLI #
# Update packages
sudo apt update
# Unstall zip tool
sudo apt install unzip -y
# Download AWS CLI zip file
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
# Unzip
unzip awscliv2.zip
# Install
sudo ./aws/install
# Verify installation / check version
/usr/local/bin/aws --version
Configure AWS CLI #
# Start AWS CLI configuration
aws configure
Install Terraform #
# Install the HashiCorp GPG key
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null
# Verify the GPG key fingerprint
gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint
# Add the official HashiCorp repository
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
# Install Terraform
sudo apt update && sudo apt-get install terraform
# Verify installation / check version
terraform version
List Available Aurora Postgres Versions #
# List available PostgreSQL verions
aws rds describe-db-engine-versions \
--engine aurora-postgresql \
--query "DBEngineVersions[].EngineVersion" \
--output table
# Shell output:
--------------------------
|DescribeDBEngineVersions|
+------------------------+
| 11.9 |
| 11.21 |
| 12.9 |
| 12.15 |
| 12.16 |
| 12.17 |
| 12.18 |
| 12.19 |
| 12.20 |
| 12.22 |
| 13.9 |
| 13.11 |
| 13.12 |
| 13.13 |
| 13.14 |
| 13.15 |
| 13.16 |
| 13.18 |
| 14.6 |
| 14.8 |
| 14.9 |
| 14.10 |
| 14.11 |
| 14.12 |
| 14.13 |
| 14.15 |
| 15.3 |
| 15.4 |
| 15.5 |
| 15.6 |
| 15.7 |
| 15.8 |
| 15.10 |
| 16.1 |
| 16.2 |
| 16.3 |
| 16.4 |
| 16.4-limitless |
| 16.6 |
| 16.6-limitless |
+------------------------+
Terraform Aurora Postgres Stack #
File and Folder Structure #
The file and folder structure of the Terraform project looks like this:
aws-rds-aurora-postgres-production-vpc
├── ec2.tf
├── outputs.tf
├── rds-aurora-postgres.tf
├── terraform.tf
├── variables.tf
└── vpc.tf
Terraform Configuration Files #
Project Folder & Terraform Provider #
# Create Terraform project folder
TF_PROJECT_NAME=aws-rds-aurora-postgres-production-vpc
mkdir $TF_PROJECT_NAME && cd $TF_PROJECT_NAME
- terraform.tf
# Terraform Provider
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
# Provider AWS Region
provider "aws" {
alias = "aws_region"
region = var.aws_region
}
Variables #
- variables.tf
# AWS Region
variable "aws_region" {
description = "AWS Region"
type = string
default = "us-east-1"
}
# VPC Name
variable "vpc_name" {
description = "The name of the VPC"
type = string
default = "Example-VPC"
}
## Postgres
# Postgres User
variable "postgres_user" {
description = "Postgres default user"
type = string
default = "postgres"
}
# Postgres Password
variable "postgres_pw" {
description = "Postgres default user"
type = string
default = "my-secure-pw"
}
## VPC & Subnets
# VPC CIDR
variable "vpc_cidr" {
description = "VPC CIDR block"
type = string
default = "10.10.0.0/16"
}
# Public Subnets CIDR (Loop)
variable "subnets_public_cidr" {
type = list(string)
default = ["10.10.0.0/24", "10.10.1.0/24"]
}
# Public Subnets AZ (Loop)
variable "subnets_public_azs" {
type = list(string)
default = ["us-east-1a", "us-east-1b"]
}
# Private Subnets CIDR (Loop)
variable "subnets_private_cidr" {
type = list(string)
default = ["10.10.2.0/24", "10.10.3.0/24"]
}
# Private Subnets AZ (Loop)
variable "subnets_private_azs" {
type = list(string)
default = ["us-east-1a", "us-east-1b"]
}
## EC2 Instances
# SSH key pair name
variable "key_name" {
default = "us-east-1-pc-le" # Define key pair name
}
# EC2 Image ID
variable "ami_id" {
default = "ami-0e2c8caa4b6378d8c" # Define EC2 AMI ID
}
VPC and Subnets #
- vpc.tf
# VPC "10.10.0.0/16"
resource "aws_vpc" "vpc" {
provider = aws.aws_region
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.vpc_name}"
Env = "Production"
}
}
## Subnets
# Public Subnets (Loop)
resource "aws_subnet" "subnets_public" {
provider = aws.aws_region
count = length(var.subnets_public_cidr)
vpc_id = aws_vpc.vpc.id
cidr_block = element(var.subnets_public_cidr, count.index)
availability_zone = element(var.subnets_public_azs, count.index)
map_public_ip_on_launch = true
tags = {
Name = "${var.vpc_name} Subnet-Public-${count.index + 1}"
Env = "Production"
}
}
# Private Subnets (Loop)
resource "aws_subnet" "subnets_private" {
provider = aws.aws_region
count = length(var.subnets_private_cidr)
vpc_id = aws_vpc.vpc.id
cidr_block = element(var.subnets_private_cidr, count.index)
availability_zone = element(var.subnets_private_azs, count.index)
tags = {
Name = "${var.vpc_name} Subnet-Private-${count.index + 1}"
Env = "Production"
}
}
## Gateways
# Internet Gateway
resource "aws_internet_gateway" "vpc_igw" {
provider = aws.aws_region
vpc_id = aws_vpc.vpc.id
depends_on = [aws_vpc.vpc]
tags = {
Name = "${var.vpc_name} IGW"
Env = "Production"
}
}
# Elastic IPs (EIP) for NAT Gateways (Loop)
resource "aws_eip" "nat_gw_eip" {
provider = aws.aws_region
count = length(var.subnets_public_cidr)
domain = "vpc"
tags = {
Name = "VPC NAT-GW EIP-${count.index + 1}"
Env = "Production"
}
}
# NAT Gateways (Loop)
resource "aws_nat_gateway" "nat_gw" {
provider = aws.aws_region
count = length(var.subnets_public_cidr)
allocation_id = element(aws_eip.nat_gw_eip[*].id, count.index)
subnet_id = aws_subnet.subnets_public[count.index].id
depends_on = [aws_internet_gateway.vpc_igw]
tags = {
Name = "VPC NAT-GW-${count.index + 1}"
Env = "Production"
}
}
## Routing
# Public Route Table
resource "aws_route_table" "public_rt" {
provider = aws.aws_region
vpc_id = aws_vpc.vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.vpc_igw.id
}
depends_on = [
aws_vpc.vpc,
aws_internet_gateway.vpc_igw
]
tags = {
Name = "${var.vpc_name} Public Route Table"
Env = "Production"
}
}
# Private Route Tables (Loop)
resource "aws_route_table" "private_rt" {
provider = aws.aws_region
count = length(var.subnets_private_cidr)
vpc_id = aws_vpc.vpc.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = element(aws_nat_gateway.nat_gw[*].id, count.index)
}
tags = {
Name = "${var.vpc_name} Private Route Table-${count.index + 1}"
Env = "Production"
}
}
# Associate Route Tables with Public Subnets
resource "aws_route_table_association" "subnets_public_ra" {
provider = aws.aws_region
count = length(var.subnets_public_cidr)
subnet_id = element(aws_subnet.subnets_public.*.id, count.index)
route_table_id = aws_route_table.public_rt.id
depends_on = [aws_route_table.public_rt]
}
# Associate Route Tables with Private Subnets
resource "aws_route_table_association" "private_rta" {
provider = aws.aws_region
count = length(var.subnets_private_cidr)
subnet_id = aws_subnet.subnets_private[count.index].id
route_table_id = aws_route_table.private_rt[count.index].id
}
RDS Aurora PostgreSQL #
- rds-aurora-postgres.tf
# Aurora Subnet Group
resource "aws_db_subnet_group" "aurora_subnet_group" {
provider = aws.aws_region
name = "aurora-main"
subnet_ids = [aws_subnet.subnets_private[0].id, aws_subnet.subnets_private[1].id]
depends_on = [aws_subnet.subnets_private]
tags = {
Name = "${var.vpc_name} Aurora Subnet Group"
Env = "Production"
}
}
# Aurora Cluster
resource "aws_rds_cluster" "aurora_cluster" {
provider = aws.aws_region
cluster_identifier = "aurora-postgres-cluster"
engine = "aurora-postgresql"
engine_version = "16.6"
database_name = "auroradb"
master_username = var.postgres_user
master_password = var.postgres_pw
db_subnet_group_name = aws_db_subnet_group.aurora_subnet_group.name
vpc_security_group_ids = [aws_security_group.sg_postgres.id]
backup_retention_period = 7
preferred_backup_window = "03:00-04:00"
availability_zones = [aws_subnet.subnets_private[0].availability_zone, aws_subnet.subnets_private[1].availability_zone]
storage_encrypted = true
skip_final_snapshot = true # Ensure final snapshots are skipped during deletion
final_snapshot_identifier = null # Prevent Terraform from expecting a final snapshot identifier
tags = {
Name = "${var.vpc_name} Aurora Cluster"
Env = "Production"
}
depends_on = [aws_db_subnet_group.aurora_subnet_group]
}
## Aurora Instances
# Aurora Writer Instance
resource "aws_rds_cluster_instance" "aurora_writer_instance" {
provider = aws.aws_region
identifier = "aurora-postgres-writer"
cluster_identifier = aws_rds_cluster.aurora_cluster.id
instance_class = "db.t3.medium"
engine = "aurora-postgresql"
publicly_accessible = false
db_subnet_group_name = aws_db_subnet_group.aurora_subnet_group.name
tags = {
Name = "${var.vpc_name} Aurora Writer Instance"
Env = "Production"
}
depends_on = [aws_rds_cluster.aurora_cluster]
}
# Aurora Reader Instance
resource "aws_rds_cluster_instance" "aurora_reader_instance" {
provider = aws.aws_region
identifier = "aurora-postgres-reader"
cluster_identifier = aws_rds_cluster.aurora_cluster.id
instance_class = "db.t3.medium"
engine = "aurora-postgresql"
publicly_accessible = false
db_subnet_group_name = aws_db_subnet_group.aurora_subnet_group.name
tags = {
Name = "${var.vpc_name} Aurora Reader Instance"
Env = "Production"
}
depends_on = [aws_rds_cluster.aurora_cluster]
}
# Aurora Security Group
resource "aws_security_group" "sg_postgres" {
provider = aws.aws_region
name = "SG_Allow_Aurora_Postgres"
description = "Allow Aurora Postgres traffic"
vpc_id = aws_vpc.vpc.id
ingress {
description = "Allow Aurora Postgres from EC2 instances"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.sg_ec2.id] # EC2 security group
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
depends_on = [aws_vpc.vpc]
tags = {
Name = "${var.vpc_name} Aurora Postgres SG"
Env = "Production"
}
}
EC2 Instances #
- ec2.tf
# EC2 Instances in Public Subnets (Loop)
resource "aws_instance" "vm" {
provider = aws.aws_region
count = length(var.subnets_public_cidr) # Create as many instances as subnets
ami = var.ami_id
instance_type = "t3.small"
subnet_id = aws_subnet.subnets_public[count.index].id
key_name = var.key_name
vpc_security_group_ids = [aws_security_group.sg_ec2.id]
depends_on = [
aws_vpc.vpc,
aws_security_group.sg_ec2
]
tags = {
Name = "VM Public Subnet ${count.index + 1}"
Env = "Production"
}
}
# Security Group for SSH Access and Ping
resource "aws_security_group" "sg_ec2" {
provider = aws.aws_region
name = "SG"
description = "Security group for SSH access and ping"
vpc_id = aws_vpc.vpc.id
ingress {
description = "Allow SSH from anywhere"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "Allow ping"
from_port = 8
to_port = -1
protocol = "icmp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "Allow all outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
depends_on = [aws_vpc.vpc]
tags = {
Name = "SG"
Env = "Production"
}
}
Output #
- outputs.tf
# RDS Aurora Postgres Writer Endpoint
output "aurora_writer_endpoint" {
value = aws_rds_cluster.aurora_cluster.endpoint
}
# RDS Aurora Postgres Reader Endpoint
output "aurora_reader_endpoint" {
value = aws_rds_cluster.aurora_cluster.reader_endpoint
}
# RDS Aurora Postgres Cluster ID
output "aurora_cluster_id" {
value = aws_rds_cluster.aurora_cluster.id
description = "The ID of the Aurora PostgreSQL cluster"
}
# EC2 Public IPs
output "EC2_Public_IPs" {
description = "Public IPs of the EC2 instances"
value = aws_instance.vm[*].public_ip
}
# EC2 Private IPs
output "EC2_Private_IPs" {
description = "Private IPs of the EC2 instances"
value = aws_instance.vm[*].private_ip
}
Configuration Deployment #
Initialize Terraform Project #
This will download and install the AWS Terraform provider defined in the terraform.tf file with “hashicorp/aws”, as well as setting up the configuration files in the project directory.
# Initialize the Terraform project
terraform init
Validate Configuration Files #
# Validates the syntax and structure of Terraform configuration files
terraform validate
# Shell output:
Success! The configuration is valid.
Plan the Deployment #
# Dry run / preview changes before applying them
terraform plan
Apply the Configuration #
Note that it takes a while for the setup to complete:
aws_rds_cluster_instance.aurora_writer_instance: Creation complete after 10m6s [id=aurora-postgres-writer]
# Create network stack
terraform apply -auto-approve
# Shell output:
Apply complete! Resources: 25 added, 0 changed, 0 destroyed.
Outputs:
EC2_Private_IPs = [
"10.10.0.204",
"10.10.1.69",
]
EC2_Public_IPs = [
"35.175.210.61",
"44.193.28.10",
]
aurora_cluster_id = "aurora-postgres-cluster"
aurora_reader_endpoint = "aurora-postgres-cluster.cluster-ro-cpyy8k66ctzh.us-east-1.rds.amazonaws.com"
aurora_writer_endpoint = "aurora-postgres-cluster.cluster-cpyy8k66ctzh.us-east-1.rds.amazonaws.com"
RDS Endpoint:
-
RDS provides a single DNS endpoint regardless of whether the database is deployed in a Single-AZ or Multi-AZ configuration
-
This endpoint automatically resolves to the primary instance (writer) in your RDS setup.
Verify PostgreSQL Connectivity #
SSH Into EC2 Instance #
# SSH into EC2 instance 1
ssh -i /home/ubuntu/.ssh/us-east-1-pc-le.pem ubuntu@35.175.210.61
# SSH into EC2 instance 2
ssh -i /home/ubuntu/.ssh/us-east-1-pc-le.pem ubuntu@44.193.28.10
Install Postgres Client #
# Install the "postgresql-common" package, used for the helper scripts
sudo apt install postgresql-common -y
# Add the PostgreSQL (PGDG) repository for latest PostgreSQL versions (bypass installation prompt)
yes "" | sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
# Install Postgres client
sudo apt install postgresql-client -y
PostgreSQL Writer Endpoint #
Connect to Endpoint #
# Connect to PostgreSQL
psql -h aurora-postgres-cluster.cluster-cpyy8k66ctzh.us-east-1.rds.amazonaws.com -U postgres -d postgres
# Shell output
Password for user postgres: # Enter PW
-
-U postgres
Username to authenticate with the PostgreSQL database -
-d postgres
Database name to connect to
Verify PostfreSQL Node Role #
# Verify if the node is a recovery node:
SELECT pg_is_in_recovery();
# Shell output:
pg_is_in_recovery
-------------------
f
(1 row)
Create Example DB & Tables #
# Create example database "example_database"
create database example_database;
# Connect to "example_database" database
\c example_database
# Create some tables
create table some_table_1(a varchar(100), b timestamp);
create table some_table_2(a varchar(100), b timestamp);
create table some_table_3(a varchar(100), b timestamp);
# Insert some values
insert into some_table_1 values('some value', '2024-01-01 01:01:01');
insert into some_table_2 values('some value', '2024-01-01 01:01:01');
insert into some_table_3 values('some value', '2024-01-01 01:01:01');
# Exit Postgres
\q
PostgreSQL Reader Endpoint #
Connect to Endpoint #
# Connect to PostgreSQL
psql -h aurora-postgres-cluster.cluster-ro-cpyy8k66ctzh.us-east-1.rds.amazonaws.com -U postgres -d postgres
# Shell output
Password for user postgres: # Enter PW
-
-U postgres
Username to authenticate with the PostgreSQL database -
-d postgres
Database name to connect to
Verify PostfreSQL Node Role #
# Verify if the node is a recovery node:
SELECT pg_is_in_recovery();
# Shell output:
pg_is_in_recovery
-------------------
t
(1 row)
List Databases #
# List databases
\l
# Shell output:
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
------------------+----------+----------+-----------------+-------------+-------------+--------+-----------+-----------------------
auroradb | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | |
example_database | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | |
postgres | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | |
rdsadmin | rdsadmin | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | | rdsadmin=CTc/rdsadmin
template0 | rdsadmin | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | | =c/rdsadmin +
| | | | | | | | rdsadmin=CTc/rdsadmin
template1 | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
(6 rows)
# Exit Postgres
\q
Verify AWS Resources #
List PostgreSQL Instances #
# List RDS instances
aws rds describe-db-instances --query "DBInstances[?DBClusterIdentifier=='aurora-postgres-cluster'].{InstanceID:DBInstanceIdentifier, Subnets:DBSubnetGroup.Subnets[*].SubnetIdentifier}" --output json
# Shell output:
[
{
"InstanceID": "aurora-postgres-reader",
"Subnets": [
"subnet-02c2f0ecbc4add9c2",
"subnet-023a32c426d08aa48"
]
},
{
"InstanceID": "aurora-postgres-writer",
"Subnets": [
"subnet-02c2f0ecbc4add9c2",
"subnet-023a32c426d08aa48"
]
}
]
List Instance Status & Details #
# List Master / Writer node details
aws rds describe-db-instances \
--db-instance-identifier aurora-postgres-writer \
--query "DBInstances[*].{InstanceID:DBInstanceIdentifier,Status:DBInstanceStatus,AZ:AvailabilityZone,Class:DBInstanceClass}" \
--output table
# Shell output:
-----------------------------------------------------------------------
| DescribeDBInstances |
+------------+---------------+--------------------------+-------------+
| AZ | Class | InstanceID | Status |
+------------+---------------+--------------------------+-------------+
| us-east-1a| db.t3.medium | aurora-postgres-writer | available |
+------------+---------------+--------------------------+-------------+
# List Slave / Reader node details
aws rds describe-db-instances \
--db-instance-identifier aurora-postgres-reader \
--query "DBInstances[*].{InstanceID:DBInstanceIdentifier,Status:DBInstanceStatus,AZ:AvailabilityZone,Class:DBInstanceClass}" \
--output table
# Shell output:
-----------------------------------------------------------------------
| DescribeDBInstances |
+------------+---------------+--------------------------+-------------+
| AZ | Class | InstanceID | Status |
+------------+---------------+--------------------------+-------------+
| us-east-1b| db.t3.medium | aurora-postgres-reader | available |
+------------+---------------+--------------------------+-------------+
Management Console #
- Verify the PostgreSQL cluster via the AWS Management Console

Links #
# GitHub Repository
https://github.com/jueklu/terraform-aws-rds-postgres