Terraforming AWS RDS : A practical guide with Postgres
Tags: aws, rds, postgres, sql
Published: 2023-06-08 11:51:04

I have recently written a series of articles about PostgreSQL and introduced storytelling. The writing with storytelling was fun, and I will write more articles like this. 

Nevertheless, I want to return to the basics and rewrite something with only the technical aspects. Please do not hesitate to give me any feedback about what kind of article you prefer. :)

In this article, we will dive into AWS basics and aim to end up with an entire terraform file to deploy a single availability zone RDS database that is available publicly.

## Create an accessible network 

To create our database accessible to the public, we will need to create a network first. This will require several components, first a VPC, then an Internet Gateway, subnetworks, and everything sprinkled with a routing table.

### Create a VPC 

First thing first, we need to configure a VPC. VPC is an acronym for Virtual Private Cloud. This virtual private network has a range of network addresses isolated from other networks. These addresses are not accessible outside your VPC, for now, without any extra component.

A VPC is located in a region and can serve each availability zone of this region. If you want to dig more about VPC, you can go to the AWS [documentation](https://docs.aws.amazon.com/fr_fr/vpc/latest/userguide/what-is-amazon-vpc.html)

You could use Terraform when you want to deploy a VPC and need your architecture to be auditable, testable, and repeatable.
This is what a terraform could look like for deploying a VPC.

```json
 resource "aws_vpc" "main" {
  cidr_block       = "10.0.0.0/16"
  instance_tenancy = "default"
  tags = {
    Name = "main"
  }
}
```

With this code, you will deploy a VPC with a range of addresses from `10.0.0.0` to `10.0.255.255`, representing 65,536 addresses. 

If you want to avoid computation of the number of addresses by head or hand, there is this [cidr calculator](https://www.ipaddressguide.com/cidr), but this is only an example; plenty is out there!

We put the `instance_tenancy = default`; this enables us to work with any RDS database. The other possible value for this is `dedicated`. But it would be best to ensure your database is approved to work with the `dedicated` mode.

### Divide ut regnes

If we want to create any instances of AWS resources, we must create a subnetwork. In our case, we will create two subnetworks in our VPC. 

```json
variable "subnets_cidr" {
  type    = list(string)
  default = ["10.0.1.0/24", "10.0.2.0/24"]
}

variable "azs" {
  type    = list(string)
  default = ["us-east-1a", "us-east-1b"]
}

resource "aws_subnet" "public" {
  count                   = length(var.subnets_cidr)
  vpc_id                  = aws_vpc.main.id
  cidr_block              = element(var.subnets_cidr, count.index)
  availability_zone       = element(var.azs, count.index)
  map_public_ip_on_launch = true
  tags = {
    Name = "Subnet-${count.index + 1}"
  }
}
```

Excellent, we have created two subnetworks. These two subnetworks have 255 addresses each. These subnets are each in a different availability zone. The first one is in `us-east-1a` and has the subrange `10.0.1.0/24`, and the second one is in `us-east-1b` and has the subrange `10.0.2.0/24`. (If you want to train, put in a comment: What is the number of addresses available in these two subnetworks)

The `element(var.name)` iterates over the value of a list and assign one value, but we need the `count = length(var.subnets_cidr)` to declare a loop.

## Routing towards the subnet

If we want the ingress and egress traffic to be well redirected, we must create a route table and associate this route table with our subnetworks.

In Terraform, this is special; you have a resource that is available to associate your route table with your subnetworks. 
But this is not an AWS resource; this is an abstraction. 

You cannot create a `route table association` object anywhere in AWS. Indeed when you create your `route table` directly, you can modify it to what subnetworks it is attached to.

```json
resource "aws_route_table" "public-rt" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gw.id
  }

  tags = {
    Name = "public-rt"
  }
}

resource "aws_route_table_association" "a" {
  count          = length(var.subnets_cidr)
  subnet_id      = element(aws_subnet.public.*.id, count.index)
  route_table_id = aws_route_table.public-rt.id
}
```

## Time to deal with serious things
Here we go, time to deal with the heart of this article:  the database.

### DB subnet groups

To deploy our database, we need two resources. The first one is the `aws_db_instance` as you can imagine. But we need an `aws_db_subnet_group`. 

What are these `db subnet groups`? Let me explain!

A DB subnet group contains several subnets. AWS RDS chooses a subnet from the DB subnet group, gives the database an IP address, and affects it to an availability zone.

```json
resource "aws_db_subnet_group" "default" {
  name       = "main"
  subnet_ids = [aws_subnet.public[0].id, aws_subnet.public[1].id]

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

When building our db subnet group, we must give it at least two subnets for the subnets ids, as we have seen above. 

### Security 

Security should be one of your top concerns; controlling traffic in and out is necessary. In particular, in the case of publicly accessible database

We need to describe what traffic can go in and what cannot. This is managed by the `aws_security_group` in Terraform.

In this resource, you will describe the port you allow access to, the protocol on that port, and the IP address entitled. You will do the same for the traffic that goes out.

```json
resource "aws_security_group" "allow_postgres_traffic" {
  name        = "allow_postgres"
  description = "Allow Postgres traffic"
  vpc_id      = aws_vpc.main.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"]
    ipv6_cidr_blocks = ["::/0"]
  }

  tags = {
    Name = "allow postgres"
  }
}
```

In that case, there is no such protection. Indeed, we authorize any IP address to access port 5432. Of course, this has to be tuned to your needs, with more restrictions.

### Almost there

Now we have to deploy our database. This is straightforward, and the attributes speak for themselves. We want to deploy a t2.micro in the main VPC. This database should be publicly accessible within the security group deployed before. 

```json
resource "aws_db_instance" "db-test1" {
  allocated_storage      = 10
  identifier             = "postgres-test2"
  db_subnet_group_name   = aws_db_subnet_group.default.id
  engine                 = "postgres"
  engine_version         = "14"
  instance_class         = "db.t3.micro"
  username               = "postgres"
  password               = "postgres"
  vpc_security_group_ids = [aws_security_group. allow_postgres_traffic.id]
  publicly_accessible    = true
  skip_final_snapshot    = true
}
```

## Deploy it

Thanks for following along with me. Through this article, we have seen how to deploy a publicly accessible database on AWS.

To do so, we need to deploy a variety of resources: 
- A VPC to ensure we have a Private virtual network for our resources. 
- Subnets to divide our Private Network; this is needed if we want to deploy a multi-AZ architecture, now or in the future.
- A DB subnet group That links the subnet and a database
- A routing table that will route the external traffic inward and 

I have created a [gist](https://gist.github.com/Lucas-Barret/32f71049d1365baa70e26cbc483da975) where the whole file is available if you want to test what we have built here.

### Keep in Touch 

On Twitter : [@yet_anotherDev] (https://twitter.com/yet_anotherDev)

On Linkedin : [Lucas Barret](https://www.linkedin.com/in/lucas-barret-67194b164/)