Part 5: Terraform - Code Structuring

In this part of the series, we look at approaches to structuring terraform code.

As usual, if you haven't read the previous parts I have linked them below.

This post assumes requisite knowledge of networking. To maintain a better flow, it is also highly recommended to read the previous posts for a background on terraform.

In this post

We start with a clear statement. There is no one size fits all approach regarding how to structure a terraform code repository!

What factors can determine code structure?

Sometimes it may make sense to write all of the code in one file. Other times, it may provide flexibility to disperse the component configurations into different files in a directory. For more complex projects, it may make sense to have multiple directories and use a modules approach referencing each other.

The optimal structure to use will depend on a variety of factors like:

To illustrate this, let's look at the following practical scenario.

We need to create an AWS infrastructure which is represented by the diagram below:

There are two VPCs: application and database VPCs. Inside each VPC, we have two subnets. app-subnet-a hosts resources for one application and app-subnet-b hosts resources for a different application. The database counterparts of these applications reside in a different VPC and availability zone.

This is not an actual design. In the real world, you'd typically choose to deploy the application and database resources in a redundant manner and in multiple availability zones.

A couple of options we can consider to accomplish this traffic isolation requirement:

What are the steps we need to take to create this environment?

As you plan your infrastructure deployment project, you will start seeing dependencies or relationships among resources. For example, you cannot put an EC2 instance in a subnet that does not exist. You cannot peer a VPC with a VPC that doesn't exist. And so on.

Consider the provisioning steps for the application VPC i.e. app-vpc

These steps need to be repeated for the database VPC and its resources on the right i.e. db-vpc

Finally, once the two VPCs exist, a VPC peering connection needs to be created between them.

I think flow- and finite-state machine diagrams are cool. So, this is to pictorially represent a simplified version of what it would look like for a VPC and its resources.

As far as I've seen, AWS and other cloud providers do not describe them as such but it helps me to think of resources as Tier 1, Tier 2, Tier 3, etc.

Suggested Code Structure

As a general guideline, this hierarchy is one way the Terraform community suggests to organize files in a working directory.

      .
      ├── LICENSE
      ├── locals.tf
      ├── main.tf
      ├── outputs.tf
      ├── providers.tf
      ├── README.md
      ├── resources.tf
      ├── changing.tfvars
      ├── variables.tf

What does each file in the hierarchy do or contain?

If some of these descriptions are not quite clear yet, read on. They will be through the practical exercise.

What if we don't use this sort of file structuring? The configuration files can become bloated, introduces risk of error, and  gets costly to maintain in the long term. Do we have to use the exact same structure suggested above? No, we can leave out the files that we believe do not provide value based on our code planning and algorithmic thinking.

To build the AWS infrastructure in the diagram above, we can structure our files in different ways. This is where subjectivity plays a big role. For example:

      .
      ├── main.tf
      ├── outputs.tf
      ├── network.tf
      ├── compute.tf
      ├── changing.tfvars
      ├── variables.tf 
      .
      ├── LICENSE
      ├── main.tf
      ├── outputs.tf
      ├── providers.tf
      ├── README.md
      ├── tier1.tf
      ├── tier2.tf
      ├── tier3.tf
      ├── changing.tfvars
      ├── variables.tf

Let’s start very simple so the descriptions come across clearly and we build up the infrastructure block by block.

We zoom in on how to create the VPCs and the subnets inside them.

Provisioning parts of the infrastructure

variable "aws_region" {
description = "AWS region"
type = string
}
//App VPC Variables
variable "app_az" {
description = "Availability Zone"
type = string
}
variable "app_env_prefix" {
description = "tag, name, or description prefix"
type = string
}
variable "app_subnet_name" {
description = "subnet name"
type = list(string)
}
variable "app_vpc_cidr" {
description = "VPC CIDR block"
type = string
}
variable "app_subnet_cidr" {
description = "Subnet CIDR block"
type = list(string)
}
//DB VPC Variables
variable "db_az" {
description = "Availability Zone"
type = string
}
variable "db_env_prefix" {
description = "tag, name, or description prefix"
type = string
}
variable "db_subnet_name" {
description = "subnet name"
type = list(string)
}
variable "db_vpc_cidr" {
description = "VPC CIDR block"
type = string
}
variable "db_subnet_cidr" {
description = "Subnet CIDR block"
type = list(string)
}
# Declare terraform provider plugin 
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
# Provider
provider "aws" {
region = var.aws_region
}
// This section of code provisions the Application VPC
// env_prefix=app, az=us-east-1c, vpc_cidr=172.23.0.0/16
// subnet-a is 172.23.0.0/24, subnet-b is 172.23.1.0/24
# Create app VPC
resource "aws_vpc" "app-vpc" {
cidr_block = var.app_vpc_cidr
tags = {
Name = format("%s-%s", var.app_env_prefix, "vpc")
}
}
# Create app-subnet-a
resource "aws_subnet" "app-subnet-a" {
vpc_id = aws_vpc.app-vpc.id
cidr_block = var.app_subnet_cidr[0]
availability_zone = var.app_az
tags = {
Name = format("%s-%s", var.app_env_prefix, var.app_subnet_name[0])
}
}
# Create app-subnet-b
resource "aws_subnet" "app-subnet-b" {
vpc_id = aws_vpc.app-vpc.id
cidr_block = var.app_subnet_cidr[1]
availability_zone = var.app_az
tags = {
Name = format("%s-%s", var.app_env_prefix, var.app_subnet_name[1])
}
}
// This section of code is for the Database VPC
// env_prefix=db, az=us-east-1d, vpc_cidr=172.24.0.0/16
// subnet-a is 172.24.0.0/24, subnet-b is 172.24.1.0/24
# Create db VPC
resource "aws_vpc" "db-vpc" {
cidr_block = var.db_vpc_cidr
tags = {
Name = format("%s-%s", var.db_env_prefix, "vpc")
}
}
# Create db-subnet-a
resource "aws_subnet" "db-subnet-a" {
vpc_id = aws_vpc.db-vpc.id
cidr_block = var.db_subnet_cidr[0]
availability_zone = var.db_az
tags = {
Name = format("%s-%s", var.db_env_prefix, var.db_subnet_name[0])
}
}
# Create db-subnet-b
resource "aws_subnet" "db-subnet-b" {
vpc_id = aws_vpc.db-vpc.id
cidr_block = var.db_subnet_cidr[1]
availability_zone = var.db_az
tags = {
Name = format("%s-%s", var.db_env_prefix, var.db_subnet_name[1])
}
}
aws_region    = "us-east-1"

app_az = "us-east-1c"
app_env_prefix = "app"
app_subnet_name = ["subnet-a", "subnet-b"]
app_vpc_cidr = "172.23.0.0/16"
app_subnet_cidr = ["172.23.0.0/24","172.23.1.0/24"]

db_az = "us-east-1d"
db_env_prefix = "db"
db_subnet_name = ["subnet-a", "subnet-b"]
db_vpc_cidr = "172.24.0.0/16"
db_subnet_cidr = ["172.24.0.0/24", "172.24.1.0/24"]

At this point, your working directory should look like this:

      .
      ├── main.tf
      ├── network.tf
      ├── changing.tfvars
      ├── variables.tf

Ok. let's run this and provision these VPCs and their subnets.

export AWS_ACCESS_KEY_ID="your_access_key"
export AWS_SECRET_ACCESS_KEY="your_secret_key"
export AWS_DEFAULT_REGION="aws_region"

The partial output will look like in the picture above. Terraform says it will add 6 resources i.e. two VPCs and four subnets. As you run the code, look through the output to see what values it will change.

If you review the code again, you can see that there are many lines that we can eliminate in order to optimize it. There are constant elements that lend themselves well to the usage of a locals.tf file. If we use list(string) type variables, we will have to rely on array references in the form of variable_name[0] or variable_name[1] to locate a value. This can make the code shorter but may reduce code readability in the long run.

So, in this case one subjective perspective could be to say "longer is better than an unreadable code." Trade offs like this (code-length vs code-readability) are what make code structures inevitably differ. It is not an exact science.

In upcoming posts, we will continue building out this topology to incorporate other terraform concepts and validate that we accomplish our stated traffic isolation objectives.



Tags: aws, terraform

← Back home