In this guide, we will explore provisioning an AWS RDS instance using Terraform. This concept is known as Infrastructure as code, i.e. provisioning infrastructure resources in as code.
Prerequisites
- AWS IaM user Access Key and Secret Key with permissions to create an RDS
- Terraform installed and added to path
- AWS Cli installed
Terraform
Terraform is an open source tool created by HashiCorp that allow you to define resources as code. Users define and provide data center infrastructure using a declarative configuration language known as HashiCorp Configuration Language, or optionally JSON.
Terraform suppport multiple clouds such as AWS, GCP, Azure, Digital Ocean, Alibaba, IBM Cloud, etc.
RDS
RDS is a relational database.
Amazon Relational Database Service is a distributed relational database service by Amazon Web Services. It is a web service running “in the cloud” designed to simplify the setup, operation, and scaling of a relational database for use in applications.
Amazon RDS provides DB engine types such as Amazon Aurora, PostgreSQL, MySQL, MariaDB, Oracle Database, and SQL Server.
Installing Terraform
Terraform is available as a binary for most distributions – Linux, Windows, Mac and BSD. It can be downladed as a binary path and added to path. The simplest way to install it is by downloading the package for your os in the downloads page here https://www.terraform.io/downloads.html.
More information on installation can be found on this page https://learn.hashicorp.com/tutorials/terraform/install-cli.
Once terraform is installed and added to path, confirm that its working as expected by checking its version as shown below:
➜ terraform --version
Terraform v1.0.6
on linux_amd64
Installing awscli
The awscli
is a command line utility that allows you to manage aws resources. It is a written in python and is available as a PyPI. We can install it using pip like in this command:
sudo pip install awscli
Confirm that its working by checking its version:
➜ aws --version
aws-cli/2.2.11 Python/3.8.8 Linux/4.18.0-305.12.1.el8_4.x86_64 exe/x86_64.centos.8 prompt/off
Configuring awscli profile
We can configure an AWS profile to be used for authentication. An AWS profile allows AWS access either from the cli or from terraform. An AWS profile contains the AWS Access Key, The AWS access Secret and a default region.
To create a profile, use the aws configure
command and enter the key id, access key and the region.
Example:
➜ aws configure
AWS Access Key ID [None]: key-here
AWS Secret Access Key [None]: secret-here
Default region name [None]: us-east-1
Default output format [None]: json
If you have another profile defined in ~/.aws/config
, you can use its credentials using:
export AWS_PROFILE=my-creds
Alternatively
If you do not want to create a default profile, you can use the environment variables to set the credentials. Export them like in this example
export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
export AWS_DEFAULT_REGION=us-west-2
Terraform code
The terraform code consist of the following: Provider definitions, variable definitions, data difinations, resources definitions and outout definitions.
Variables
Input variables serve as parameters for a Terraform module, allowing aspects of the module to be customized without altering the module’s own source code, and allowing modules to be shared between different configurations.
We can pass the access key, secret key, and the region of AWS as variables so we don’t have to hard code into the file.
This is out variable definitionas
variable "aws_access_key" {
type = "string"
}
variable "aws_secret_key" {
type = "string"
}
Provider
A Terraform Provider represents an integration that is responsible for understanding API interactions with the underlying infrastructure, such as a public cloud service (AWS, GCP, Azure), a PaaS service (Heroku), a SaaS service (DNSimple, CloudFlare), or on-prem resources (vSphere).
A plugin will be installed using terraform to communicate with the respective providers.
This is our definition of AWS provider.
provider "aws" {
access_key = var.aws_access_key
secret_key = var.aws_secret_key
region = "us-west-2"
}
Data
A data block requests that Terraform read from a given data source (“aws_vpc”) and export the result under the given local name (“our_vpc”). The name is used to refer to this resource from elsewhere in the same Terraform module, but has no significance outside of the scope of a module.
The RDS instance we launch will reside in subnets inside a vpc. To query the VPC id given the vpc ID and subnets inside it, use this code:
data "aws_vpc" "our_vpc" {
id = "vpc-xxxxxxx"
}
data "aws_subnet_ids" "subnet_ids" {
vpc_id = data.aws_vpc.our_vpc.id
tags = {
Name = "private-*"
}
}
Resources
Now that we have initialized our code with variables, providers and data, let’s now define resources that will create the AWS resources required.
DB Subnet group
An RDS Subnet Group is a collection of subnets that you can use to designate for your RDS database instance in a VPC. The database within your VPC will use the Subnet Group and the preferred Availability Zone to select a subnet and an IP address within that subnet.
Let us define a subnet group for our RDS:
resource "aws_db_subnet_group" "prod_mariadb" {
name = "prod-mariadb"
subnet_ids = data.aws_subnet_ids.subnet_ids.ids
}
DB Instance
This the database definition. This will launch the AWS RDS instance with te given values:
resource "aws_db_instance" "the_db" {
engine = "mariadb"
engine_version = "10.5"
instance_class = "db.t3.medium"
name = "prodmariadb"
identifier = "prod-mariadb"
username = "root"
password = "<some-secure-password>"
parameter_group_name = "default.mariadb10.5"
db_subnet_group_name = aws_db_subnet_group.prod_mariadb.name
vpc_security_group_ids = [aws_security_group.allow_mariadb.id]
skip_final_snapshot = true
allocated_storage = 50
max_allocated_storage = 1000
}
name
– default database nameidentifier
– A unique name for the DB Instanceengine_version
– DB version to use
Security Group
A security group acts as a virtual firewall for your rds instance to control inbound and outbound traffic.
Let us define one for our instance allowing only traffic within the VPC:
resource "aws_security_group" "allow_mariadb" {
name = "prod-mariadb-rds-sg"
description = "Allow TLS inbound traffic"
vpc_id = data.aws_vpc.our_vpc.id
ingress {
description = "Mariadb Access from within the VPC"
from_port = 3306
to_port = 3306
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "Allow access to prod mariadb rds"
}
}
Outputs
Terraform output values allow you to export structured data about your resources. You can use this data to configure other parts of your infrastructure with automation tools, or as a data source for another Terraform workspace. Outputs are also necessary to share data from a child module to your root module.
Let us define outputs to print out our RDS instance.
output "this_db_name" {
value = aws_db_instance.the_db.name
}
output "this_db_instance_address" {
value = aws_db_instance.the_db.address
}
output "this_db_instance_arn" {
value = aws_db_instance.the_db.arn
}
output "this_db_instance_domain" {
value = aws_db_instance.the_db.domain
}
output "this_db_instance_endpoint" {
value = aws_db_instance.the_db.endpoint
}
output "this_db_instance_status" {
value = aws_db_instance.the_db.status
}
Launch instance from snapshot
If we want to launch the instance from another instance’s snapshot, we can query that instance snapshot then pass snapshot_identifier
when launching.
Query for latest snapshot for rds instance named prod-db-01
:
data "aws_db_snapshot" "prod_snapshot" {
most_recent = true
db_instance_identifier = "prod-db-01"
}
Then use it to provision new instance:
resource "aws_db_instance" "the_db" {
engine = "mariadb"
engine_version = "10.5"
instance_class = "db.t3.medium"
identifier = "prod-mariadb-snap-01"
username = "root"
password = "<some-secure-password>"
parameter_group_name = "default.mariadb10.5"
db_subnet_group_name = aws_db_subnet_group.prod_mariadb.name
vpc_security_group_ids = [aws_security_group.allow_mariadb.id]
skip_final_snapshot = true
snapshot_identifier = data.aws_db_snapshot.prod_snapshot.id
allocated_storage = 50
max_allocated_storage = 1000
}
Save this code in a fine with an extention .tf
then use these terraform commands to apply the changes:
terraform plan
terraform apply
Final code
This is the final code that will launch the instance in the subnets inside the VPC.
provider "aws" {
access_key = var.aws_access_key
secret_key = var.aws_secret_key
region = "us-west-2"
}
data "aws_vpc" "our_vpc" {
id = "vpc-xxxxxxx"
}
data "aws_subnet_ids" "subnet_ids" {
vpc_id = data.aws_vpc.our_vpc.id
tags = {
Name = "private-*"
}
}
resource "aws_db_subnet_group" "prod_mariadb" {
name = "prod-mariadb"
subnet_ids = data.aws_subnet_ids.subnet_ids.ids
}
resource "aws_db_instance" "the_db" {
engine = "mariadb"
engine_version = "10.5"
instance_class = "db.t3.medium"
name = "prodmariadb"
identifier = "prod-mariadb"
username = "root"
password = "<some-secure-password>"
parameter_group_name = "default.mariadb10.5"
db_subnet_group_name = aws_db_subnet_group.prod_mariadb.name
vpc_security_group_ids = [aws_security_group.allow_mariadb.id]
skip_final_snapshot = true
allocated_storage = 50
max_allocated_storage = 1000
}
resource "aws_security_group" "allow_mariadb" {
name = "prod-mariadb-rds-sg"
description = "Allow TLS inbound traffic"
vpc_id = data.aws_vpc.our_vpc.id
ingress {
description = "Mariadb Access from within the VPC"
from_port = 3306
to_port = 3306
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "Allow access to prod mariadb rds"
}
}
output "this_db_name" {
value = aws_db_instance.the_db.name
}
output "this_db_instance_address" {
value = aws_db_instance.the_db.address
}
output "this_db_instance_arn" {
value = aws_db_instance.the_db.arn
}
output "this_db_instance_domain" {
value = aws_db_instance.the_db.domain
}
output "this_db_instance_endpoint" {
value = aws_db_instance.the_db.endpoint
}
output "this_db_instance_status" {
value = aws_db_instance.the_db.status
}