Creating an AWS RDS instance with a read replica using Terraform

Creating an AWS RDS instance with a read replica using Terraform

ยท

8 min read

Hello folks! In this article, I'm going to be going through a simple example of creating an RDS instance along with its read replica on AWS using Terraform.

If you read any of my previous terraform articles you know the drill. We're going to be creating a VPC and creating the RDS instances in a private subnet. Then spinning up an EC2 instance in a public subnet and we're going to ssh into that instance and access the RDS instances.

You may wonder why create the instances in a private subnet, not a public one.

Yes they will only be accessible from inside the VPC and we won't be able to access them from outside but I learned recently about a command sshuttle that tunnels from my network to the AWS VPC network and executes the requests from there. Hence why I used a simple EC2 instance that can access the RDS. (I was only trying to use it somehow forgive me ๐Ÿ˜…)

If you don't want to use it you can just add the RDS instances in the public subnets and you'll be able to access them normally. (I'll let you know when to do this down below)

Let's start by creating a new directory terraform-rds then start by creating our provider in our case AWS as shown below

# provider.tf
provider "aws" {
  access_key = "access-key"
  secret_key = "super-secret-key"
  region = "eu-central-1"
}

VPC and Subnets

We'll start by creating our VPC and subnets as shown below

# vpc.tf
resource "aws_vpc" "rds-shuttle-vpc" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_internet_gateway" "prod-gateway" {
  vpc_id = aws_vpc.rds-shuttle-vpc.id

  tags = {
    Name = "main-gateway"
  }
}

Simply we create our VPC giving it a CIDR block of 10.0.0.0/16 (First 16 bits are dedicated to the network, the rest to the host). And then we create an Internet Gateway so our VPC can have access to the public internet.

Now our subnets, we'll have 1 public and 2 private subnets:

resource "aws_subnet" "priv-subnet" {
  vpc_id     =  aws_vpc.rds-shuttle-vpc.id
  cidr_block = "10.0.10.0/24"
  availability_zone = "eu-central-1a"
  tags = {
    Name = "rds-priv-subnet"
  }
  map_public_ip_on_launch = false
}

resource "aws_subnet" "priv-subnet2" {
  vpc_id     =  aws_vpc.rds-shuttle-vpc.id
  cidr_block = "10.0.12.0/24"
  availability_zone = "eu-central-1b"
  tags = {
    Name = "rds-priv-subnet2"
  }
  map_public_ip_on_launch = false
}

resource "aws_db_subnet_group" "rds-sg" {
  name       = "rds-subnet-group"
  subnet_ids = [aws_subnet.priv-subnet.id, aws_subnet.priv-subnet2.id]

  tags = {
    Name = "My DB subnet group"
  }
}

For the private subnets, we choose a cidr block of 10.0.0.0/24 (First 3 bytes indicate a subnet, rest for the host), choosing a random IP address range for each subnet. We give them also the availability zone giving each one a different zone. Lastly, we set map_public_ip_on_launch to False and what this does is it prevents anything launched in this instance from having a public IP address.

Lastly, we create a subnet group (Our RDS instances will need it later on) adding in our 2 private subnets to it.

Now for the public subnet:

resource "aws_subnet" "public-subnet" {
  vpc_id     =  aws_vpc.rds-shuttle-vpc.id
  cidr_block = "10.0.1.0/24"
  availability_zone = "eu-central-1b"
  tags = {
    Name = "rds-pub-subnet"
  }
}
resource "aws_route_table" "pub-route-table" {
  vpc_id = aws_vpc.rds-shuttle-vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.prod-gateway.id
  }

  route {
    ipv6_cidr_block        = "::/0"
    gateway_id = aws_internet_gateway.prod-gateway.id
  }

  tags = {
    Name = "public subnet route table"
  }
}

resource "aws_route_table_association" "subnet-1-association" {
  subnet_id      = aws_subnet.public-subnet.id
  route_table_id = aws_route_table.pub-route-table.id
}

Same thing as before, creating a random IP address range for the subnet as well as an availability zone.

We create a routing table for this subnet to allow any requests to go through the internet gateway previously created since it should be a public subnet. The 0.0.0.0/0 is a shorthand for any requested IP address. The other block is for IPV6 . Then we proceed to bind the routing table with our public subnet.

Security Groups

We'll be creating security groups to open only required ports for our to be created rds and ec2 instances.

resource "aws_security_group" "rds-sg" {
  name        = "rds-security-group"
  description = "the rds sg"
  vpc_id      = aws_vpc.rds-shuttle-vpc.id

  ingress {
    description      = "5432 psql"
    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"]
    ipv6_cidr_blocks = ["::/0"]
  }

  tags = {
    Name = "rds-sg"
  }
}

This security group is for our RDS instances and simply allows requests through port 5432 which is the default port for Postgresql (The database we'll be using). We also allow any IP address range to request from it. We can tailor this to only the VPC address range we chose since it's a private instance (optional).

For the egress (outgoing requests) we allow everything not putting a restriction here.

Now for the ec2 instance security group

resource "aws_security_group" "ec2-sg" {
  name        = "ec2 sg"
  description = "the ec2 sg"
  vpc_id      = aws_vpc.rds-shuttle-vpc.id

  ingress {
    description      = "22 psql"
    from_port        = 22
    to_port          = 22
    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"]
    ipv6_cidr_blocks = ["::/0"]
  }

  tags = {
    Name = "rds-sg"
  }
}

We allow port 22 (our SSH port) only. Again no restrictions on our egress.

RDS instance + its read replica

For the rds instance we'll do the following

resource "aws_db_instance" "postgresql-rds" {
  allocated_storage    = 5
  db_name              = "funkymonkey"
  engine               = "postgres"
  engine_version       = "14.7"
  instance_class       = "db.t3.micro"
  username             = "db-username"
  password             = "super-secret-password"
  skip_final_snapshot  = true
  backup_retention_period = 7
  vpc_security_group_ids = [aws_security_group.rds-sg.id]
  db_subnet_group_name = aws_db_subnet_group.rds-sg.name
}

allocated_storage is pretty much what we want the storage of our database to be in GBs. I chose 5 GB because it's just for demo purposes. The database won't auto scale storage once it passes 5 GB. We can do that but i didn't add that part in the tutorial here.

We then add our db_name (obvious right), our engine is Postgres as mentioned before and the version we'll be using is 14.7. Feel free to tailor these based on your needs of course

Our instance class is db.t3.micro which provides a baseline performance level. For more info on instance classes visit here

Then we provide our database authentication username and password.

skip_final_snapshot Determines whether a final DB snapshot is created before the DB instance is deleted. We don't need anything since this is just for demo but it's better to enable this in real-life servers.

backup-retention-period is the number of days that we should retain (keep) backups. Since we have a read replica that we're going to provision, this must be added and must be greater than 0.

Then we proceed to provide the security group id we created previously along with the subnet group too.

Finally, we'll output the RDS endpoint

output "rds-url" {
  value = aws_db_instance.postgresql-rds.endpoint
}

To place the instance in a public subnet you'll need to have 2 public subnets in one group, then after creating a public subnet group simply add its id in the rds resource you're creating. That way when you get the RDS endpoint you can access it from your machine

Now for the read replica it's very simple

resource "aws_db_instance" "replica-postgresql-rds" {
  instance_class       = "db.t3.micro"
  skip_final_snapshot  = true
  backup_retention_period = 7
  replicate_source_db = aws_db_instance.postgresql-rds.identifier
}

output "replica-url" {
  value=aws_db_instance.replica-postgresql-rds.endpoint
}

We simply provide replicate_source_db and give it the identifier of the master db.

So far we created the RDS and it's read replica. But in private subnets, let's spin up and EC2 instance and try to access them!

EC2 instance to access the database

Let's spin up a basic EC2 instance giving it a public IP address so we can ssh to it. Before doing so we need a couple of resources ready

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"]
}

This is the data resource that fetches the AMI (Image) For our EC2 instance. We'll be using ubuntu base image

resource "aws_network_interface" "shuttle" {
  subnet_id   = aws_subnet.public-subnet.id
  private_ips = ["10.0.1.164"]
  security_groups = [ aws_security_group.ec2-sg.id ]

  tags = {
    Name = "ec2-ni"
  }
}

A network interface that we'll use to attach to our EC2 instance. Giving it a private ip address of 10.0.1.164 (notice that it's located inside our public subnet of range 10.0.1.0/24 ) and our previously created security group.

resource "aws_eip" "shuttle-ip" {
  domain                    = "vpc"
  network_interface         = aws_network_interface.shuttle.id
  associate_with_private_ip = "10.0.1.164"
  depends_on                = [aws_internet_gateway.prod-gateway]
}

A public static IP Address for our EC2 instance. Giving it the network interface and IP address.

resource "aws_key_pair" "deployer" {
  key_name   = "shuttle-key"
  public_key = "pub-key"
}

A aws_key_pair which is super important to be able to ssh into the ec2 instance, you need to generate a public-private key pair using ssh keygen on your machine and paste the public key into the resource above.

resource "aws_instance" "shuttle-server" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
  network_interface {
    network_interface_id = aws_network_interface.shuttle.id
    device_index         = 0
  }
  tags = {
    Name = "shuttle server"
  }
  key_name = aws_key_pair.deployer.key_name
}

Finally the ec2 instance of type t3.micro, giving it the key name and network interface we created above.

Now that everything is set, let's execute terraform init followed by terraform apply and give it time to get everything ready

Once everything is up and running let's use sshuttle and dbeaver to connect to our database. Follow the steps below

  1. Install sshuttle and dbeaver our database tool we'll be using

  2. Once everything is installed, open a terminal and execute sshuttle -r ubuntu@<ec2-ip-address> 0.0.0.0/0 --ssh-cmd "ssh -i <path to private key created>"

    The username has to be ubuntu since its the default for a ubuntu image, You can find the EC2 public address in your management console when you head to EC2. Or you can use output in terraform to output the address. The 0.0.0.0/0 is the IP address range you wish to direct to the EC2 instance. It means any request in that IP address range will be tunneled to the shuttle server and executed from there. (Exactly how a VPN works). We'll allow all requests to be tunneled through the ec2 instance.

  3. Open dbeaver and create a new connection adding the RDS hostname (given from output), username and password and viola! you have access to both rds instances created. (The read replica has the same username and password as the master db)

That's all you need to know to simply provision an RDS instance along with its read replica! Thanks for reading and hope you learned something out of it. Till the next one!

Did you find this article valuable?

Support Amr Elhewy by becoming a sponsor. Any amount is appreciated!

ย