Creating a free-tier AWS RDS PostgreSQL instance using Terraform
Tags: aws, devops, terraform
Published: 2024-04-06 19:14:25

One of the most powerful tools to use as a DevOps engineer is Terraform. Simply put, Terraform is

> an infrastructure as code (IaC) tool that lets you define both cloud and on-prem resources in human-readable configuration files that you can version, reuse, and share.

This means that you can actually create a whole infrastructure using declarative code. In other words, you are able to manage resources like storages as well as databases using only code.

One of the main benefits in using IaC is to have a better control of your resources and make them scalable. Even more: if you are interested in using multiple cloud services, you can control all the resources from one place, saving you time from clicking and opening multiple tabs in your browser to access each cloud service console.

In this article, I will provide a guide on how to **create a free-tier AWS RDS database instance using Terraform**. I write for you who want to create a RDS database instance but don't want to deal too much with AWS console. Or if you just want to learn a bit the basics of Terraform. Let's get started!

---

## Prerequisites

First of all, you need to have:
- An AWS account eligible to use RDS free-tier
- An AWS Access Key and Secret Key with Full Access to RDS associated to a new IAM user ([or use root at your own risk](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_root-user_manage_add-key.html))
- The [Terraform CLI](https://developer.hashicorp.com/terraform/install) installed

## Selecting and pointing to AWS provider

First of all, let's create a folder to initialize a Terraform template:

```bash
mkdir terraform-aws-free-rds
cd terraform-aws-free-rds
```

In this folder, create a `provider.tf` that is going to be our entry point to declare which provider are we going to use. This is where we define  For now, its content will be:

```hcl
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.43.0"
    }
  }
}

provider "aws" {}
```

This will tell Terraform: "I want to use the AWS provider to start my infrastructure!"

But still Terraform don't know to which region we are going to create our resources or even how to access our AWS account to make these changes. In order to do so, we are going to create an AWS access key and secret key. 

We also do not want to use it explicitly in our `provider.tf` file. So we are going to create a `variables.tf` file and put this:

```hcl
variable "aws_access_key" {
  type        = string
  description = "AWS access key"
}

variable "aws_secret_key" {
  type        = string
  description = "AWS secret key"
}
``` 

This will declare to Terraform that we have these two variables that can be accessed using `var.aws_secret_key` and `var.aws_access_key` and set using a `terraform.tfvars` file.

Create your `terraform.tfvars` and put your credentials:

```hcl
aws_access_key = "your-access-key"
aws_secret_key = "your-secret-keys"
```

This way, Terraform will know which values these variables have and use it on our `provider.tf` file. Terraform will able to create, update or delete resources using this account. One more step and our provider will be completely set:

```hcl
# on `provider.tf`

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.43.0"
    }
  }
}

provider "aws" {
  region     = "us-east-1"
  access_key = var.aws_access_key
  secret_key = var.aws_secret_key
}
```

Now Terraform will indeed know to which region and which account the resources are going to be created.

## Declare your RDS resource

Now we just need to tell Terraform to create a RDS resource. As we are using aws provider to Terraform, [you may check this documentation](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_instance) to understand a bit more of what we are going to write on the `main.tf` file.

```hcl
resource "aws_db_instance" "my-free-rds-db-instance" {
  allocated_storage        = 20
  engine                   = "postgres"
  engine_version           = "16.1"
  identifier               = "my-free-rds-db-instance"
  instance_class           = "db.t3.micro"
  storage_encrypted        = false
  publicly_accessible      = true
  delete_automated_backups = true
  skip_final_snapshot      = true
  db_name                  = "database_name"
  username                 = "database_username"
  password                 = "database_password"
  apply_immediately        = true
  multi_az                 = false
}
```

Basically, what is happening here is that:

- We tell Terraform to use AWS provider, using the credentials we set in `terraform.tfvars`
- We declare a `aws_db_instance` called `my-free-rds-db-instance` and specify all the instance configuration that would be also specified via AWS console if we were creating using it.

This configuration is known to be a free tier in RDS for 12 months. There are other configurations but for the PostgreSQL is this one:

> 750 hours of Amazon RDS Single-AZ db.t2.micro, db.t3.micro, and db.t4g.micro Instances usage running MySQL, MariaDB, PostgreSQL databases each month. ... 20 GB of General Purpose SSD (gp2) storage per month.

If you check in the `main.tf` file, we define this configuration in a declarative way.

## Spinning up your Terraform resources

Now run

```bash
terraform init # to initialize the dependency lock file

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "5.43.0"...
- Installing hashicorp/aws v5.43.0...
- Installed hashicorp/aws v5.43.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

...
```

Now we have our AWS provider set in our Terraform context, which will enable us to tell our local Terraform CLI commands which version of the provider to use.

We may see the execution plan of the resources we have declared:

```bash
terraform plan
```

The output should be something like this:

```bash
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_db_instance.my-free-rds-db-instance will be created
  + resource "aws_db_instance" "my-free-rds-db-instance" {
      + address                               = (known after apply)
      + allocated_storage                     = 20
      + apply_immediately                     = true
      + arn                                   = (known after apply)
      + auto_minor_version_upgrade            = true
      + availability_zone                     = (known after apply)
      + backup_retention_period               = (known after apply)
      + backup_target                         = (known after apply)
      + backup_window                         = (known after apply)
      + ca_cert_identifier                    = (known after apply)
      + character_set_name                    = (known after apply)
      + copy_tags_to_snapshot                 = false
      + db_name                               = "database_name"
      + db_subnet_group_name                  = (known after apply)
      + delete_automated_backups              = true
      + domain_fqdn                           = (known after apply)
      + endpoint                              = (known after apply)
      + engine                                = "postgres"
      + engine_version                        = "16.1"
      + engine_version_actual                 = (known after apply)
      + hosted_zone_id                        = (known after apply)
      + id                                    = (known after apply)
      + identifier                            = "my-free-rds-db-instance"
      + identifier_prefix                     = (known after apply)
      + instance_class                        = "db.t3.micro"
      + iops                                  = (known after apply)
      + kms_key_id                            = (known after apply)
      + latest_restorable_time                = (known after apply)
      + license_model                         = (known after apply)
      + listener_endpoint                     = (known after apply)
      + maintenance_window                    = (known after apply)
      + master_user_secret                    = (known after apply)
      + master_user_secret_kms_key_id         = (known after apply)
      + monitoring_interval                   = 0
      + monitoring_role_arn                   = (known after apply)
      + multi_az                              = false
      + nchar_character_set_name              = (known after apply)
      + network_type                          = (known after apply)
      + option_group_name                     = (known after apply)
      + parameter_group_name                  = (known after apply)
      + password                              = (sensitive value)
      + performance_insights_enabled          = false
      + performance_insights_kms_key_id       = (known after apply)
      + performance_insights_retention_period = (known after apply)
      + port                                  = (known after apply)
      + publicly_accessible                   = true
      + replica_mode                          = (known after apply)
      + replicas                              = (known after apply)
      + resource_id                           = (known after apply)
      + skip_final_snapshot                   = true
      + snapshot_identifier                   = (known after apply)
      + status                                = (known after apply)
      + storage_encrypted                     = false
      + storage_throughput                    = (known after apply)
      + storage_type                          = (known after apply)
      + tags_all                              = (known after apply)
      + timezone                              = (known after apply)
      + username                              = "database_username"
      + vpc_security_group_ids                = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.
```

This tells us which resources we declared and that are going to be created, changed or destroyed using the code we wrote.

[As Bane said in The Dark Knight Rises: "It doesn't matter who we are, what matters is our plan"](https://www.youtube.com/watch?v=RBE4fZLqrVQ), after reviewing and check the plan Terraform showed us, it is time to apply it:

```bash
terraform apply -auto-approve
``` 

This will apply the plan and should take like three minutes to AWS make the health check of the instance available.

```bash
aws_db_instance.my-free-rds-db-instance: Creating...
aws_db_instance.my-free-rds-db-instance: Still creating... [10s elapsed]
aws_db_instance.my-free-rds-db-instance: Still creating... [20s elapsed]
...
```

If you log in to your AWS account and go to RDS, you'll see that the instance is going to be in the creating state:

![my-free-rds-db-instance showing the creating state in AWS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ohi7xrjm27kmts8eng5v.png)

After a few minutes, it will be available and Terraform will tell us:

```bash
...
aws_db_instance.my-free-rds-db-instance: Still creating... [3m30s elapsed]
aws_db_instance.my-free-rds-db-instance: Still creating... [3m40s elapsed]
aws_db_instance.my-free-rds-db-instance: Creation complete after 3m49s [id=db-UZADWSYL266LYV2W6D6H7CUM4U]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
```

![my-free-rds-db-instance showing the available state in AWS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r91gkfe9zwqantye8vh0.png)

You may check the endpoint to the instance via AWS Console or just accessing your `terraform.tfstate` and looking for the `address`. To connect, you just connect like it was a usual database. 

> Remember to use SSL to make the connection, otherwise you'll get an error. You could also add a parameter group in your Terraform files to make the `force_ssl` to 0 in your db instance.

![Connection test of my-free-rds-db-instance working](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/clt4twt3e4rocp2xj5lk.png)

## Conclusion

This was a simple guide to introduce the readers on Terraform and also help them to create a free PostgreSQL RDS instance in AWS.

I hope this helps you to spin up a free PostgresQL to simulate a production-ready application and learn a bit more about using Terraform.

This project code is available on Github: [terraform-aws-free-rds](https://github.com/ericbsantana/terraform-aws-free-rds)

{% github ericbsantana/terraform-aws-free-rds %}