One of the things that I find most interesting about using Hashicorp Terraform is the fact that it keeps state for the resources that you've specified.
What I especially like is that you can specify a backend to store this state, for instance in an s3 bucket.
Whats even better is that when you write a Terraform module you can provide context each time it's executed. For instance, Joe Bloggs has asked if he can have a VPC for testing out some new auto scaling functionality he needs to test. This can easily be achieved using the module that was written which allows us to provide context about where to store the state (as it needs to be unique so that we don't delete or create the wrong resources). By providing context at run time this allows us to perform this action for Joe Bloggs, Mr Smith and whomever would like the same resources, all with separate state files ;-) but not without a little help from our wrapper.
Our wrapper is nothing more than a simple dummy makefile. The makefile creates a reference to a bucket in a file called s3.backend.tf:
terraform {
backend "s3" {
bucket = "terraform-state-us-east-1-myfantasticTerraform"
key = "env-prefix/my-service-name/terraform.tfstate"
region = "us-east-1"
encrypt = "true"
}
}
Let's go through these in more detail. The following is the name of the S3 bucket that will store all your state files, regardless if they're for Joe Bloggs or Mr Smith.
bucket = "terraform-state-us-east-1-myfantasticTerraform"
The key is the path that terraform can locate its state file
key = "env-prefix/my-service-name/terraform.tfstate"
The key value is structured as follows:
env-prefix: This is the prefix of the target environment, like dev,tst,stg,prd (Development, Test, Stage or Prod)
my-service-name: This is specific to the service that you are creating with terraform (could be something like vpc, redis, grafana)
The target AWS region that you want the resource created in
region = "us-east-1"
Whether or not you want to encrypt the state file
encrypt = "true"
Now that we know the Makefile creates reference to our state file location, how do we provide context to know which state to pull? This is done when we run make. When we run make we provide it three options: customer, environment prefix and region. Customer and environment prefix can be anything that you like, however I prefer to keep the environment name prefix to one of dev, tst, stg or prod but you could call it cat, dog, foo, bar or whatever!
The wrapper takes some switches to know what to run, in our case there are only two switches, clean and terraform-state. Consider the following command:
make terraform-state REGION=us-east-1 ENV=dev CUSTOMER=teslacar
The makefile will create me an s3 backend reference that will look like this:
terraform {
backend "s3" {
bucket = "terraform-state-us-east-1-teslacar"
key = "dev/my-service-name/terraform.tfstate"
region = "us-east-1"
encrypt = "true"
}
}
At the same time the makefile also copies any specific configs from our config folder in our module to a file named terraform.tfvars along with vars.tf.json being created on the fly as well. It also performs a terraform init and terraform get so you are ready to run your plan and apply. I can easily swap to a different state file for another customer in a different region by running make clean and make terraform-state again with different inputs.
and finally the makefile.....
.DEFAULT_GOAL := help
help:
@echo "help -- print this help"
@echo "clean -- clean the working directory of any local copies of state management and temp files"
@echo "terraform-state REGION=us-east-1 ENV=dev CUSTOMER=teslacar"
@echo ""
@echo "Using commands in this Makefile will require that you have access to the relevant target buckets in S3."
tags ?= all
skip ?=
OUTPUT="s3.backend.tf"
BACKUP="s3.backend.tf.backup"
terraform-state:
@if [ -z "$(REGION)" -o -z "$(ENV)" -o -z "$(CUSTOMER)" ] ; then \
echo 'Missing one or more required parameter(s). Use: make $@ REGION=<AWS_REGION> ENV=<env> CUSTOMER=<customer>'; \
exit 1; \
fi
@if [ ! -f "configs/$(CUSTOMER)/$(REGION)/$(ENV).tfvars" ] ; then \
echo "You're trying to use a configuration that doesn't exist ($(ENV).tfvars in config/$(CUSTOMER)/$(REGION)/). Please check the region and ENV that you're providing and that the corresponding files exist"; \
exit 1; \
fi
@if [ ! -f "configs/$(CUSTOMER)/$(CUSTOMER)_vars.tf.json" ] ; then \
echo "You're trying to use a configuration that doesn't exist ($(CUSTOMER)_vars.tf.json in config/$(CUSTOMER)/). Please check the region and ENV that you're providing and that the corresponding files exist"; \
exit 1; \
fi
@if [ -f "$(OUTPUT)" ] ; then \
echo 'removing existing state file'; \
mv s3.backend.tf s3.backend.tf.backup; \
make clean; \
fi
@if [ ! -f "$(OUTPUT)" ] ; then \
STATE="terraform {\n backend \"s3\" {\n\tbucket = \"terraform-state-$(REGION)-$(CUSTOMER)\"\n\tkey = \"$(ENV)/your-service/terraform.tfstate\"\n\tregion = \"$(REGION)\"\n\tencrypt = \"true\"\n }\n}"; \
cp configs/$(CUSTOMER)/$(REGION)/$(ENV).tfvars terraform.tfvars; \
cp configs/$(CUSTOMER)/$(CUSTOMER)_vars.tf.json vars.tf.json; \
echo 's3 state file missing'; \
touch s3.backend.tf; \
echo $$STATE > s3.backend.tf; \
fi
@if [ -f "$(OUTPUT)" ] ; then \
echo "Initializing Terrform Remote State for your module in $(ENV) in $(REGION) for $(CUSTOMER)"; \
/usr/local/bin/terraform init; \
/usr/local/bin/terraform get; \
echo "#####################################################"; \
echo "# You should now be good to go, happy Terraforming #"; \
echo "#####################################################"; \
fi
clean:
echo 'removing existing files'; \
rm -rf s3.backend.tf \
s3.backend.tf.backup \
terraform.tfstate \
terraform.tfstate.d \
.terraform/terraform.tfstate \
*.tfvars \
*vars.tf.json; \