Manage an RDS Instance using Terraform
Tags: iac, terraform, devops, aws
Published: 2023-02-19 09:35:16

I tried to manage an RDS instance using Terraform. I followed the [hashicorp.com tutorial](https://developer.hashicorp.com/terraform/tutorials/aws/aws-rds).

# Table Of Contents
* [Prerequisites](#chapter-0)
* [1. Clone the sample repository](#chapter-1)
* [2. Check resource configuration](#chapter-2)
* [3. Provision resources](#chapter-3)
* [4. Modify instance configuration](#chapter-4)
* [5. Create a Read Replica](#chapter-5)
* [6. Clean up infrastructure](#chapter-6)

# Prerequisites <a name="chapter-0"></a>
1. Terraform 0.14+ installed locally.
2. An AWS account with credentials configured for Terraform.
3. PostgreSQL 14.2+ installed.

# 1. Clone the sample repository <a name="chapter-1"></a>
Clone the repository from Harshicorp of Github.
```
% git clone https://github.com/hashicorp/learn-terraform-rds.git
Cloning into 'learn-terraform-rds'...
remote: Enumerating objects: 52, done.
remote: Counting objects: 100% (42/42), done.
remote: Compressing objects: 100% (25/25), done.
remote: Total 52 (delta 21), reused 30 (delta 17), pack-reused 10
Receiving objects: 100% (52/52), 16.13 KiB | 8.07 MiB/s, done.
Resolving deltas: 100% (21/21), done.
```
Change the directory to the installed repository.
```
cd learn-terraform-rds
```
# 2. Check resource configuration <a name="chapter-2"></a>
Use the less command to check resource configuration
```
% less main.tf
```
## Networking components
The VPC and the subnets are defined as the first resource by using the terraform-aws-vpc module.
Caution: For simplicity, the RDS instance is publicly accessible in this tutorial.
```
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "2.77.0"

  name                 = "education"
  cidr                 = "10.0.0.0/16"
  azs                  = data.aws_availability_zones.available.names
  public_subnets       = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"]
  enable_dns_hostnames = true
  enable_dns_support   = true
}
```
Memo:
```
A Terraform module is a group of Terraform resources that are packaged together in a reusable way. Modules are useful because they allow you to reuse your infrastructure configuration code, and reduce duplication of code and effort.

The terraform-aws-modules/vpc/aws module that's used in the configuration file is a module that creates an Amazon Virtual Private Cloud (VPC) in AWS. A VPC is a virtual network dedicated to your AWS account that you can use to launch AWS resources in a logically isolated manner.

The vpc module in the configuration file creates an AWS VPC with the specified cidr (network address range), public_subnets (public subnet addresses), and tags (metadata). The VPC is given a name of example-vpc. Once the module is declared, you can use the outputs of the module to connect other resources to the VPC, such as the RDS instance in the configuration file.

By using the VPC module, you can quickly create a VPC with a consistent configuration and eliminate the need to duplicate the VPC configuration across multiple Terraform projects.
```
## Subnet group
The next resource is an aws_db_subnet_group, which is a collection of subnets where your RDS instance can be provisioned. This subnet group uses the subnets created by the VPC module.
```
resource "aws_db_subnet_group" "education" {
  name       = "education"
  subnet_ids = module.vpc.public_subnets

  tags = {
    Name = "Education"
  }
}
```
This subnet group resource is an optional parameter in your aws_db_instance block shown below. Otherwise, Terraform will create your RDS instances in the default VPC.
## Database instance
Check the aws_db_instance configuration.
```
resource "aws_db_instance" "education" {
  identifier             = "education"
  instance_class         = "db.t3.micro"
  allocated_storage      = 5
  engine                 = "postgres"
  engine_version         = "14.1"
  username               = "edu"
  password               = var.db_password
  db_subnet_group_name   = aws_db_subnet_group.education.name
  vpc_security_group_ids = [aws_security_group.rds.id]
  parameter_group_name   = aws_db_parameter_group.education.name
  publicly_accessible    = true
  skip_final_snapshot    = true
}
```
As I mentioned earlier, the argument of publicly_accessible is set to true, which means that this DB is accessible from the public.[You can check all arguments on this page.](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_instance)
## Parameter group
Now check the aws_db_parameter_group.
```
resource "aws_db_parameter_group" "education" {
  name   = "education"
  family = "postgres14"

  parameter {
    name  = "log_connections"
    value = "1"
  }
}
```
You can specify a name for the db parameter group. Otherwise, Terraform will randomly assign a unique name. You do not need to set a custom DB parameter group. Terraform will use a default one, but the default parameter group cannot be modified later, so if you want to modify it, it is better to use a custom parameter group.
[You can check all arguments on this page.](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_parameter_group)
## Input variables
You can check the input variable by opening variables.tf file. The DB root user password is configured in the file.
```
% less variables.tf
variable "db_password" {
  description = "RDS root user password"
  sensitive   = true
}
```
Terraform can hide the password in the output file, but Terrafor stores it in the state file.
## Output variables
You can review the output file. It shows the details of the RDS that will be created.
```
output "rds_hostname" {
  description = "RDS instance hostname"
  value       = aws_db_instance.education.address
  sensitive   = true
}

output "rds_port" {
  description = "RDS instance port"
  value       = aws_db_instance.education.port
  sensitive   = true
}

output "rds_username" {
  description = "RDS instance root username"
  value       = aws_db_instance.education.username
  sensitive   = true
}
```
# 3.Provision resources <a name="chapter-3"></a>
First, set the db_password variable as an environment variable.
Then, initialize the terraform configuration. 
```
% export TF_VAR_db_password="hashicorp"
% terraform init
Initializing modules...
Downloading registry.terraform.io/terraform-aws-modules/vpc/aws 2.77.0 for vpc...
- vpc in .terraform/modules/vpc

Initializing the backend...

Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Installing hashicorp/aws v3.32.0...
- Installed hashicorp/aws v3.32.0 (signed by HashiCorp)

Terraform has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
```
Next, apply the configuration. When prompted to confirm, answer Yes.
After a few minutes, the RDS creation was complete.

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

Outputs:

rds_hostname = <sensitive>
rds_port = <sensitive>
rds_username = <sensitive>
```
Then you can try to connect to the RDS by a following command.
```
psql -h $(terraform output -raw rds_hostname) -p $(terraform output -raw rds_port) -U $(terraform output -raw rds_username) postgres
```
But, I have received the following error even though I have installed PostgreSQL.
```
psql -h $(terraform output -raw rds_hostname) -p $(terraform output -raw rds_port) -U $(terraform output -raw rds_username) postgres
zsh: command not found: psql
```
So I looked it up on Google. It seems like I need to specify the path in ~/.zshrc file.
If you get the same error, run the following command and Add the path
```
$ vim ~/.zshrc
export PATH="/opt/homebrew/opt/postgresql@15/bin:$PATH"
:wq!
```
Then, I run the psql command again to connect.
Password is "hashicorp"
After connecting the DB, you can create a database named "hashicorp" within this instance.
```
postgres=> CREATE DATABASE hashicorp;
CREATE DATABASE
```
You can verify that the database has been created by running the following command.
```
postgres-> \list
                                                 List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    | ICU Locale | Locale Provider |   Access privileges   
-----------+----------+----------+-------------+-------------+------------+-----------------+-----------------------
 hashicorp | edu      | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | 
 postgres  | edu      | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | 
 rdsadmin  | rdsadmin | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | rdsadmin=CTc/rdsadmin+
           |          |          |             |             |            |                 | rdstopmgr=Tc/rdsadmin
 template0 | rdsadmin | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | =c/rdsadmin          +
           |          |          |             |             |            |                 | rdsadmin=CTc/rdsadmin
 template1 | edu      | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | =c/edu               +
           |          |          |             |             |            |                 | edu=CTc/edu
(5 rows)
```
You can exit by the following command.
```
\q
```
# 4. Modify instance configuration <a name="chapter-4"></a>
Open main.tf and change the storage size from 5 to 10GB.
```
resource "aws_db_instance" "education" {
   name                   = "education"
   instance_class         = "db.t3.micro"
-  allocated_storage      = 5
+  allocated_storage      = 10
##...
}
```
Before you apply the change, you will need to add the apply_immediately argument to set it to true so that the RDS can be restarted.
```
resource "aws_db_instance" "education_replica" {
   name                   = "education-replica"
   identifier             = "education-replica"
   replicate_source_db    = aws_db_instance.education.identifier
   instance_class         = "db.t3.micro"
 + apply_immediately      = true
   publicly_accessible    = true
   skip_final_snapshot    = true
   vpc_security_group_ids = [aws_security_group.rds.id]
   parameter_group_name   = aws_db_parameter_group.education.name
}
```
Then, apply the change.
```
% terraform apply
data.aws_availability_zones.available: Reading...
module.vpc.aws_vpc.this[0]: Refreshing state... [id=vpc-0b256089f0091780c]
aws_db_parameter_group.education: Refreshing state... [id=education]
data.aws_availability_zones.available: Read complete after 0s [id=us-east-2]
aws_security_group.rds: Refreshing state... [id=sg-014424185901691c4]
module.vpc.aws_internet_gateway.this[0]: Refreshing state... [id=igw-0719c56f100ace0d7]
module.vpc.aws_route_table.public[0]: Refreshing state... [id=rtb-03d7c847b9a295364]
module.vpc.aws_subnet.public[1]: Refreshing state... [id=subnet-008ab5f5d5587f486]
module.vpc.aws_subnet.public[0]: Refreshing state... [id=subnet-062890da2dae35429]
module.vpc.aws_subnet.public[2]: Refreshing state... [id=subnet-02cb916ed21e23d79]
module.vpc.aws_route.public_internet_gateway[0]: Refreshing state... [id=r-rtb-03d7c847b9a2953641080289494]
module.vpc.aws_route_table_association.public[1]: Refreshing state... [id=rtbassoc-03ce6f881ca509cf7]
module.vpc.aws_route_table_association.public[0]: Refreshing state... [id=rtbassoc-0f0f18b8c334690d4]
module.vpc.aws_route_table_association.public[2]: Refreshing state... [id=rtbassoc-02bb355ff4c6d01f1]
aws_db_subnet_group.education: Refreshing state... [id=education]
aws_db_instance.education: Refreshing state... [id=education]

Terraform used the selected providers to generate the following execution plan. Resource
actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # aws_db_instance.education will be updated in-place
  ~ resource "aws_db_instance" "education" {
      ~ allocated_storage                     = 5 -> 10
        id                                    = "education"
        name                                  = ""
        tags                                  = {}
        # (49 unchanged attributes hidden)
    }

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_db_instance.education: Modifying... [id=education]
aws_db_instance.education: Still modifying... [id=education, 10s elapsed]
aws_db_instance.education: Still modifying... [id=education, 20s elapsed]
aws_db_instance.education: Still modifying... [id=education, 30s elapsed]
aws_db_instance.education: Modifications complete after 33s [id=education]

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

Outputs:

rds_hostname = <sensitive>
rds_port = <sensitive>
rds_username = <sensitive>
```
After completing the change, you can verify that it was applied correctly.
```
% terraform plan
data.aws_availability_zones.available: Reading...
aws_db_parameter_group.education: Refreshing state... [id=education]
module.vpc.aws_vpc.this[0]: Refreshing state... [id=vpc-0b256089f0091780c]
data.aws_availability_zones.available: Read complete after 1s [id=us-east-2]
aws_security_group.rds: Refreshing state... [id=sg-014424185901691c4]
module.vpc.aws_internet_gateway.this[0]: Refreshing state... [id=igw-0719c56f100ace0d7]
module.vpc.aws_subnet.public[0]: Refreshing state... [id=subnet-062890da2dae35429]
module.vpc.aws_subnet.public[2]: Refreshing state... [id=subnet-02cb916ed21e23d79]
module.vpc.aws_subnet.public[1]: Refreshing state... [id=subnet-008ab5f5d5587f486]
module.vpc.aws_route_table.public[0]: Refreshing state... [id=rtb-03d7c847b9a295364]
module.vpc.aws_route.public_internet_gateway[0]: Refreshing state... [id=r-rtb-03d7c847b9a2953641080289494]
module.vpc.aws_route_table_association.public[0]: Refreshing state... [id=rtbassoc-0f0f18b8c334690d4]
module.vpc.aws_route_table_association.public[1]: Refreshing state... [id=rtbassoc-03ce6f881ca509cf7]
module.vpc.aws_route_table_association.public[2]: Refreshing state... [id=rtbassoc-02bb355ff4c6d01f1]
aws_db_subnet_group.education: Refreshing state... [id=education]
aws_db_instance.education: Refreshing state... [id=education]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no
differences, so no changes are needed.
```
# 5. Create a Read Replica <a name="chapter-5"></a>
Open main.tf file and add the following block.
```
resource "aws_db_instance" "education_replica" {
   name                   = "education-replica"
   identifier             = "education-replica"
   replicate_source_db    = aws_db_instance.education.identifier
   instance_class         = "db.t3.micro"
   apply_immediately      = true
   publicly_accessible    = true
   skip_final_snapshot    = true
   vpc_security_group_ids = [aws_security_group.rds.id]
   parameter_group_name   = aws_db_parameter_group.education.name
}
```
And add this argument in the primary instance block.
```
resource "aws_db_instance" "education" {
    name                      = "education"
    instance_class            = "db.t3.micro"
    allocated_storage         = 10
+   backup_retention_period   = 1
##...
}
```
Add this block into outputs.tf file.
```
output "rds_replica_connection_parameters" {
  description = "RDS replica instance connection parameters"
  value       = "-h ${aws_db_instance.education_replica.address} -p ${aws_db_instance.education_replica.port} -U ${aws_db_instance.education_replica.username} postgres"
}
```
Then apply the change with "terraform apply".
You will see the following message at the bottom of the screen.
```
aws_db_instance.education_replica: Creation complete after 11m47s [id=education-replica]

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

Outputs:

rds_hostname = <sensitive>
rds_port = <sensitive>
rds_replica_connection_parameters = "-h education-replica.cl6o1zzqtrp1.us-east-2.rds.amazonaws.com -p 5432 -U edu postgres"
```
Now you can connect to the read replica.
```
% psql $(terraform output -raw rds_replica_connection_parameters)
Password for user edu: 
```
List database to verify that the database has actually been created.
```
postgres=> \list
                                                 List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    | ICU Locale | Locale Provider |   Access privileges   
-----------+----------+----------+-------------+-------------+------------+-----------------+-----------------------
 hashicorp | edu      | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | 
 postgres  | edu      | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | 
 rdsadmin  | rdsadmin | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | rdsadmin=CTc/rdsadmin+
           |          |          |             |             |            |                 | rdstopmgr=Tc/rdsadmin
 template0 | rdsadmin | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | =c/rdsadmin          +
           |          |          |             |             |            |                 | rdsadmin=CTc/rdsadmin
 template1 | edu      | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | =c/edu               +
           |          |          |             |             |            |                 | edu=CTc/edu
(5 rows)
```
# 6. Clean up infrastructure <a name="chapter-6"></a>
You can delete the infrastructure by this command.
```
% terraform destroy 
....
module.vpc.aws_vpc.this[0]: Destroying... [id=vpc-0b256089f0091780c]
module.vpc.aws_vpc.this[0]: Destruction complete after 1s

Destroy complete! Resources: 15 destroyed.
```
Well, this is the end of this tutorial.
I'm getting used to Terraform, so I'll keep learning.
[You may refer to this site to review the argument of aws RDS.](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_instance)