GitHub Repository Available
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
Terraform RDS Postgres Stack #
File and Folder Structure #
The file and folder structure of the Terraform project looks like this:
aws-rds-postgres-basic-vpc
├── ec2.tf
├── outputs.tf
├── rds-postgres.tf
├── terraform.tf
├── variables.tf
└── vpc.tf
Terraform Configuration Files #
Project Folder & Terraform Provider #
# Create Terraform project folder
TF_PROJECT_NAME=aws-rds-postgres-basic-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"]
}
## 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"
}
}
# 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"
}
}
# 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"
}
}
## 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"
}
}
# Associate Routes with 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]
}
RDS PostgreSQL #
- rds-postgres.tf
# RDS Subnet Group
resource "aws_db_subnet_group" "rds_subnet_group" {
provider = aws.aws_region
name = "main"
subnet_ids = [aws_subnet.subnets_public[0].id, aws_subnet.subnets_public[1].id]
depends_on = [aws_subnet.subnets_public]
tags = {
Name = "${var.vpc_name} RDS Subnet Group"
Env = "Production"
}
}
# RDS PostgreSQL
resource "aws_db_instance" "rds_db" {
provider = aws.aws_region
allocated_storage = 10
identifier = "postgres-test2"
db_subnet_group_name = aws_db_subnet_group.rds_subnet_group.id
engine = "postgres"
engine_version = "14"
instance_class = "db.t3.micro"
username = var.postgres_user
password = var.postgres_pw
vpc_security_group_ids = [aws_security_group.sg_postgres.id]
publicly_accessible = true
skip_final_snapshot = true
multi_az = true # Enables Multi-AZ for HA
depends_on = [aws_db_subnet_group.rds_subnet_group]
}
# RDS Security Group
resource "aws_security_group" "sg_postgres" {
provider = aws.aws_region
name = "SG_Allow_Postgres"
description = "Allow Postgres traffic"
vpc_id = aws_vpc.vpc.id
ingress {
description = "Allow Postgres"
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
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} RDS Postgres"
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 Postgres DNS Endpoints
output "rds_endpoint" {
value = aws_db_instance.rds_db.endpoint
}
# EC2 Public IPs
output "EC2_Public_IPs" {
description = "Public IPs of the EC2 instances"
value = aws_instance.vm[*].public_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_db_instance.rds_db: Creation complete after 13m1s [id=db-MKHTUOXIM667QBROAJ5I7L6GOM]
# Create network stack
terraform apply -auto-approve
# Shell output:
Apply complete! Resources: 13 added, 0 changed, 0 destroyed.
Outputs:
EC2_Public_IPs = [
"34.228.231.45",
"3.235.232.96",
]
rds_endpoint = "postgres-test2.cpyy8k66ctzh.us-east-1.rds.amazonaws.com:5432"
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@34.228.231.45
# SSH into EC2 instance 2
ssh -i /home/ubuntu/.ssh/us-east-1-pc-le.pem ubuntu@3.235.232.96
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
Connect to PostgreSQL Server #
# Connect to PostgreSQL
psql -h postgres-test2.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
# List Postgres version:
SELECT version();
# Shell output:
version
----------------------------------------------------------------------------------------------------------
PostgreSQL 14.12 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 7.3.1 20180712 (Red Hat 7.3.1-17), 64-bit
(1 row)
# Exit Postgres
\q
Links #
# GitHub Repository
https://github.com/jueklu/terraform-aws-rds-postgres