"Infrastructure as Code should be fully treated as code." - Some guy on reddit
This repo is intended to display a approach to Terraform project design with the recommendations given in the 12factor App.
The 12 factor app recommends that configuration has to be separated from the application code. To externalize this, I decided to wrap the module with a YAML wrapper. This allows me to use a YAML file for the configuration.
- YAML is easier to read.
- Typed variables offer input validation.
- YAML can be generated by other tools.
You can also use JSON for input, but this helps more on the automation side. For example, REST requests could be made to deploy Terraform.
There are multiple ways to provide values to Terraform:
- Hardcode them.
- Using variables and default values.
- Using variables and passing a
.tfvars
file. - On the command line using
-var
. - Through environment variables
TF_VAR_
.
Each of these different ways can override the others. This can result in configuration living in various locations and makes code reviews painful.
Why on earth would you wrap Terraform into a container?
- Provide a single way to set variables for Terraform.
- Can be promoted through the stages (dev, qa, prod).
- Everything needed for the deployment is inside the container. Only docker is required.
- The artifact can be used in other pipelines. For example, the pipeline in the application repo can pull this artifact and create a new deployment.
My implementation also includes a provider mirror Terraform Docs - Provider Installation meaning the providers are baked into the docker image.
I've included a compiled version of the Terratest file in the container image. Configuration for this test is read from the same YAML file as the deployment uses.
# Build the docker image
docker build -t terraform-azurerm-reference .
- YAML config can be passed into
/config/input.yaml
location.
docker run -it -e "ARM_SUBSCRIPTION_ID=${ARM_SUBSCRIPTION_ID}" -e "ARM_TENANT_ID=${ARM_TENANT_ID}" \
-e "ARM_CLIENT_ID=${ARM_CLIENT_ID}" -e "ARM_CLIENT_SECRET=${ARM_CLIENT_SECRET}" \
-e "STORAGE_ACCOUNT_NAME=${storage_account_name}" -e "STORAGE_ACCOUNT_KEY=${storage_account_key}" \
-e "CONTAINER_NAME=${container_name}" -e "STATE_FILE_NAME=${state_file_name}" \
-v $PWD/input.yaml:/config/input.yaml:ro \
terraform-azurerm-reference /tests/integration_test
- A docker volume will be used to hold the initialized Terraform as well as the plan file.
Terraform init
docker run -it -e "ARM_SUBSCRIPTION_ID=${ARM_SUBSCRIPTION_ID}" -e "ARM_TENANT_ID=${ARM_TENANT_ID}" \
-e "ARM_CLIENT_ID=${ARM_CLIENT_ID}" -e "ARM_CLIENT_SECRET=${ARM_CLIENT_SECRET}" \
-v tf-wd:/working_dir \
terraform-azurerm-reference terraform init -from-module=/tf \
-backend-config="storage_account_name=${storage_account_name}" -backend-config="container_name=${container_name}" \
-backend-config="access_key=${storage_account_key}" -backend-config="key=${state_file_name}"
Terraform plan
docker run -it -e "ARM_SUBSCRIPTION_ID=${ARM_SUBSCRIPTION_ID}" -e "ARM_TENANT_ID=${ARM_TENANT_ID}" \
-e "ARM_CLIENT_ID=${ARM_CLIENT_ID}" -e "ARM_CLIENT_SECRET=${ARM_CLIENT_SECRET}" \
-v tf-wd:/working_dir -v $PWD/input.yaml:/config/input.yaml:ro \
terraform-azurerm-reference terraform plan -out /working_dir/plan
Terraform apply
docker run -it -e "ARM_SUBSCRIPTION_ID=${ARM_SUBSCRIPTION_ID}" -e "ARM_TENANT_ID=${ARM_TENANT_ID}" \
-e "ARM_CLIENT_ID=${ARM_CLIENT_ID}" -e "ARM_CLIENT_SECRET=${ARM_CLIENT_SECRET}" \
-v tf-wd:/working_dir -v $PWD/input.yaml:/config/input.yaml:ro \
terraform-azurerm-reference terraform apply /working_dir/plan
Terraform destroy
docker run -it -e "ARM_SUBSCRIPTION_ID=${ARM_SUBSCRIPTION_ID}" -e "ARM_TENANT_ID=${ARM_TENANT_ID}" \
-e "ARM_CLIENT_ID=${ARM_CLIENT_ID}" -e "ARM_CLIENT_SECRET=${ARM_CLIENT_SECRET}" \
-v tf-wd:/working_dir -v $PWD/input.yaml:/config/input.yaml:ro \
terraform-azurerm-reference terraform destroy
Based on the YT Video Series by Antonio Masucci