7 Terraform tips for application modernization

Debra Miller
nClouds
Published in
7 min readJun 23, 2021

Here at nClouds, we help companies modernize their applications on AWS. Sometimes we are modernizing a workload already running on AWS, and sometimes we are migrating a workload to AWS. With each application modernization project, we learn something new that enables us to do the next one even more efficiently.

We rely on infrastructure as code (IAC) for provisioning resources. In my experience, there are two viable solutions for provisioning resources on AWS (I guess there are more, but these two options are your best bets): AWS CloudFormation and Terraform.

In this blog post, we’ll share some best practices around Terraform. Just like our other blog posts, most of the sharing comes from our experience in building infrastructure at scale across many clients. So, these tips are battle-tested. Hopefully, you’ll find them helpful.

In most application modernization projects, you are typically creating multiple environments, making the code more reusable, and increasing the feedback loop. The goal is to be able to test the code with confidence and get your product out faster.

When we talk about Terraform with clients, one of the first things that comes up is maintaining the Terraform state. Let’s begin with a tip on that topic.

Tip #1: Setting up Terraform remote state and locking with Amazon DynamoDB

While building your AWS infrastructure, a state file is created with the information of your infrastructure’s current state in a json format. It contains information such as all the resources created by Terraform and their attributes (such as id, tags, etc.).

By default, this file is stored on your local machine with the name terraform.tfstate. However, using a local file can cause issues when several people are working on the same infrastructure:

  • Are they all using the latest state file?
  • How can you avoid having different users applying changes simultaneously?

These issues are resolved when all team members are using Terraform remote state, where the state data is written to a remote data store. The solution includes using Amazon Simple Storage Service (Amazon S3) as the backend and an Amazon DynamoDB table to lock the state.

Here is the architecture for a Terraform remote state:

Let’s begin by creating a couple of AWS resources. Log in to the console and create the following:

  • Amazon Simple Storage Service (Amazon S3) bucket to store Terraform state files.
  • Amazon DynamoDB table to manage locks on the Terraform state files.

The Amazon S3 bucket and Amazon DynamoDB table need to be in the same AWS Region to manage the state and can have any name you want to use, as in the examples below. But it doesn’t restrict you from managing multiple workspaces in different AWS Region using that state, as long as you reference the correct S3 bucket and DynamoDB table in your code. The only restriction is that the Amazon DynamoDB table must have a partition key named LockID.

terraform {required_version = “~> 0.10”backend “s3”{bucket = “<YOUR_BUCKET_NAME>”region = “<YOUR_BUCKET_REGION>”key = “backend.tfstate”dynamodb_table = “<YOUR_DYNAMODB_TABLE_NAME>”}}

Once we have our backend.tf file set up, we can initialize Terraform with terraform init to sync with the Amazon S3 bucket and the Amazon DynamoDB table. If everything goes well, you will see the following output:

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.

Every time someone tries to apply changes, a lock will be set in the table, and no one else will be able to implement changes until the lock is released. This lock helps avoid having different users applying changes simultaneously.

Beware of this “gotcha”: To use a remote state, you need to have an Amazon S3 bucket and Amazon DynamoDB table already created.

Tip #2: Creating Terraform workspaces

It’s commonplace to have multiple environments such as PROD, QA, and STAGING with fundamentally the same structure (with minimal differences such as the size of instances, storage, etc.).

A mistake that is made frequently is replicating the same code on different folders and setting up the remote backend with different Amazon S3 buckets to isolate your state file. The best practice to isolate Terraform state is to create Terraform workspaces.

Initially, the backend has only one workspace, called “default,” so only one Terraform state is associated with that configuration. We can create more workspaces with the command `terraform workspace new `, isolating the state into a separate folder inside the Amazon S3 bucket so your bucket will look like this:

In this example, prod and qa are the names of our two workspaces. We can switch between workspaces using the command `terraform workspace select `.

Tip #3: Using the Terraform import command

You may have AWS resources already created manually and want Terraform to manage them. A good example is where Amazon Relational Database Service (Amazon RDS) already has data, and you don’t want to create a new Amazon RDS. In such cases, there’s a useful command: terraform import. With terraform import, you can tell Terraform to include a manually created resource. First, add the code of the resource in Terraform:

resource "aws_instance" "web" {   
ami = "ami-123456"
instance_type = "t2.micro"
tags = {
Name = "HelloWorld"
}
}

Once your Terraform code matches the resource you want to import, look at the Terraform documentation about the resource. In some cases, you only need the name or id of the resource to import it.

In our example, since the resource is an EC2 instance, we need the instance id:

terraform import aws_instance.web i-12345678

Tip #4: Using Terraform locals

Terraform local values (known as “locals”) let you interpolate values and add some logic as well. With locals, you can define values depending on your workspace. For example, if you want to have your environment in a different Region, you can define a map in your locals as follows:

locals = {region = {prod = “us-west-2”qa = “us-west-1”}environment = terraform.workspace}

Notice that the local environment is getting its value from the selected workspace. Set your provider the following way, and it will dynamically select the Region based on that workspace:

provider “aws” {version = “~> 1.56”region = local.region[local.environment]}

Tip #5: How to access outputs of another Terraform

When you have a management Virtual Private Cloud (VPC), and you want to have that VPC peered with your environments, you’ll need to access the outputs of another Terraform.

Suppose you have multiple infrastructures deployed, and you need some information from one to create a peering connection. You can use the root-level outputs of one or more Terraform configurations as input data for another configuration. In that case, you can access the outputs of the other Terraform state if they are both stored remotely on an Amazon S3 bucket using the data source remote state.

To access outputs of another Terraform, if the Terraform configuration you want to access looks like this:

terraform {backend = “s3”config = {bucket = “terraform-state”key = “backend.tfstate”encrypt = trueregion = “us-west-2”dynamodb_table = “dynamo”}}

then your data source must match with the values of that backend:

data “terraform_remote_state” “global” {backend = “s3”config = {bucket = “terraform-state”key = “backend.tfstate”encrypt = trueregion = “us-west-2”dynamodb_table = “dynamo”}}Once you have that setup, you can retrieve outputs like the following:output “vpc_id” {value = data.terraform_remote_state.global.vpc_id}

Tip #6: Resource targeting in Terraform

In cases when you don’t want to apply changes to all your modules, you can use a target flag to apply the changes to a specific resource or an entire module:

terraform apply — target=module.ec2

Tip #7: How to delete resources from state without deleting the resource on AWS

What do you do when you want to apply a change (such as changing the name of a bucket) but don’t want that change to delete a critical resource of your AWS infrastructure (such as Terraform deleting the old bucket, getting rid of all your files inside the old bucket, and creating a new one with the new name)? Use the command `terraform state rm` to detach the physical resource in AWS from your Terraform state:

terraform state rm aws_s3_bucket.foo

This solution will create a new resource without affecting the old one since Terraform will no longer manage the old one. Then create the new bucket, and you can move your files from the old one to the new one.

In conclusion:

I hope you find these seven Terraform tips for application modernization helpful. They will enable you to:

  • Reuse code.
  • Reduce time to deploy multiple infrastructures in AWS.
  • Avoid creating resources that already exist in AWS and are not captured in your current Terraform.
  • Avoid conflicts when using Terraform with multiple teams and environments in AWS.
  • Separate your Terraform state and infrastructure without changing anything in your code.
  • Improve your Terraform skills.

Need help with modernizing your applications? The nClouds team is here to help with that and all your AWS infrastructure requirements.

--

--

nClouds
nClouds

Published in nClouds

nClouds is an Amazon Web Services (AWS) Premier Consulting Partner, leading MSP, and award-winning provider of AWS and DevOps consulting and implementation services. We partner with customers to build and manage modern infrastructure solutions that deliver innovation faster.

Debra Miller
Debra Miller

Written by Debra Miller

Content Marketing & Social Media Manager at nClouds, writing compelling content for blogs, case studies, & social media, optimized for SEO, strategy, demand gen