Another view on Terraform DRY: Part 1

As the size of the infrastructure grows and terraform code becomes more complex, maybe you have several environments spanning multiple regions.

Having to maintain all of this code between environments becomes more error prone, and you will end up with staging environment that are different than the production ones.

Also, is a really good practice to refrain yourself to run this code on your own machine and staple it to CI/CD like Gitlab-CI or Jenkins. Since my CI workers are ephemeral I won’t use Terraform Workplaces.

Consider the following file structure, which defines two environments (stg, prd) and two regions (us-east-1, eu-west-1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vpc
├── vars
│   ├── stg
│   │   ├── us-east-1
│   │   │   ├── remote_backend.tfvars
│   │   │   └── terraform.tfvars
│   │   ├── eu-west-1
│   │   │   ├── remote_backend.tfvars
│   │   │   └── terraform.tfvars
│   │   └── terraform.tfvars
│   ├── prd
│   │   ├── us-east-1
│   │   │   ├── remote_backend.tfvars
│   │   │   └── terraform.tfvars
│   │   ├── eu-west-1
│   │   │   ├── remote_backend.tfvars
│   │   │   └── terraform.tfvars
│   │   └── terraform.tfvars
│   └── terraform.tfvars
├── main.tf (locals)
├── provider.tf (provider definitions)
├── variables.tf
└── vpc.tf (actual terraform code)

The contents of each environment will be identical since they all use the same .tf files, except perhaps for a few settings that will be defined with variables (e.g. the prod environment may run bigger or more servers and staging could have a smaller vpc length).

Each region and environment will have their own Terraform State Files (or tfstate) defined in remote_backend.tfvars file. You can pass the flag -backend-config=... during terraform init to setup your remote backend. Keep in mind that since our CI runners are ephemeral we are always starting fresh with no .terraform directory.

1
terraform init -backend-config ${VAR_DIR}/${ENV}/${REGION}/remote_backend.tfvars

The contents of remote_backend.tfvars will look like this:

1
2
3
4
5
6
➜ cat vpc/vars/prd/us-east-1/remote_backend.tfvars

bucket = "somebucket-for-terraform"
key = "prd/us-east-1/vpc.tfstate"
region = "us-east-1"
dynamodb_table = "terraform_statelock"

With each level of terraform.tfvars, will overwrite the previous ones. This means that the lower terraform.tfvars will take over the previous ones.

This will allow us to configure variables that are global to all environments and regions and have variables that are exclusive to a single environment.

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
├── vars
│   ├── stg
│   │   ├── us-east-1
│   │   │   ├── remote_backend.tfvars
│   │   │   └── terraform.tfvars <------- Regional variables (lower tier)
│   │   ├── eu-west-1
│   │   │   ├── remote_backend.tfvars
│   │   │   └── terraform.tfvars
│   │   └── terraform.tfvars <------- General environment variables (mid tier)
│   ├── prd
│   │   ├── us-east-1
│   │   │   ├── remote_backend.tfvars
│   │   │   └── terraform.tfvars
│   │   ├── eu-west-1
│   │   │   ├── remote_backend.tfvars
│   │   │   └── terraform.tfvars
│   │   └── terraform.tfvars
│   └── terraform.tfvars <------- Global variables (top tier)

This will allow me to have 3 tiers of tags variables, very useful when you want to have all your resources properly tagged for cost analysis.

1
2
3
4
5
6
7
8
9
10
➜  cat vpc/vars/terraform.tfvars
# Global TF vars

foo = "bar"

tags_global = {
project_id = "bla"
service_name = "eks"
team_name = "someteam"
}
1
2
3
4
5
6
7
8
9
10
➜  cat vpc/vars/stg/terraform.tfvars
# Environment TF vars

foo = "bar1"

environment = "stg"

tags_env = {
env = "stg"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
➜  cat vpc/vars/stg/us-east-1/terraform.tfvars
# Region TF vars

foo = "bar2"

aws_region = "us-east-1"

cidr_block = "10.80.0.0/21"

azs = ["a", "b", "c"]

newbits_public = "5" #/23
newbits_private = "2" #/26

vpc_dns_server = "AmazonProvidedDNS"

tags_region = {
region = "us-east-1"
}

In the case of the variable foo the value that will take precedence is bar2

To load all these variables we will need to pass terraform plan action the variables in the order we want, e.g.

1
terraform plan -var-file ./vars/terraform.tfvars -var-file ./vars/stg/terraform.tfvars -var-file ./vars/stg/us-east-1/terraform.tfvars -out terraform.tfplan

To apply my plan I only need to point terraform to the plan file, because the plan file already have all the necessary variables and configuration to work.

1
terraform apply terraform.tfplan