Skip to content

Instantly share code, notes, and snippets.

@nathanielks
Last active June 3, 2023 17:24
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save nathanielks/5bd4de708e831bbc170f to your computer and use it in GitHub Desktop.
Save nathanielks/5bd4de708e831bbc170f to your computer and use it in GitHub Desktop.
Simple wrapper around terraform to manage multiple environments

This script will pull down an S3 remote configuration before running any terraform actions. Assumes the following structure:

main.tf
terraform.cfg
env/dev/vars
env/staging/vars
env/whatever/vars
env/whatever/somefile.tf

terraform.cfg is required for the remote configuration.

You can use it like so:

./terraform-env.sh dev show
./terraform-env.sh staging plan -out plan
./terraform-env.sh production apply -var="some_var=some_value"

You can use the script just like you'd use terraform, passing whatever arguments you would before. It will only pull the latest environment if the current environment is different from the one you're requesting.

Caveat: this won't work if you've written a plan out to a file and then try to apply that plan as terraform apply doesn't accept the -var-file flag when reading from a plan. For reference:

./terraform-env.sh plan -out plan
./terraform-env.sh apply plan

The above will not work. It's recommend you just run terraform apply plan as the plan file will contain all the necessary bits to run the apply.

This script also allows you to use environment-specific config files. For example, you could have specific dns settings for production in env/production/dns-extra.tf and the script will copy the file into the directory you're calling the script from, run terraform, then delete the copied file.

#!/usr/bin/env bash
set -o pipefail
set -u
function help {
echo "usage: ${0} <environment> <action> [<args>]"
exit 1
}
function contains_element () {
local i
for i in "${@:2}"; do
[[ "$i" == "$1" ]] && return 0
done
return 1
}
function files_exist(){
ls ${1} 1> /dev/null 2>&1
}
#All of the args are mandatory.
if [ $# -lt 1 ]; then
help
fi
# Let's set up our environment
export ENVIRONMENT=$1
export ACTION=$2
ADDTL_PARAMS=${*:3}
CONFIG_FILE=./terraform.cfg
# Let's check the existence of the config file
if [ ! -f $CONFIG_FILE ]; then
echo "Error: $CONFIG_FILE does not exist. You'll need to create a config file so we know where to set up the remote config."
exit 1
fi
source ${CONFIG_FILE}
# Let's set up our variables
ALLOWS_VARFILE=(apply plan push refresh destroy)
ENV_FILE=.terraform/environment
ENV_DIR=env/$ENVIRONMENT
VARS_FILE=${ENV_DIR}/vars
VARS_FILE_FLAG=
BUCKET_KEY=$bucket_prefix/state/$ENVIRONMENT
PREVIOUS_ENVIRONMENT=$([ -f $ENV_FILE ] && echo "$(<$ENV_FILE)" || echo "previous")
EXTRA_ARGS=${extra_args:-''}
PRE_CMD=${pre_command:-''}
POST_CMD=${post_command:-''}
# Let's check to see if a vars file exists for the requested environment before proceeding
if [ ! -f $VARS_FILE ]; then
echo "Error: $VARS_FILE does not exist. You'll need to create a vars file for the requested environment before continuing."
exit 1
fi
# Checks if current action allows a varfile to be passed
contains_element "$ACTION" "${ALLOWS_VARFILE[@]}"
if [ $? -eq 0 ]; then
VARS_FILE_FLAG="-var-file=$VARS_FILE"
fi
# Let's check if the requested environment is different from the previous environment
if [ $PREVIOUS_ENVIRONMENT != $ENVIRONMENT ] || [ ! -f '.terraform/terraform.tfstate' ]
then
# Move current state out of the way to make room for the new state
mv -f .terraform/terraform.tfstate .terraform/terraform.tfstate.$PREVIOUS_ENVIRONMENT > /dev/null 2>&1
mv -f .terraform/terraform.tfstate.backup .terraform/terraform.tfstate.backup.$PREVIOUS_ENVIRONMENT > /dev/null 2>&1
# Let's log the new environment for later
echo $ENVIRONMENT > $ENV_FILE
# Set up remote configuration and pull latest version
terraform remote config -backend S3 -backend-config="bucket=$bucket" -backend-config="key=$BUCKET_KEY" -backend-config="region=$region"
fi
# Let's run the PRE_CMD hook if it's defined
eval ${PRE_CMD}
# let's copy environment specific configuration to the root of the directory
if files_exist ${ENV_DIR}/*.tf; then
cd env/${ENVIRONMENT}
pax -wrs'/\.tf$/\.env\.tf/' *.tf ../../
cd ../../
fi
# Let's do work!
terraform $ACTION $VARS_FILE_FLAG $ADDTL_PARAMS ${EXTRA_ARGS}
# Let's remove those environment-specific configuration files we copied earlier
if files_exist *.env.tf; then
rm *.env.tf
fi
# Let's run the POST_CMD hook if it's defined
eval ${POST_CMD}
bucket=some_secret_bucket
bucket_prefix=example
region=us-east-1
# Both are optional. This allows you to run scripts before and after terraform executes
# (perhaps to decrypt/encrypt files, move files, modify templates, etc)
pre_command='bin/pre.sh'
post_command='bin/post.sh'
@nadnerb
Copy link

nadnerb commented Jul 23, 2015

I just had a chance to look at this. It won't work as you are using the cache located in .terraform/terraform.tfstate. All you will end up doing is changing the location on S3 and then changing the state to use the different variables. It will modify the same infrastructure unless I am missing something?

@nathanielks
Copy link
Author

@nadnerb yeah, I noticed that not too long ago. I'm trying to fix that right now, actually.

@nadnerb
Copy link

nadnerb commented Jul 23, 2015

Cool! I ended up modifying the socorro-infra aws cli S3 version for the moment.

@nathanielks
Copy link
Author

@nadnerb and fixed! Moving the existing state file out of the way allows for the remote config to set up an empty local copy and then pulls whatever is set up remotely. If there's nothing remotely, you have a clean slate!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment