Skip to content

Instantly share code, notes, and snippets.

@brodygov
Last active January 10, 2020 17:29
Show Gist options
  • Save brodygov/22cecf784e7c2b5b3f830507651068fb to your computer and use it in GitHub Desktop.
Save brodygov/22cecf784e7c2b5b3f830507651068fb to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
set -eu
# shellcheck source=/dev/null
. "$(dirname "$0")/lib/common.sh"
usage() {
cat >&2 <<EOM
Usage: $0 [OPTIONS...] <bucket_name> <state_file_path> <terraform_dir> <region> <dynamodb_table>
Configure terraform to store state in S3 with a dynamodb lock table.
This script will create the relevant S3 bucket and dynamodb table as needed,
manage the .terraform directory with either the older symlinking style or the
newer separate subdirectory style, and then run \`terraform init\`.
Arguments:
bucket_name: Name of S3 bucket containing state files
state_file_path: S3 key path to the state file
terraform_dir: Directory in which to run terraform init
region: AWS region to connect to
dynamodb_table: Name of dynamodb table for state file locking
Options:
-h, --help Display this message
--module-style Use the newer separate subdirectory style and do not create
or manage any .terraform symlinks.
--shared-style Use the older shared style with .terraform symlinks.
This script understands two modes for managing the .terraform directory, where
terraform keeps local information about modules and remote state.
shared / identity-devops-private style:
In the older, shared directory style, the terraform_dir is shared among
multiple environments, with env variables delivered from
identity-devops-private. This means that we have to blow away the
.terraform directory every run so that there is no cross-env contamination.
To make this safer, we use a system of symlinks so that the .terraform
directory contents remain in persistently under .deploy/, and all we do on
an individual run is to swap the .terraform symlink to point to it. This
script manages a tree of .terraform directories under '.deploy/' scoped by
the S3 state bucket, region, and key path.
module / local style: (newer, preferred)
In the new style, where we keep a separate subdirectory for each
environment, there is no reuse of the subdirectory, so we can use a
standard plain .terraform directory without doing any special management.
EOM
}
# Log all terraform and aws commands in this script
terraform() {
echo >&2 "+ terraform $*"
env terraform "$@"
}
aws() {
echo >&2 "+ aws $*"
env aws "$@"
}
# Ensure remote state S3 bucket and dynamodb table exist.
# If not, create them.
check_or_create_remote_state_resources() {
echo >&2 "+ aws s3api head-bucket --bucket $BUCKET"
output="$(env aws s3api head-bucket --bucket "$BUCKET" 2>&1)" \
&& ret=$? || ret=$?
if grep -F "Not Found" <<< "$output" >/dev/null; then
log "$output"
log "Bucket $BUCKET does not exist, creating..."
log "Creating an s3 bucket for terraform state"
aws s3 mb "s3://$BUCKET" --region "$REGION"
log "Enabling versioning on the s3 bucket"
aws s3api put-bucket-versioning --bucket "$BUCKET" \
--versioning-configuration Status=Enabled
elif [ "$ret" -ne 0 ]; then
exit "$ret"
fi
log "State lock table: $LOCK_TABLE"
if ! aws dynamodb describe-table --table-name "$LOCK_TABLE" \
--region "$REGION" >/dev/null
then
log "Lock table does not exist, creating..."
log "Creating a dynamodb table for terraform lock files"
aws dynamodb create-table \
--region "${REGION}" \
--table-name "$LOCK_TABLE" \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--sse-specification Enabled=true \
--provisioned-throughput ReadCapacityUnits=2,WriteCapacityUnits=1
log "Waiting for table to appear"
aws dynamodb wait table-exists --table-name "$LOCK_TABLE" \
--region "$REGION"
log "Finished creating dynamodb table $LOCK_TABLE"
fi
}
MODULE_STYLE=1
while [ $# -gt 0 ] && [[ $1 == -* ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
--module-style)
MODULE_STYLE=1
;;
--shared-style)
MODULE_STYLE=
;;
*)
usage
echo_red >&2 "Unknown option: $1"
exit 1
;;
esac
shift
done
if [ $# -ne 5 ] ; then
usage
exit 1
fi
BUCKET=$1
STATE=$2
TF_DIR=$3
REGION=$4
LOCK_TABLE=$5
if [ -n "$MODULE_STYLE" ]; then
log --blue "Setting up TF state (local, module style .terraform)"
else
log --blue "Setting up TF state and symlinking .terraform (shared style)"
fi
log "State file: $STATE"
log "State bucket: $BUCKET"
check_or_create_remote_state_resources
# Set up local .terraform directory with either the old or new .terraform
# directory management styles.
#
cd "${TF_DIR}"
# Sanity check: make sure we have a main.tf
assert_file_exists "main.tf"
if [ -z "$MODULE_STYLE" ]; then
log --blue "Setting up shared style .terraform symlink"
local_state_path=".deploy/$BUCKET/$REGION/$STATE/.terraform"
if [ ! -d "$local_state_path" ]; then
log "Creating new local state directory"
fi
mkdir -vp "$local_state_path" >&2
if [ -L .terraform ]; then
run rm -v .terraform >&2
elif [ -d .terraform ]; then
# We should no longer need to ever delete the .terraform directory.
echo_red >&2 "error: .terraform is a directory, not the expected symlink"
echo_red >&2 "Cowardly refusing to proceed"
exit 5
fi
log "Linking .terraform to local state directory"
run ln -sv "$local_state_path" .terraform >&2
fi
log --blue "Calling terraform init"
# https://github.com/hashicorp/terraform/issues/12762
case "$(CHECKPOINT_DISABLE=1 terraform --version)" in
*v0.9.*|*v0.10.*|*v0.11.*|*v0.12.*)
terraform init \
-backend-config="bucket=${BUCKET}" \
-backend-config="key=${STATE}" \
-backend-config="dynamodb_table=$LOCK_TABLE" \
-backend-config="region=${REGION}"
;;
*)
echo_red >&2 "$0: ERROR: Unsupported terraform version"
exit 1
;;
esac
#!/usr/bin/env bash
set -eu
# Try really hard not to let anything accidentally write to stdout.
# Point stdout at stderr and open FD 3 to point to the original stdout.
# Use echo >&3 to write to stdout hereafter.
exec 3>&1 1>&2
BASENAME="$(basename "$0")"
if [ $# -ne 0 ]; then
cat >&2 <<EOM
usage: $BASENAME
Print the directory containing environment-specific variables. Clone the
private repo containing these variables if it doesn't exist. Within this
directory, the caller should source '\$ENV.sh'.
LOCATION OF PRIVATE REPO:
This script expects to find the private configuration checked out in a separate
repository located (from the root of this repo) at ../{repo-name}-private/.
Customize this path with \$IDENTITY_DEVOPS_PRIVATE_PATH.
If the checkout does not exist, it will offer to clone the repo for you. Set
environment variable \$IDENTITY_DEVOPS_PRIVATE_URL to configure the URL for
identity-devops-private, otherwise 'git remote get-url origin' will be used
with an appended '-private'.
Set SKIP_GIT_CLONE=1 in your environment to skip the prompt for the git
clone.
Set SKIP_GIT_PULL=1 in your environment to skip the automatic git pull.
EOM
exit 1
fi
SENTINEL_ENV_NAME=prod
SKIP_GIT_CLONE="${SKIP_GIT_CLONE-}"
SKIP_GIT_PULL="${SKIP_GIT_PULL-}"
# shellcheck source=/dev/null
. "$(dirname "$0")/lib/common.sh"
# Determine the likely URL for identity-devops-private based on the "origin"
# git remote of the current repository (just by appending -private).
# Expects that the CWD is under the current git checkout.
get_private_url_from_origin() {
local origin_url private_url
origin_url="$(run git remote get-url origin)"
# splice off trailing .git if present
origin_url="${origin_url%.git}"
# splice off trailing / if present
origin_url="${origin_url%/}"
private_url="$origin_url-private"
echo >&2 "Inferred default identity-devops-private URL: $private_url"
echo "$private_url"
}
get_private_path() {
local toplevel basename
# We assume git rev-parse --show-toplevel returns an absolute path with no
# trailing slash.
toplevel="$(run git rev-parse --show-toplevel)"
if [ -z "$toplevel" ]; then
echo "This script needs to be run within a valid git repo." >&2
return 1
fi
basename="$(basename "$toplevel")"
# ../{basename}-private
echo "$(dirname "$toplevel")/$basename-private"
}
# usage: clone_private_repo PARENT_DIR
#
# Git clone the identity-devops-private repo under PARENT_DIR locally.
clone_private_repo() {
local parent_dir clone_url
parent_dir="$1"
clone_url="${IDENTITY_DEVOPS_PRIVATE_URL-$(get_private_url_from_origin)}"
pushd "$parent_dir"
run git clone "$clone_url"
popd
}
check_maybe_clone_private_repo() {
local path
path="$1"
if [ ! -d "$path" ]; then
echo >&2 "warning: Private repo is not checked out at $path"
if [ -n "$SKIP_GIT_CLONE" ]; then
echo >&2 "SKIP_GIT_CLONE is set, aborting."
return 1
fi
if prompt_yn "Do you want to git clone the private repo?"; then
echo >&2 "OK, cloning..."
clone_private_repo "$(dirname "$path")"
if [ -d "$path" ]; then
echo >&2 "Clone done"
else
echo >&2 "Something went wrong cloning."
return 2
fi
else
echo >&2 "OK, aborting."
return 1
fi
fi
}
git_pull() {
if [ -n "$SKIP_GIT_PULL" ]; then
echo >&2 "SKIP_GIT_PULL is set, skipping git pull of private repo"
return
fi
local dir basename
dir="$1"
basename="$(basename "$dir")"
echo >&2 "Updating $basename, set env var SKIP_GIT_PULL=1 to skip"
local cur_branch
cur_branch="$(run git -C "$dir" symbolic-ref --short HEAD)"
if [ "$cur_branch" != "master" ]; then
echo_yellow >&2 \
"Warning: current $basename branch is $cur_branch, not master"
fi
if run git -C "$dir" pull --ff-only --no-rebase; then
return
else
echo_red >&2 "Error: git pull failed"
fi
}
log "Looking for env-specific variables"
private_path="${IDENTITY_DEVOPS_PRIVATE_PATH-$(get_private_path)}"
check_maybe_clone_private_repo "$private_path"
git_pull "$private_path"
# assume there should be an env called prod or else bail out
if [ ! -e "$private_path/env/$SENTINEL_ENV_NAME.sh" ]; then
echo >&2 "Somehow $private_path/env/$SENTINEL_ENV_NAME.sh is missing!"
exit 3
fi
found="$private_path/env"
log "Found env variables at $found/"
echo >&3 "$found"
#!/bin/bash
#
# Currently, our automation depends on a number of environment variables to
# configure terraform. See
# https://www.terraform.io/docs/configuration/variables.html#environment-variables.
#
# There are some variables that are the same across each run, but some user
# specific configuration that's mixed in. This script is an attempt to factor
# out and check for some of that configuration.
#
# Do not source this directly, only source it from another script that depends
# on this environment configuration, because it does some error checking and
# may exit your shell.
# See: https://github.com/18F/identity-devops/pull/252
# Increment this to make breaking changes in the environment config. The
# load-env.sh script will bail out if the environment does not set a variable
# $ID_ENV_COMPAT_VERSION >= this value, which provides a way to ensure that new
# scripts get run with a new enough environment config.
ENFORCED_ENV_COMPAT_VERSION=4
BASENAME="$(basename "${BASH_SOURCE[0]}")"
DIRNAME="$(dirname "${BASH_SOURCE[0]}")"
# shellcheck source=/dev/null
. "$DIRNAME/lib/common.sh"
usage() {
cat >&2 <<EOM
Usage: $BASENAME ENVIRONMENT_NAME
Load environment files for ENVIRONMENT_NAME, and do some sanity checking to
ensure we have all the necessary variables.
This script loads environment variables from identity-devops-private. It is not
typically called directly, but instead is invoked by \`deploy\`.
NOTE: This script might exit your shell if you source it and don't have all the
right stuff set up. It's a good idea to only source it from another script, or
just to be really sure you've set all the necessary variables.
Set \$ENV_DEBUG=1 to print environment variables once we finish.
Set \$SKIP_GIT_PULL=1 to skip automatic git pull of identity-devops-private.
(Expert mode only) Set \$ID_ENV_FILE to override the normal environment loader
and use a single env file. Not recommended unless you have particular reason to
do so.
EOM
}
# Make sure the loaded environment is new enough.
enforce_environment_compat_version() {
if [ -z "${ID_ENV_COMPAT_VERSION-}" ]; then
echo_red "$BASENAME: error: \$ID_ENV_COMPAT_VERSION not set by env"
echo_red "This ought to be set in identity-devops-private/env/*.sh"
echo_red "Maybe check if that repo is up-to-date?"
return 1
fi
if [ "$ID_ENV_COMPAT_VERSION" -lt "$ENFORCED_ENV_COMPAT_VERSION" ]; then
echo_red "$BASENAME: error: \$ID_ENV_COMPAT_VERSION set by env is old"
echo_red "This means your identity-devops-private clone is outdated."
echo_red "Try updating that repo to latest master?"
echo_red "\$ID_ENV_COMPAT_VERSION: $ID_ENV_COMPAT_VERSION"
echo_red "\$ENFORCED_ENV_COMPAT_VERSION: $ENFORCED_ENV_COMPAT_VERSION"
return 1
fi
}
if [ $# -ne 1 ]; then
usage
# The `return 1 || exit 1` pattern allows us to return non-zero exit codes
# to the user without exiting their shell if they are sourcing this file.
return 1 2>/dev/null || exit 1
fi
echo_blue >&2 "$BASENAME $*"
export TF_VAR_env_name="$1"
ID_ENV_FILE="${ID_ENV_FILE-}"
if [ -n "$ID_ENV_FILE" ]; then
echo_red "Warning: not using normal variables from identity-devops-private"
echo_red "Loading variables as requested from '$ID_ENV_FILE'"
# shellcheck source=/dev/null
. "$ID_ENV_FILE"
enforce_environment_compat_version || return 4 2>/dev/null || exit 4
return 0 2>/dev/null || exit 0
fi
# Locate and potentially git clone identity-devops-private
ID_ENV_DIR="$(run "$DIRNAME/get-private-env.sh")"
if [ -z "$ID_ENV_DIR" ]; then
echo_red "get-private-env.sh failed"
return 3 >/dev/null || exit 3
fi
if [ "$(wc -l <<< "$ID_ENV_DIR")" -ne 1 ]; then
echo_red "get-private-env.sh bug: file path shouldn't be multiple lines"
echo_red "Path: '$ID_ENV_DIR'"
return 3 >/dev/null || exit 3
fi
env_specific_path="$ID_ENV_DIR/$TF_VAR_env_name.sh"
if [ -e "$env_specific_path" ]; then
log "Sourcing env-specific private env vars."
log "Path: '$env_specific_path'"
# shellcheck source=/dev/null
. "$env_specific_path"
else
log "No env-specific vars file found: ($TF_VAR_env_name.sh)"
echo_red >&2 "Unknown environment: '$TF_VAR_env_name'"
echo_red >&2 "Please create env file $TF_VAR_env_name.sh in $ID_ENV_DIR"
return 4 2>/dev/null || exit 4
fi
enforce_environment_compat_version || return 4 2>/dev/null || exit 4
if [ -n "${ENV_DEBUG-}" ]; then
env
fi
if env | grep ^TV_VAR_; then
echo_red "Found variables named TV_VAR_, but you probably meant TF_VAR_!"
echo_red "$(env | grep ^TV_VAR_)"
fi
# shellcheck disable=SC2163,SC2086
export ${!TF_VAR_*}
#!/usr/bin/env bash
set -euo pipefail
# shellcheck source=/dev/null
. "$(dirname "$0")/lib/common.sh"
# Directory where we will install terraform
TERRAFORM_DOT_D="${TERRAFORM_DOT_D-}"
if [ -z "$TERRAFORM_DOT_D" ]; then
TERRAFORM_DOT_D="$HOME/.terraform.d"
if [ ! -d "$TERRAFORM_DOT_D" ]; then
run mkdir -vp "$TERRAFORM_DOT_D"
fi
fi
TF_DEPRECATED_DIR="${TF_DEPRECATED_DIR-"$HOME/.terraform-plugins"}"
TERRAFORM_EXE_DIR="$TERRAFORM_DOT_D/tf-switch"
TERRAFORM_PLUGIN_DIR="$TERRAFORM_DOT_D/plugin-cache"
TF_DOWNLOAD_URL="https://releases.hashicorp.com/terraform"
# Set this to skip installing TF symlink to TERRAFORM_SYMLINK
ID_TF_SKIP_SYMLINK="${ID_TF_SKIP_SYMLINK-}"
# Set this to skip GPG verification
ID_TF_SKIP_GPG="${ID_TF_SKIP_GPG-}"
# Set this to skip the terraform plugin cache check
ID_TF_SKIP_PLUGIN_CACHE="${ID_TF_SKIP_PLUGIN_CACHE-}"
# Location of installed TF symlink
TERRAFORM_SYMLINK="${TERRAFORM_SYMLINK-/usr/local/bin/terraform}"
SUDO_LN=
# Hashicorp GPG key fingerprint
TF_GPG_KEY_FINGERPRINT=91A6E7F85D05C65630BEF18951852D87348FFC4C
TF_GPG_KEY_CONTENT='
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFMORM0BCADBRyKO1MhCirazOSVwcfTr1xUxjPvfxD3hjUwHtjsOy/bT6p9f
W2mRPfwnq2JB5As+paL3UGDsSRDnK9KAxQb0NNF4+eVhr/EJ18s3wwXXDMjpIifq
fIm2WyH3G+aRLTLPIpscUNKDyxFOUbsmgXAmJ46Re1fn8uKxKRHbfa39aeuEYWFA
3drdL1WoUngvED7f+RnKBK2G6ZEpO+LDovQk19xGjiMTtPJrjMjZJ3QXqPvx5wca
KSZLr4lMTuoTI/ZXyZy5bD4tShiZz6KcyX27cD70q2iRcEZ0poLKHyEIDAi3TM5k
SwbbWBFd5RNPOR0qzrb/0p9ksKK48IIfH2FvABEBAAG0K0hhc2hpQ29ycCBTZWN1
cml0eSA8c2VjdXJpdHlAaGFzaGljb3JwLmNvbT6JATgEEwECACIFAlMORM0CGwMG
CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEFGFLYc0j/xMyWIIAIPhcVqiQ59n
Jc07gjUX0SWBJAxEG1lKxfzS4Xp+57h2xxTpdotGQ1fZwsihaIqow337YHQI3q0i
SqV534Ms+j/tU7X8sq11xFJIeEVG8PASRCwmryUwghFKPlHETQ8jJ+Y8+1asRydi
psP3B/5Mjhqv/uOK+Vy3zAyIpyDOMtIpOVfjSpCplVRdtSTFWBu9Em7j5I2HMn1w
sJZnJgXKpybpibGiiTtmnFLOwibmprSu04rsnP4ncdC2XRD4wIjoyA+4PKgX3sCO
klEzKryWYBmLkJOMDdo52LttP3279s7XrkLEE7ia0fXa2c12EQ0f0DQ1tGUvyVEW
WmJVccm5bq25AQ0EUw5EzQEIANaPUY04/g7AmYkOMjaCZ6iTp9hB5Rsj/4ee/ln9
wArzRO9+3eejLWh53FoN1rO+su7tiXJA5YAzVy6tuolrqjM8DBztPxdLBbEi4V+j
2tK0dATdBQBHEh3OJApO2UBtcjaZBT31zrG9K55D+CrcgIVEHAKY8Cb4kLBkb5wM
skn+DrASKU0BNIV1qRsxfiUdQHZfSqtp004nrql1lbFMLFEuiY8FZrkkQ9qduixo
mTT6f34/oiY+Jam3zCK7RDN/OjuWheIPGj/Qbx9JuNiwgX6yRj7OE1tjUx6d8g9y
0H1fmLJbb3WZZbuuGFnK6qrE3bGeY8+AWaJAZ37wpWh1p0cAEQEAAYkBHwQYAQIA
CQUCUw5EzQIbDAAKCRBRhS2HNI/8TJntCAClU7TOO/X053eKF1jqNW4A1qpxctVc
z8eTcY8Om5O4f6a/rfxfNFKn9Qyja/OG1xWNobETy7MiMXYjaa8uUx5iFy6kMVaP
0BXJ59NLZjMARGw6lVTYDTIvzqqqwLxgliSDfSnqUhubGwvykANPO+93BBx89MRG
unNoYGXtPlhNFrAsB1VR8+EyKLv2HQtGCPSFBhrjuzH3gxGibNDDdFQLxxuJWepJ
EK1UbTS4ms0NgZ2Uknqn1WRU1Ki7rE4sTy68iZtWpKQXZEJa0IGnuI2sSINGcXCJ
oEIgXTMyCILo34Fa/C6VCm2WBgz9zZO8/rHIiQm1J5zqz0DrDwKBUM9C
=LYpS
-----END PGP PUBLIC KEY BLOCK-----
'
EXE_SUFFIX=
case "$OSTYPE" in
*darwin*) TF_OS=darwin ;;
linux-gnu) TF_OS=linux ;;
cygwin|msys)
TF_OS=windows
EXE_SUFFIX=.exe
;;
solaris*) TF_OS=solaris ;;
freebsd*) TF_OS=freebsd ;;
*)
echo >&2 "Unknown OSTYPE '$OSTYPE'"
echo >&2 "If you know the appropriate mapping, add this to the OSTYPE"
echo >&2 "case statement: using $TF_DOWNLOAD_URL"
exit 3
;;
esac
case "$(uname -m)" in
x86_64|amd64) TF_ARCH=amd64 ;;
i386|i686) TF_ARCH=386 ;;
arm*) TF_ARCH=arm ;;
*)
echo >&2 "Unknown architecture '$(uname -m)'"
exit 4
;;
esac
usage() {
cat >&2 <<EOM
usage: $(basename "$0") TERRAFORM_VERSION
Download and install precompiled terraform binaries from Github.
Keep multiple versions installed under $TERRAFORM_EXE_DIR and
symlink the chosen active binary at $TERRAFORM_SYMLINK.
Much of this script's behavior is configurable by environment variables, such
as TERRAFORM_SYMLINK to set the symlink install location, or TERRAFORM_DOT_D to
override the location used instead of ~/.terraform.d/.
For example:
$(basename "$0") 0.9.11
Known Terraform versions:
EOM
echo "$KNOWN_TF_VERSIONS" | cut -d' ' -f1 | sed 's/^/ /'
}
# If macOS shipped with a modern version of bash (i.e. Bash 4.0), we would have
# associative arrays and wouldn't need this hack.
#
# Upstream references for the releases:
# - https://releases.hashicorp.com/terraform/
#
KNOWN_TF_VERSIONS='
0.9.11
0.10.8
0.11.14
0.12.17
'
sha256_cmd() {
if which sha256sum >/dev/null; then
run sha256sum "$@"
elif which shasum >/dev/null; then
run shasum -a 256 "$@"
else
echo >&2 "Could not find sha256sum or shasum"
return 1
fi
}
install_tf_version() {
local target download_prefix terraform_exe tmpdir checksum_file csum
local download_basename
target="$1"
terraform_exe="$2"
echo_blue "Installing terraform version $target"
download_prefix="$TF_DOWNLOAD_URL/$target"
download_basename="terraform_${target}_${TF_OS}_${TF_ARCH}.zip"
checksum_file="terraform_${target}_SHA256SUMS"
tmpdir="$(mktemp -d)"
(
echo >&2 "+ cd '$tmpdir'"
cd "$tmpdir"
run curl -Sf --remote-name-all \
"${download_prefix}/${checksum_file}"{,.sig} \
"${download_prefix}/${download_basename}"
if [ -n "$ID_TF_SKIP_GPG" ]; then
echo "\$ID_TF_SKIP_GPG is set, skipping GPG verification"
else
echo >&2 "Checking GPG signature"
if ! run gpg --batch --list-keys "$TF_GPG_KEY_FINGERPRINT"; then
#echo >&2 "Fetching Hashicorp GPG key"
#run gpg --recv-keys "$TF_GPG_KEY_FINGERPRINT"
echo >&2 "Importing Hashicorp GPG key"
run gpg --import <<< "$TF_GPG_KEY_CONTENT"
fi
run gpg --batch --status-fd 1 --verify "$checksum_file"{.sig,} \
| grep '^\[GNUPG:\] VALIDSIG '"$TF_GPG_KEY_FINGERPRINT"
echo >&2 "OK, finished verifying"
fi
echo >&2 "Checking SHA256 checksum"
csum="$(run grep "${TF_OS}_${TF_ARCH}.zip" "$checksum_file")"
sha256_cmd -c <<< "$csum"
echo >&2 "OK"
run unzip -d "$tmpdir" "$tmpdir/$download_basename"
mv -v "$tmpdir/terraform" "$terraform_exe"
)
rm -r "$tmpdir"
echo_blue "New terraform was installed to $terraform_exe"
}
setup_terraform_plugin_cache() {
if [ -n "$ID_TF_SKIP_PLUGIN_CACHE" ]; then
echo >&2 "Skipping terraform plugin cache management as requested"
return
fi
if [ ! -d "$TERRAFORM_PLUGIN_DIR" ]; then
run mkdir -v "$TERRAFORM_PLUGIN_DIR"
fi
if [ -e "$HOME/.terraformrc" ]; then
if ! grep "^plugin_cache_dir " "$HOME/.terraformrc" >/dev/null; then
cat >&2 <<EOM
Please modify ~/.terraformrc to include something like:
plugin_cache_dir = "\$HOME/.terraform.d/plugin-cache"
If you want to skip this check and not cache plugins, set
ID_TF_SKIP_PLUGIN_CACHE=1 when running this script.
EOM
if prompt_yn "Answer Yes when this is done"; then
setup_terraform_plugin_cache
else
return 1
fi
fi
else
echo "$HOME/.terraformrc does not exist."
if prompt_yn "Should I create it for you?"; then
cat > "$HOME/.terraformrc" <<EOM
plugin_cache_dir = "\$HOME/.terraform.d/plugin-cache"
EOM
echo "Created:"
cat "$HOME/.terraformrc"
else
return 1
fi
fi
}
install_tf_symlink() {
local terraform_exe
terraform_exe="$1"
if [ -n "$ID_TF_SKIP_SYMLINK" ]; then
echo "ID_TF_SKIP_SYMLINK is set, not installing terraform symlink"
return
fi
# if homebrew terraform is installed, unlink it
if which brew >/dev/null; then
# if we already have any terraforms, unlink first
if run brew list terraform; then
run brew unlink terraform
fi
fi
echo_blue "Installing terraform symlink to $TERRAFORM_SYMLINK"
if [ -n "$SUDO_LN" ]; then
run sudo ln -sfv "$terraform_exe" "$TERRAFORM_SYMLINK"
else
run ln -sfv "$terraform_exe" "$TERRAFORM_SYMLINK"
fi
}
deprecation_check() {
if grep "$TF_DEPRECATED_DIR" "$HOME/.terraformrc" >/dev/null; then
echo_yellow >&2 "Warning: found reference to $TF_DEPRECATED_DIR in $HOME/.terraformrc"
echo_yellow >&2 "You may want to remove the acme plugin entirely from your ~/.terraformrc"
fi
if [ -d "$TF_DEPRECATED_DIR" ]; then
echo_yellow >&2 "Warning: found directory from older terraform-switch.sh"
echo_yellow >&2 "You may want to run: rm -r $TF_DEPRECATED_DIR"
fi
}
main() {
local target_version current_version terraform_exe
target_version="$1"
if which terraform >/dev/null; then
current_version="$(get_terraform_version)"
else
current_version=
fi
setup_terraform_plugin_cache
deprecation_check
if [ "v$target_version" = "$current_version" ]; then
echo "Already running terraform $target_version"
return
fi
if ! which gpg >/dev/null; then
echo >&2 "$(basename "$0"): error, gpg not found"
echo >&2 "Set ID_TF_SKIP_GPG=1 if you want to skip the signature check"
return 1
fi
if [ ! -e "$TERRAFORM_EXE_DIR" ]; then
run mkdir -v "$TERRAFORM_EXE_DIR"
fi
terraform_exe="$TERRAFORM_EXE_DIR/terraform_${target_version}$EXE_SUFFIX"
if [ -e "$terraform_exe" ]; then
echo_blue "Terraform $target_version already installed at $terraform_exe"
else
echo "Terraform $target_version does not appear to be installed."
if prompt_yn "Install it?"; then
install_tf_version "$target_version" "$terraform_exe"
fi
fi
install_tf_symlink "$terraform_exe"
echo_blue 'All done!'
}
if [ $# -lt 1 ]; then
usage
exit 1
fi
main "$@"
#!/usr/bin/env bash
# function to exit process with error message
die() { echo_red "$*" >&2 ; exit 1; }
# http://redsymbol.net/articles/unofficial-bash-strict-mode/
set -euo pipefail
# Try really hard not to let anything accidentally write to stdout.
# Point stdout at stderr and open FD 3 to point to the original stdout.
# Use echo >&3 to write to stdout hereafter.
exec 3>&1 1>&2
basename="$(basename "$0")"
usage() {
cat >&2 <<EOM
# identity-devops-private configs:
usage: $basename [OPTIONS] ENV_NAME TERRAFORM_DIR [TERRAFORM_COMMANDS...]
# newer module-style configs:
usage: $basename [OPTIONS] TERRAFORM_DIR/ENV_NAME [TERRAFORM_COMMANDS...]
Run TERRAFORM_COMMANDS against environment ENV_NAME using the configuration in
TERRAFORM_DIR.
Arguments:
TERRAFORM_DIR:
Directory path relative to the repo root to a terraform directory such
as "terraform_app"
ENV_NAME:
Environment name for the deployment, e.g. "dev", "prod", or "global".
The specifics will differ between terraform dirs.
TERRAFORM_COMMANDS:
Arguments and options passed to terraform, such as "plan", "apply", or
"state list".
Options:
-h, --help Display this message
--import-remote-state Bootstrap terraform remote state management by
importing into terraform management the terraform
remote state S3 bucket and DynamoDB table that are
used to manage TF remote state, which are
automatically created by configure_state_bucket.sh.
For older terraform directories that use identity-devops-private, env-specific
config variables will be loaded automatically from identity-devops-private.
This script will automatically delete .terraform/ and set it up as a symlink to
a common .deploy/ subdirectory.
Example:
$basename dev terraform-app plan
For newer terraform directories that use module-style configuration, any
env-specific config variables will be loaded directly from the specified
subdirectory, and identity-devops-private will not be used. In this mode,
.terraform/ is left as-is and not managed specially, which makes it easier to
use terraform without invoking this script.
Example:
$basename terraform-sms/sandbox plan
EOM
}
# usage: module_style_check_required_files TF_DIR
module_style_check_required_files() {
local tf_dir
tf_dir="$1"
if [ ! -e "$tf_dir/main.tf" ]; then
echo_red >&2 "$basename: No such file: $tf_dir/main.tf"
echo_red >&2 "Are you sure that $tf_dir is a module-style TF_DIR?"
return 1
fi
if [ ! -e "$tf_dir/env-vars.sh" ]; then
echo_red >&2 "$basename: No such file: $tf_dir/env-vars.sh"
echo_red >&2 "Are you sure that $tf_dir is a module-style TF_DIR?"
return 1
fi
}
# usage: verify_not_module_style
#
# Variables: $TF_DIR, $ENVIRONMENT
#
# Ensure that the target directory is not a module/subdirectory style TF_DIR.
#
verify_not_module_style() {
local tf_dir test_file
tf_dir="$TF_DIR/$ENVIRONMENT"
test_file="$tf_dir/env-vars.sh"
if [ -e "$test_file" ]; then
echo_red >&2 "
Error: mismatch between expected and apparent .terraform directory style.
I thought this was a shared-style TF_DIR that uses identity-devops-private.
However, it contains the env-vars.sh that is only supposed to exist in
module/subdirectory style TF_DIRs.
$basename: file unexpectedly exists: $test_file
Are you sure that $tf_dir is a shared-style TF_DIR?
Try instead:
$basename $TF_DIR/$ENVIRONMENT ${TF_CMD[*]}
"
return 1
fi
}
if [ $# -lt 2 ]; then
usage
exit 1
fi
repo_root_before_cd="$(git rev-parse --show-toplevel 2>/dev/null || true)"
# This script assumes it is being run from the repo root.
cd "$(dirname "$0")"
cd "$(git rev-parse --show-toplevel)"
# shellcheck source=/dev/null
. "./bin/lib/common.sh"
# Make sure our repo root didn't change after cd, because that implies that the
# user is running a different script than they expected.
verify_repo_root_unchanged "$repo_root_before_cd" "$basename"
ORIGIN_REMOTE_NAME="${ORIGIN_REMOTE_NAME-origin}"
# Parse options
IMPORT_REMOTE_STATE=
while [ $# -gt 0 ] && [[ $1 == -* ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
--import-remote-state)
IMPORT_REMOTE_STATE=1
;;
*)
usage
exit 1
;;
esac
shift
done
# This is a bit of a mess since we handle both old identity-devops-private
# style config and new module-style config. Once we deprecate the former, we can
# simplify the argument parsing dramatically.
ENVIRONMENT_OR_TF_DIR=$1; shift
if [ -d "$ENVIRONMENT_OR_TF_DIR" ]; then
# assume new module-style config if first arg is a directory that exists
MODULE_STYLE=1
ENVIRONMENT_OR_TF_DIR="${ENVIRONMENT_OR_TF_DIR%/}" # strip off trailing /
# split input on '/'
IFS='/' read -ra parts <<< "$ENVIRONMENT_OR_TF_DIR"
# Turn "./foo/bar" into "foo/bar"
if [ "${parts[0]-}" = "." ]; then
parts=("${parts[@]:1}")
fi
if [ "${#parts[@]}" -ne 2 ]; then
echo_red >&2 "Error: could not parse first argument as TF_DIR."
echo_red >&2
echo_red >&2 "First arg is a directory, so I'm assuming it is a new"
echo_red >&2 "module-style terraform dir. This format is expected to"
echo_red >&2 "contain two '/'-separated parts."
echo_red >&2
echo_red >&2 "Expected format: '<TF_DIR>/<ENV>/'"
echo_red >&2 "Received: '$ENVIRONMENT_OR_TF_DIR'"
exit 2
fi
TF_DIR="${parts[0]}"
ENVIRONMENT="${parts[1]}"
TF_DIR_FULL="${parts[0]}/${parts[1]}"
echo_blue >&2 "TF_DIR: $TF_DIR (style: module vars)"
echo_blue >&2 "ENVIRONMENT: $ENVIRONMENT"
module_style_check_required_files "$ENVIRONMENT_OR_TF_DIR"
else
# otherwise use traditional identity-devops-private style
MODULE_STYLE=
# environment is first arg
ENVIRONMENT="$ENVIRONMENT_OR_TF_DIR"
# tf_dir is second arg
TF_DIR=$1; shift
TF_DIR="${TF_DIR%/}" # strip off trailing /
TF_DIR_FULL="$TF_DIR"
echo_blue >&2 "TF_DIR: $TF_DIR (style: identity-devops-private vars)"
echo_blue >&2 "ENVIRONMENT: $ENVIRONMENT"
fi
# If we're loading the terraform remote state, then override our default
# argument parsing. (Yeah, this is ugly.)
if [ -n "$IMPORT_REMOTE_STATE" ]; then
if [ $# -gt 0 ]; then
usage
echo_red "Cannot pass arguments when --import-remote-state is set"
exit 1
fi
echo_blue "Importing TF remote state, as requested..."
TF_CMD=("command-should-not-be-reached")
else
# Consume all remaining arguments as Terraform command
if [ $# -eq 0 ]; then
usage
exit 1
fi
TF_CMD=("$@")
fi
case "$TF_DIR" in
terraform-dns|terraform-cloudtrail|terraform-common)
# These configurations apply account wide, so set the environment to the
# account ID.
#
# https://stackoverflow.com/a/33791322
if [ "$ENVIRONMENT" != "global" ]; then
echo_red >&2 "$TF_DIR applies to the whole AWS account!"
echo_red >&2 "For safety, pass 'global' as the environment name"
die "You provided '$ENVIRONMENT'"
fi
ENVIRONMENT="account_global_$(run aws sts get-caller-identity --output text --query 'Account')"
echo >&2 "Forcing environment name to be $ENVIRONMENT since $TF_DIR is account wide configuration"
;;
*/?*)
echo_red >&2 "\$TF_DIR cannot contain slashes, must be at top level"
die "You provided '$TF_DIR'"
;;
esac
check_release_versioning() {
local strict_check rev_name
echo >&2 "DEPLOY: Checking whether environment versioning is recommended"
case "$TF_DIR" in
terraform-app|terraform-sms)
# versioning enabled
;;
*)
# versioning not enabled
return
;;
esac
case "$ENVIRONMENT" in
# strict checks in these user-facing environments
prod|staging|int)
strict_check=1
;;
# limited checks in these testing environments
dev|qa|pt|dm)
strict_check=
;;
# no checking in other environments
*)
return
;;
esac
rev_name="$(git rev-parse --abbrev-ref HEAD)"
# make sure we're on stages/$ENVIRONMENT
if [ "$rev_name" != "stages/$ENVIRONMENT" ]; then
echo_yellow >&2 "
warning: Current git branch is not the expected stages/$ENVIRONMENT
Environment: $ENVIRONMENT
Expected branch: stages/$ENVIRONMENT
Actual branch: $rev_name
"
# check is fatal for strict environments
if [ -n "$strict_check" ] && [ -z "${DEPLOY_WEIRD_BRANCH-}" ]; then
echo_red >&2 "error: branch name must be stages/$ENVIRONMENT"
echo_red >&2 "set DEPLOY_WEIRD_BRANCH=1 to skip this check"
return 2
fi
else
# make sure local stages/<env> is the same as origin/stages/<env>
# fetch latest from remote
if ! run git fetch "$ORIGIN_REMOTE_NAME"; then
echo_yellow >&2 "git fetch failed"
fi
local local_ref origin_ref
local_ref="$(git rev-parse "stages/$ENVIRONMENT")"
origin_ref="$(git rev-parse "$ORIGIN_REMOTE_NAME/stages/$ENVIRONMENT")"
if [ "$local_ref" != "$origin_ref" ]; then
echo_yellow >&2 "
Warning: Local stages branch differs from origin stages branch!
Maybe you need to git push?
stages/$ENVIRONMENT: $local_ref
$ORIGIN_REMOTE_NAME/stages/$ENVIRONMENT: $origin_ref
You can also set ORIGIN_REMOTE_NAME if your primary git remote is named
something other than 'origin'.
"
# check is fatal for strict environments
if [ -n "$strict_check" ] && [ -z "${DEPLOY_WEIRD_BRANCH-}" ]; then
echo_red >&2 "error: local and remote stages branches differ"
echo_red >&2 "set DEPLOY_WEIRD_BRANCH=1 to skip this check"
return 3
fi
fi
fi
if [ -n "$strict_check" ]; then
if [[ ( $(cat VERSION.txt) == *pre* ||
! "$(git log -n 1 --pretty=format:'%d')" =~ tag: ) ]]
then
echo_yellow >&2 "
###############################################################################
# INFORMATIONAL #
###############################################################################
We would like to start using tagged release versions that we carry through
environments for user-facing environments.
The $ENVIRONMENT environment ideally should be deployed from a tagged,
non-prerelease version because it is one of the environments officially
supported by the devops team for client use.
Please consider running \`rake release\` to edit VERSION.txt and create a
tagged release.
See https://github.com/18F/identity-devops/tree/master/doc/process/releases.md
for more details about our release tools.
The current version in VERSION.txt is: $(cat VERSION.txt)
"
fi
fi
}
# Verify that the account ID expected from env vars matches the actual AWS
# account that we connect to when running AWS commands. This isn't as useful
# for newer module-style terraform directories, which can independently specify
# 'allowed_account_ids = ["01234..."]' in their main.tf files.
check_account_id() {
real_account_id="$(run aws sts get-caller-identity --output text --query 'Account')"
if [ "$real_account_id" != "$aws_account_id" ]; then
echo_red "Account ID from environment does not match actual account"
echo_red "From env vars: $aws_account_id"
echo_red "Actual account ID: $real_account_id"
echo_red "Are you sure you set AWS_PROFILE / secret key right?"
die "error: Account ID mismatch"
fi
}
# Use the newer, massively simpler, module style of keeping env vars in subdirs.
load_env_module_style() {
local env_vars_file
env_vars_file="$TF_DIR/$ENVIRONMENT/env-vars.sh"
echo >&2 "DEPLOY: Loading module-style env variables from $env_vars_file..."
# shellcheck source=/dev/null
. "$env_vars_file"
if [ -z "${aws_account_id-}" ]; then
echo_red "Expected to find var \$aws_account_id in env vars file"
return 3
fi
MOVED_TO_TFVARS=
STATE="${TF_DIR_FULL}.tfstate"
}
# Load and validate environment variables from identity-devops-private using
# bin/load-env.sh and bin/get-tfvars-for-env
load_env_devops_private_style() {
echo >&2 "DEPLOY: Loading shared-style environment variables..."
verify_not_module_style
. bin/load-env.sh "$ENVIRONMENT"
if [ -z "${TF_VAR_account_id-}" ]; then
echo_red >&2 "Expected to find TF_VAR_account_id in private env"
return 3
fi
aws_account_id="$TF_VAR_account_id"
if [ -n "$MOVED_TO_TFVARS" ]; then
echo >&2 "Looks like this environment has switched to using .tfvars files"
# read a list of .tfvars files printed by get-tfvars-for-env
tfvars_names="$(bin/get-tfvars-for-env "$aws_account_id" "$ENVIRONMENT")"
# render an array prefixing each filename with -var-file
# in bash 4 we could use mapfile instead
tf_vars_opts=()
while IFS= read -r line; do
tf_vars_opts+=( "-var-file" "$line" )
done <<< "$tfvars_names"
fi
# Check that current version of terraform is supported by our environment. This
# array should be set in the environment-specific variables.
check_terraform_version "${ID_SUPPORTED_TERRAFORM_VERSIONS[@]}" >&2
STATE="${TF_DIR}/terraform-${TF_VAR_env_name}.tfstate"
case "$TF_DIR" in
terraform-dns|terraform-cloudtrail|terraform-common)
echo "This TF_DIR uses a common state file, not env-specific"
STATE=${TF_DIR}/terraform.tfstate
;;
esac
}
import_remote_state_resources() {
local addr1 addr2 cmd1 cmd2
# Import the terraform remote state S3 bucket and remote state lock table.
# The commands will end up looking something like:
# terraform import $prefix.tf-state.aws_s3_bucket.tf-state $TERRAFORM_STATE_BUCKET
# terraform import \
# module.main.module.tf-state.aws_dynamodb_table.tf-lock-table $
# terraform import module.... ID_state_lock_table
if [ -z "${ID_state_module_prefix-}" ]; then
echo_red "error: ID_state_module_prefix is unset"
echo_red "This must be set to the terraform address of the module"
echo_red "where the remote state resources should be imported."
echo_red "For example, 'module.main.module.tf-state'"
return 5
fi
# Prepare commands for execution and prompt for confirmation / edits
while true; do
addr1="$ID_state_module_prefix.aws_s3_bucket.tf-state"
addr2="$ID_state_module_prefix.aws_dynamodb_table.tf-lock-table"
cmd1=(terraform import "$addr1" "$TERRAFORM_STATE_BUCKET")
cmd2=(terraform import "$addr2" "$ID_state_lock_table")
echo "About to run these terraform commands:"
echo " + ${cmd1[*]}"
echo " + ${cmd2[*]}"
if prompt_yn "Is this correct? (Answer N to edit)"; then
break
fi
read -rp "Terraform remote state S3 bucket name: " TERRAFORM_STATE_BUCKET
read -rp "Terraform remote state lock table name: " ID_state_lock_table
done
# Send output from these commands to the real stdout (FD 3)
run "${cmd1[@]}" >&3
run "${cmd2[@]}" >&3
echo_blue "Finished importing terraform remote state resources"
}
# Be sure ruby is working and bundler is setup since some terraform modules
# rely on erb templates and ruby to be working.
run bundle check >&2
check_release_versioning
if [ -n "$MODULE_STYLE" ]; then
style_description="module, separate subdirectory style"
load_env_module_style
else
style_description="shared, identity-devops-private, symlink style"
load_env_devops_private_style
fi
echo_blue >&2 "Finished loading environment variables"
echo_blue >&2 "The .terraform directory for $TF_DIR is: $style_description"
check_account_id
echo >&2 "Using state file: $STATE"
echo >&2 "Ensuring ${TF_DIR_FULL} is a terraform directory"
if [ ! -f "${TF_DIR_FULL}/main.tf" ] ; then
echo_red "deploy: not found: '${TF_DIR_FULL}/main.tf'"
echo_red "Are you sure '${TF_DIR_FULL}' is a terraform project folder?"
echo_red "Known examples include terraform-app, etc."
echo_red "Did you specify your username? This script no longer takes a "
echo_red "username argument."
die "error: Could not find terraform files"
fi
if [ -z "${TERRAFORM_STATE_BUCKET:=}" ] ; then
die "You must set the TERRAFORM_STATE_BUCKET environment variable. \
This should contain the name of the s3 bucket used to store terraform state for this run."
fi
if [ -z "${TERRAFORM_STATE_BUCKET_REGION:=}" ] ; then
die "You must set the TERRAFORM_STATE_BUCKET_REGION environment variable. \
This should contain the region of the s3 bucket used to store terraform state for this run."
fi
if [ -z "${ID_state_lock_table-}" ]; then
die "Must set ID_state_lock_table to dynamodb terraform state locking table"
fi
if [ -n "$MODULE_STYLE" ]; then
style_option="--module-style"
else
style_option="--shared-style"
fi
echo >&2 "Configuring state bucket $TERRAFORM_STATE_BUCKET with path $STATE"
run bin/configure_state_bucket.sh "$style_option" "$TERRAFORM_STATE_BUCKET" \
"$STATE" "$TF_DIR_FULL" "$TERRAFORM_STATE_BUCKET_REGION" "$ID_state_lock_table"
echo >&2 "+ cd $TF_DIR_FULL"
cd "$TF_DIR_FULL"
cat >&2 <<EOF
########################################
#
# Deploy environment:
# TF_DIR: $TF_DIR
# ENVIRONMENT: $ENVIRONMENT
#
########################################
EOF
echo_blue >&2 "Running terraform get"
run terraform get >&2
# If we are using variables from .tfvars files, splice in the -var-file options
# after the first element in TF_CMD, which will be the terraform command like
# plan/apply etc. (Only do this for terraform subcommands that accept
# -var-file. For terraform apply you can't pass vars if you also pass a plan
# file, so allow the caller to disable loading vars with ID_SKIP_LOADING_VARS.)
if [ -n "$MOVED_TO_TFVARS" ] && [ -z "${ID_SKIP_LOADING_VARS-}" ]; then
tf_subcommand="${TF_CMD[0]-}"
# these are the subcommands that accept -var-file
case "$tf_subcommand" in
apply|destroy|import|plan|refresh)
TF_CMD=(
"$tf_subcommand" "${tf_vars_opts[@]}" "${TF_CMD[@]:1}"
)
;;
esac
fi
if [ -n "$IMPORT_REMOTE_STATE" ]; then
# Import terraform remote state rather than running any terraform commands
import_remote_state_resources
echo_blue "All done"
exit
fi
echo >&2
echo_blue >&2 "Running terraform..."
# Run main terraform command. This is the only command that should print to
# stdout (reassigned to FD 3)
run terraform "${TF_CMD[@]}" >&3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment