Cloud Connections

Cloud Connections

cloud engineering, automation, devops, systems architecture and more…

27 Oct 2020

Building Cloud Infra Using Terraform - Part 2 (RDS MySQL Module)

Today, I will be going into detail of how I implemented the RDS MySQL module for the example infrastructure that we saw in Part 1.

Just a quick refresh of that post, we decided to use the following custom modules to build out the infrastructure:

  • A web app service
  • A database service

We also covered the advantages of using this approach, and we will see this in action, with the actual implementation of the database service module.

I decided to go with an RDS MySQL instance to keep things simple. After all, why manage your own database when AWS will do it for you.

This post won’t go into much detail to what each code configuration is doing. I assume that you have had some hands-on experience with Terraform code. If you have done a few deploys, this should be familiar to you.

Modules from the Terraform Registry vs. Writing Your Own

There are 2 approaches when writing modules:

  • Writing out your own configuration with the resources available from the Terraform documentation. You would consider the various configurable options available on RDS and implement them accordingly. The module would then be made available for others to use.
  • Using an existing RDS module from the registry and customizing it to your needs. This would deploy the services and configure it, based on how you want your custom module to be consumed.

This is a common situation that comes up when writing any type of code. Whether you want to build out your own implementation or make use of a package and customize it to your needs.

Personally, I find that using an existing module from the registry or any other 3rd party is a better approach. They have already laid the groundwork to deploy it with consideration to multiple scenarios. They also make certain features and options are easily configurable, rather than having to figure it all out from the documentation. I believe this approach saves time and you can be deploying services faster with the confidence that they wouldn’t break anything (you should always test before applying it in production). If you notice a problem with the module, the source code is publicly available, and you can contribute a fix.

That said, there are times when building out your own implementation is a good learning experience. It helps to understand in details the level of work that goes into developing one of these public modules. There are also concerns of dependence on external packages, so some prefer to have direct control over what plugins/modules they use within their code.

MySQL Database Module

Ok, enough concepts and theory. Let’s get to some real work.

Deploy Notes

rds_deploy_diagram

This will be the components that make up the database service.

  • The RDS instance is deployed to the default VPC subnet.
  • An RDS security group is created and attached to this instance. Other modules can use this security group’s ID to add ingress rules that allow access to the instance.

Code Repository

We will need to keep things under version control, so start by creating a public Git repository for your modules. If using Github, clone the repository to your local folder.

git clone <link_to_my_repo>
cd <my_repo_folder>

For the directory structure, I followed the convention used by most Terraform modules and configurations. This is totally up to you, as there are some who categorize based on level of service or type of infrastructure etc.

muhannad@LA04:/e/PROJECTS/terraform-example-module $ tree -d
.
|-- examples # example configurations of how to use your custom module
|   `-- mysql-db
`-- modules # actual configuration for the custom module
    `-- mysql-db

Example Configuration

Before writing out the actual mysql-db module, write an example configuration of how this module can be used by others.

Writing an example configuration more like a TDD approach to writing infrastructure-as-code. We have a clear idea of:

  • The inputs and outputs required by the module.
  • An example of how to use the module in a configuration.
  • A configuration that we can use to test during module development.

Create a folder for mysql-db in the examples folder.

Create a main.tf file

# examples/mysql-db/main.tf

terraform {
  required_version = ">= 0.13"
}

provider "aws" {
  region = "us-east-1"
}

module "db" {
  source = "../../modules/mysql-db"

  identifier = var.environment

  name     = var.db_name
  username = var.db_user
  password = var.db_pass
  port     = 3306
}

I wanted to keep it very simple for the user configuration, so that a few inputs are required.

Create a variables.tf file to get the inputs.

# examples/mysql-db/variables.tf

variable "environment" {
  description = "The deploy environment."
  type        = string
}

variable "db_name" {
  description = "The name of the database to create."
  type        = string
}

variable "db_user" {
  description = "The administrator username for the database."
  type        = string
}

variable "db_pass" {
  description = "The password for the administrator user of the database."
  type        = string
}

With this configuration, the mysql-db module should be able to deploy an RDS instance.

Module Configuration

Let’s see how we can implement it in the actual module.

We will be importing the RDS module from the public Terraform registry.

This module has a ton of customizable options that you can use. This is just the basic configuration (with defaults) to get a working instance deployed (enough for a demo, not for production use though).

In the modules folder we create a directory for the mysql-db module. Create a main.tf file.

# modules/mysql-db/main.tf

terraform {
  required_version = ">= 0.12"
}

data "aws_vpc" "default" {
  default = true
}

module "db" {
  source  = "terraform-aws-modules/rds/aws"
  version = "~> 2.0"

  identifier = var.identifier

  engine            = "mysql"
  engine_version    = "5.7.19"
  instance_class    = "db.t2.micro"
  allocated_storage = 10

  name     = var.name
  username = var.username
  password = var.password
  port     = var.port

  maintenance_window = "Fri:20:00-Fri:21:00"
  backup_window      = "22:00-23:00"

  backup_retention_period = 0

  # DB subnet group
  create_db_subnet_group = false
  db_subnet_group_name   = "default"

  # DB Security group
  vpc_security_group_ids = [aws_security_group.db.id]

  # DB parameter group
  create_db_parameter_group = false
  parameter_group_name      = "default.mysql5.7"

  # DB option group
  create_db_option_group = false
  option_group_name      = "default:mysql-5-7"
}

resource "aws_security_group" "db" {
  name = "db-sg-${var.identifier}"
}

resource "aws_security_group_rule" "allow_all_outbound" {
  type              = "egress"
  security_group_id = aws_security_group.db.id

  from_port   = 0
  to_port     = 0
  protocol    = "-1"
  cidr_blocks = ["0.0.0.0/0"]
}

Next, create a variables.tf to map the input variables from the example to the parameters in the RDS module.

# modules/mysql-db/variables.tf

variable "identifier" {
  description = "The database identifier, here used as deploy environment."
  type        = string
}

variable "name" {
  description = "The name of the database to create."
  type        = string
}

variable "username" {
  description = "The administrator username for the database."
  type        = string
}

variable "password" {
  description = "The password for the administrator user of the database."
  type        = string
}

variable "port" {
  description = "The port for database service."
  type        = number
  default     = 3306
}

Finally, we have to configure the outputs that can be accessed by the user (example) configuration. The user would need to know the address and port of the database deployed by this module. These need to be set as outputs in outputs.tf.

Since this module makes use of a public module, it provides multiple outputs. Refer to the documentation to find out what values you require.

# modules/mysql-db/outputs.tf

output "db_address" {
  description = "The address of the database."
  value       = module.db.this_db_instance_address
}

output "db_port" {
  description = "The port of the database service."
  value       = module.db.this_db_instance_port
}

output "db_security_group_id" {
  description = "The security group ID of the database service."
  value       = aws_security_group.db.id
}

Let’s go back to the example configuration now. The outputs need to also be defined in the example configuration, so that the user can get the details of the database deployed.

Since we already set the output values in the module, these can be accessed by the example configuration.

Create an outputs.tf file.

# examples/mysql-db/outputs.tf

output "db_address" {
  description = "The address of the database."
  value       = module.db.db_address
}

output "db_port" {
  description = "The port of the database service."
  value       = module.db.db_port
}

output "db_security_group_id" {
  description = "The ID of the database security group."
  value       = module.db.db_security_group_id
}

Here, we are referring to the output names provided by our own module (not the public module)

It is important to understand that the outputs are available to child modules only if you define them in the output configuration.

  • Public module output -> custom module output -> user configuration output
    module_outputs

Testing

To find out if the mysql-db module that we wrote works, we need to test it. Good thing we already wrote an example configuration, right? 😄

In the example folder for mysql-db, create a testing.tfvars with the inputs to the module.

# examples/mysql-db/testing.tfvars

environment = "testing"
db_name     = "test"
db_user     = "testuser"
db_pass     = "testpassword"

Make sure you add this file and couple of others to your .gitignore. You don’t want to check-in sensitive information that might be in these files (such as database credentials, state information) to version control.

It’s time to let Terraform do its magic.

terraform init
terraform apply -var-file="testing.tfvars"

It’ll be a while before the whole process is finished.

terraform_apply_run

Once done, head over to the AWS Console to check if the instance is up.

aws_console_rds

Hooray! You just deployed an RDS instance using your own custom module.

Commit and Save

Now that the module is ready to be used by an actual configuration, we can check-in the configuration to Git.

In the reposiroty directory run:

git add .
git commit -m "feat: mysql-db module"
git push

Cleanup

Make sure you destroy the test instances (to avoid running a huge bill of course 😛). Terraform makes this very easy.

In the mysql-db example directory run:

terraform destroy

Next Steps

Congratulations! You’ve made it this far. Luckily, that’s it for now.

We have built a custom module that can deploy an RDS MySQL instance. We also wrote an example configuration for that custom module that can be used as reference by others.

However, there are a lot more things we need to do before we use this custom module in an actual configuration. We shall continue with creating another module for the web application service in the next post.

I would appreciate any feedback on these articles, so that I can continue to write better. Feel free to send me message on Twitter or LinkedIn

Categories