DEV Community

Paul Eggerling-Boeck
Paul Eggerling-Boeck

Posted on • Originally published at Medium on

Create an AWS RDS instance with Terraform

Terraform is an open-source infrastructure as code (IaC) tool that allows developers and system administrators to manage infrastructure resources in a declarative and version-controlled manner. With Terraform, you can define your infrastructure as a set of declarative configuration files, which can be used to create, update, and delete resources across multiple cloud providers and on-premises systems. By using Terraform, you can automate the process of provisioning and managing infrastructure, reducing the risk of manual errors and increasing the speed and reliability of your deployments. In this article, I’ll walk you through a simple Terraform configuration for creating a minimal MySQL RDS database instance at AWS.

Image of a Hashicorp Terraform logo and an AWS RDS logo

As with my previous article, my goal is to keep things simple and give you a way to experiment with AWS services in the easiest, and most affordable (i.e free) way possible. To that end, I’ll show you the steps you can follow to create a minimal MySQL RDS instance in the default VPC for your AWS account using an instance type that qualifies for the AWS free tier pricing model. This instance will be publicly available so that you don’t have to complicate things by creating additional infrastructure to make it private, yet still be able to connect to it remotely.

Getting Started

Install and configure authentication for the AWS CLI. I have found that the easiest way to manage and work with AWS CLI authentication credentials is to use named profiles to label your access keys.

If you use named profiles, you can simply set the AWS_PROFILE environment variable to the appropriate profile name prior to running Terraform. Then Terraform will use the access key specified for the given profile to authenticate to AWS.

The IAM user associated with your access key (i.e. the user running Terraform) will need to have the necessary IAM permissions to manage the resources that Terraform is configured to manage. To keep it simple, I generally run Terraform as an IAM user with the AdministratorAccess policy.

If you like, you can fork my GitHub repository for this article to get access to the files I’ll describe below.

Terraform Configuration

First, create a main.tf file to declare the terraform provider for AWS and tell Terraform which cloud provider it will be working with. With this simple provider declaration, Terraform will download and use the latest version of the AWS provider. Note: there is nothing significant about Terraform file names other than the extension. Terraform will look for, and use, any files in the current directory that end with .tf.

// main.tf

provider "aws" {
}
Enter fullscreen mode Exit fullscreen mode

Next, create an rds.tf file to declare the resources you’ll need for the RDS instance. I want to point out four important things about the declarations you see below:

  1. You’ll need to declare anaws_security_group resource in order to make your RDS instance publicly accessible. That security group needs in inbound/ingress rule defined to allow internet traffic to access the default MySQL port, 3306. Despite it’s name, the publicly_accessible = true attribute of the aws_db_instance resource isn’t enough to do the trick on it’s own.
  2. In order for Terraform to destroy these resources without additional intervention or configuration, you’ll need to set the attribute skip_final_snapshot = true. For this simple example (and to remain free) you don’t want to be dealing with RDS snapshots.
  3. Notice that this declaration uses the Terraform variablesvar.db-username and var.db-password to define the database username and password respectively. These allow you to enter this sensitive information by responding to console prompts, or on the command line when you run Terraform commands rather than hard-coding them in the configuration files. It also requires a bit of additional Terraform configuration as you’ll see.
  4. This is an extremely simplistic example. For all the details on how an AWS RDS instance can be declared with Terraform, see the Terraform documentation.
// rds.tf

resource "aws_security_group" "example" {
  name_prefix = "example-"
  ingress {
    from_port = 0
    to_port = 3306
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_db_instance" "example" {
  engine = "mysql"
  db_name = "example"
  identifier = "example"
  instance_class = "db.t2.micro"
  allocated_storage = 20
  publicly_accessible = true
  username = var.db-username
  password = var.db-password
  vpc_security_group_ids = [aws_security_group.example.id]
  skip_final_snapshot = true

  tags = {
    Name = "example-db"
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, create a variables.tf file to declare Terraform variables for the database username and password so that you don’t have to put the actual values in your configuration.

// variables.tf

variable "db-username" {
  type = string
}
variable "db-password" {
  type = string
}
Enter fullscreen mode Exit fullscreen mode

For this simple example, you don’t really need to separate your Terraform files like this, but it’s good to get into the habit, so that’s how I’ve set it up. As with any other software development you do, it’s best to break things down into small, easy to understand components.

Running Terraform

Terraform keeps information about what it thinks the current state of your resources is. It will compare the state of the resources at your configured provider (e.g. AWS) with the current state of the .tf files in your project and attempt to make changes so that the resources in your AWS account match what you have declared in your project files. Terraform only knows about the resources you have declared in your .tf files. It will not attempt to identify or manage any resources you have manually created in the AWS console. That’s an important point to remember as it can lead to a fair amount of confusion once your AWS account starts to grow and have a bunch of resources. There are multiple ways that Terraform can manage resource state. In the interest of keeping things simple, this example will allow Terraform to manage state locally. You can read the Terraform documentation about the importance of resource state.

The first thing you need to do is tell Terraform to initialize the resource state by running the terraform init command. You should run this command from the directory where your .tf files reside. For this example, you should run all of the Terraform commands from that same directory. You’ll notice that a new hidden folder named .terraform and a new file named terraform.tfstate exist after running the init command.

Though not required, it is a best practice to review the plan of what modifications Terraform will make when the configuration is applied before actually applying them. You can do this by running the terraform plan command. The plan output will display a detailed listing of the changes that Terraform will make when the apply command is run followed by a summary giving the counts of resources to be added, changed, and destroyed. Initially, the output will look something like what you see below (Note that you’ll be prompted for db username and password during the plan execution as well).

var.db-password
  Enter a value: ********

var.db-username
  Enter a value: ********

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.example will be created
  + resource "aws_db_instance" "example" {
      + address = (known after apply)
      + allocated_storage = 20
      + apply_immediately = false
      + arn = (known after apply)
      + auto_minor_version_upgrade = true
      + availability_zone = (known after apply)
      + backup_retention_period = (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 = "example"
      + db_subnet_group_name = (known after apply)
      + delete_automated_backups = true
      + endpoint = (known after apply)
      + engine = "mysql"
      + engine_version = (known after apply)
      + engine_version_actual = (known after apply)
      + hosted_zone_id = (known after apply)
      + id = (known after apply)
      + identifier = (known after apply)
      + identifier_prefix = (known after apply)
      + instance_class = "db.t2.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 = (known after apply)
      + name = (known after apply)
      + 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_throughput = (known after apply)
      + storage_type = (known after apply)
      + tags = {
          + "Name" = "example-db"
        }
      + tags_all = {
          + "Name" = "example-db"
        }
      + timezone = (known after apply)
      + username = "paulboeck"
      + vpc_security_group_ids = (known after apply)
    }

  # aws_security_group.example will be created
  + resource "aws_security_group" "example" {
      + arn = (known after apply)
      + description = "Managed by Terraform"
      + egress = (known after apply)
      + id = (known after apply)
      + ingress = [
          + {
              + cidr_blocks = [
                  + "0.0.0.0/0",
                ]
              + description = ""
              + from_port = 0
              + ipv6_cidr_blocks = []
              + prefix_list_ids = []
              + protocol = "tcp"
              + security_groups = []
              + self = false
              + to_port = 3306
            },
        ]
      + name = (known after apply)
      + name_prefix = "example-"
      + owner_id = (known after apply)
      + revoke_rules_on_delete = false
      + tags_all = (known after apply)
      + vpc_id = (known after apply)
    }

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

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
Enter fullscreen mode Exit fullscreen mode

As you saw, when you run Terraform commands, you’ll be prompted to enter values for any declared variables. Alternatively, you could pass those variables on the Terraform command line like this: terraform -var="db-username=someusername" -var="db-password=somepassword".

Next you should run terraform apply. Again, you’ll be prompted to supply variable values. Then Terraform will show you the plan of what it’s about to do and prompt you for confirmation. If you enter yes, Terraform will go ahead and apply the changes in the plan. You should see Terraform output similar to what you see below. The RDS instance creation will take a few minutes and you’ll get a line of output every 10 seconds while that’s happening. I have removed those lines below and replaced them with ... for the sake of brevity.

aws_security_group.example: Creating...
aws_security_group.example: Creation complete after 3s [id=sg-09131bb0d66fcb405]
aws_security_group_rule.example: Creating...
aws_db_instance.example: Creating...
aws_security_group_rule.example: Creation complete after 0s [id=sgrule-1293966328]
aws_db_instance.example: Still creating... [10s elapsed]
...
aws_db_instance.example: Still creating... [3m30s elapsed]
aws_db_instance.example: Creation complete after 3m34s [id=terraform-20230402143333546100000002]

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Enter fullscreen mode Exit fullscreen mode

If you run terraform plan again immediately after a successful apply execution, you will see the output below because Terraform does not find any changes to be applied.

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.
Enter fullscreen mode Exit fullscreen mode

In order to delete the resources you just created from your AWS account, you can run the terraform destroy command. Terraform will again prompt you for variable values, display it’s plan, and prompt you for confirmation. You should see output similar to that below.

aws_db_instance.example: Destroying... [id=terraform-20230402200559875300000002]
aws_db_instance.example: Still destroying... [id=terraform-20230402200559875300000002, 10s elapsed]
...
aws_db_instance.example: Still destroying... [id=terraform-20230402200559875300000002, 2m30s elapsed]
aws_db_instance.example: Destruction complete after 2m31s
aws_security_group.example: Destroying... [id=sg-08e2c108f188c65e9]
aws_security_group.example: Destruction complete after 1s

Destroy complete! Resources: 2 destroyed.
Enter fullscreen mode Exit fullscreen mode

Disclaimer : I do not recommend creating a publicly accessible database instance for anything but the most trivial experiments. When creating an RDS instance that is intended to hold any sort of valuable or sensitive information, it should be locked down so that access to it can be controlled and limited to only those who need access. That sort of setup is beyond the scope of this example, however.

Update: I learned a useful Terraform feature while I was implementing JPA database access in my weather data collection application. I wanted an automated way to get the endpoint URL of my newly created RDS instance without logging in to the the AWS console. I found that if you create an output resource in one of your terraform configuration files (I called mine output.tf) You can get Terraform to print out some useful information. Here’s the output definition I used to get the RDS endpoint URL for the RDS instance created in the example above


output "rds_endpoint" {
  description = "The endpoint of the RDS instance"
  value = aws_db_instance.example.endpoint
}
Enter fullscreen mode Exit fullscreen mode

Including that bit of configuration will output something similar to this:

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

Outputs:

rds_endpoint = "example.clkntxi8yxif.us-east-2.rds.amazonaws.com:3306"
Enter fullscreen mode Exit fullscreen mode

Which is exactly what I was looking for. Now I can simply copy that endpoint value and put it in my Spring application.properties file and I’m off to the races.

Was this article helpful? Did you learn something? Was it worth your time, or a waste of time? I’d love to hear from you if you have questions or feedback on this article and/or if I can help you get past any stumbling blocks you may have encountered along the way!

Top comments (0)