Terraform Pattern
Date: May 23, 2019 Author: Brian HooperTags:
TLDR
This post is a quick walk-through of a solution pattern I often use for new Terraform Projects.
Introduction
Terraform is an open-source infrastructure as code software tool that provides a consistent workflow to manage cloud services by codifying cloud APIs into declarative configuration files.
For effective collaboration, it’s important to delegate and empower teams to work in parallel without conflict. It is best to use a modular approach as it provides flexibility over time for maintaining a solution while improving the ability to manage the blast radius of changes as well as drift across workspaces and module versions. In practice I have found that it is best to follow: One Workspace per Environment per Terraform Configuration while using modules to ensure consistency.
A module is a container for multiple resources that are used together. They provide a single code base where all resources regardless of environment are provisioned with the same code ensuring that changes are promoted in a predictable way. Use modules as a level of abstraction, but be careful to avoid unecessary complexity. Lastly, DO NOT write modules that are simply thin wrappers around single resource types, just use the resource type directly in those cases.
Structure
Here is a base structure for using a modular approach with your Terraform projects:
/project # Terraform Project
|
|__ /bin # Project Binaries
| |
| |__ terraform # Locally Source Terraform (Used by Wrapper)
|
|__ /modules # Locally Sourced Modules (Organized by Provider)
| |
| |__ /acme
| |__ /aws
| |__ /gitlab
| |__ /grafana
| |__ /rundeck
| |__ /custom # Custom Modules
|
|__ /scripts # Custom Scripts for CI/CD Processes
| |
| |__ build.sh
| |__ deploy.sh
| |__ destroy.sh
| |__ drift.sh
| |__ lint.sh
| |__ test.sh
|
|__ /source # Application Source Code
|
|__ /workspaces # Workspaces (Organized by Account/Environment)
| |
| |__ /dev # Development Workspace
| | |__ /application
| | |__ /database
| | |__ /network
| | |__ /security
| | |__ /storage
| |__ /tst # Testing Workspace
| | |...
| |__ /stg # Staging Workspace
| | |...
| |__ /prd # Production Workspace
| | |...
|
|__ terraform.sh # Terraform Wrapper
Workflow
The terraform.sh wrapper is a helpful mechanism for your local workspace to simplify and standardize the workflow:
- Manages the local Terraform Version (/bin)
- Installs any custom binaries (/bin)
- Manages Terraform Stack Initialization
- Manages Terraform Workspace Selection
- Runs Native Terraform Commands using the following arguments:
- Workspace
- Stack
- Action
Usage
./terraform.sh <workspace> <stack> <action> <args>
# Examples
./terraform.sh dev my-stack init
./terraform.sh dev my-stack plan
./terraform.sh dev my-stack apply
Script
terraform.sh
#!/usr/bin/env bash
## CONSTANTS
## ====================================
TERRAFORM_VERSION=0.12.0
PROJECT_PATH=$(dirname $(readlink -f $0))
GLOBAL_PATH=$PROJECT_PATH/global
OS_SHORTNAME=$(uname | awk '{print tolower($0)}')
## ARGUMENTS
## ====================================
WORKSPACE=$1
STACK=$2
ACTION=$3
ACTION_ARGS=$(echo "$@" | awk '{first = $1; second = $2; third = $3; $1 = ""; $2 = ""; $3 = ""; print $0 }')
set -e
## TERRAFORM VERSION (LOCAL)
## ====================================
CURRENT_VERSION=$($PROJECT_PATH/bin/terraform --version 2>/dev/null | head -1 | awk '{print $2}' | sed 's/^v//')
if [ ! "$CURRENT_VERSION" == "$TERRAFORM_VERSION" ]; then
FILE=$(mktemp)
echo "This project is currently using terraform v$TERRAFORM_VERSION. You're at v$CURRENT_VERSION. Installing terraform@${TERRAFORM_VERSION}..."
if [ "$(uname)" == "Darwin" ]; then
curl -L https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_darwin_amd64.zip -o $FILE
unzip -o $FILE -d /tmp terraform
mv /tmp/terraform $PROJECT_PATH/bin/terraform
rm -rf $FILE
elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
curl -L https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip -o $FILE
unzip -o $FILE -d /tmp terraform
mv /tmp/terraform $PROJECT_PATH/bin/terraform
rm -rf $FILE
elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW64_NT" ]; then
echo "Appears that you are running windows. Have you tried turning it off an on again?"
fi
fi
## TERRAFORM WRAPPER
## ====================================
echo ""
echo "=============================================================================="
echo " Executing terraform $ACTION against the $STACK stack in $WORKSPACE. "
echo "=============================================================================="
echo ""
## EXPORT VARIABLES
export STACK
export TARGET_DIR=$PROJECT_PATH/workspaces/$WORKSPACE/$STACK
## VERIFY TARGET DIRECTORY EXISTS (e.g. STACK)
if [ ! -d "$TARGET_DIR" ]; then
echo "Error: Stack \"$STACK\" does not exist in workspace \"$WORKSPACE\"."
exit 1
fi
## CHANGE TO TARGET DIRECTORY
cd $TARGET_DIR
## FORMAT TERRAFORM COMMAND
export CMD="$PROJECT_PATH/bin/terraform $ACTION $ACTION_ARGS"
## VERIFY TERRAFORM STACK IS INITIALIZED
if [ ! -d "$PROJECT_PATH/workspaces/$WORKSPACE/$STACK/.terraform" ]; then
echo "initalizing stack"
$PROJECT_PATH/bin/terraform init
fi
## GET UPDATES FOR TERRAFORM MODULES
$PROJECT_PATH/bin/terraform get -update
set +e
## SELECT TERRAFORM WORKSPACE, ELSE CREATE WORKSPACE
$PROJECT_PATH/bin/terraform workspace select $WORKSPACE >/dev/null 2>&1
if [ "$?" -eq "1" ]; then
echo "creating new workspace: $WORKSPACE for stack $STACK"
$PROJECT_PATH/bin/terraform workspace new $WORKSPACE
fi
set -e
## TERRAFORM COMMAND
$CMD
Next
The above structure and workflow are useful for initializing new terraform projects and maintaining them over time as they scale. The workflow via the terraform.sh wrapper is beneficial because it standardizes and reinforces a consistent structure as well as manages the version of terraform in use. As an architect and consultant I am often collaborating with different customers and teams. This approach is especially helpful when working across multiple projects with differing versions of Terraform or rapidly prototyping new solutions.
After using this basic pattern in practice for several AWS Cloud projects I have designed and developed an approach for building a “Pattern Library” as a mechanism for scaling knowledge across projects and teams. I am working on an open source project called Supply that can be used to implement similar patterns and approaches. More to come!
Resources
Updates
12/20/2020
Many teams use open source approaches such as custom wrapper scripts, custom CI/CD Pipelines, Atlantis, Terraboard, Terragrunt and etc to implement governance mechanisms for their Infrastructure as Code and Cloud Native projects. Since the original date of this post Hashicorp has continued to improve and evolve a number of services to support Terraform such as the Terraform Registry and Terraform Cloud.
At the time of this update you can sign up for Terraform Cloud for FREE ($0 up to 5 Users).
Let's Work Together
Let's Get Ship Done!
Handcrafted by a @KnownTraveler