Skip to content

Instantly share code, notes, and snippets.

@hoo29
Last active Aug 8, 2022
Embed
What would you like to do?
Bash script to migrate Terraform workspcae state from Hashicorp Terraform Cloud to Scalr.
#!/bin/bash
# This script was used to migrate Terraform workspace state from Hashicorp Terraform Cloud (TFC) to Scalr.
# It loops through all projects in a given directory, extracts the workspace id, downloads the state from TFC, and then uploads it to Scalr.
# It was made for the Ably Terraform projects directory structure and so will unlikely work as-is. It can easily be tweaked to support other folder structures.
# The workspaces in Scalr need to already exist.
set -e
set -o pipefail
# Call with ws_id as arg
function move_state() {
echo "$1"
echo "downloading state"
TFC_WS_ID=$(curl -sfS \
--header "Authorization: Bearer $TFC_TOKEN" \
--header "Content-Type: application/vnd.api+json" \
"https://app.terraform.io/api/v2/organizations/ablyrealtime/workspaces/${1}" | jq '.data.id' -r)
echo "tfc ws id $TFC_WS_ID"
SCALR_WS_ID=$(curl -sfS \
--header 'Prefer: profile=preview' \
--header 'Content-Type: application/vnd.api+json' \
--header "Authorization: Bearer $SCALR_TOKEN" \
"https://ablyrealtime.scalr.io/api/iacp/v3/workspaces?filter%5Bname%5D=${1}" | jq '.data[0].id' -r)
echo "scalr ws id $SCALR_WS_ID"
if [ "$SCALR_WS_ID" == "null" ]; then
echo "SCALR_WS_ID came back null"
exit 1
fi
STATE_DOWNLOAD_URL=$(curl -sfS \
--header "Authorization: Bearer $TFC_TOKEN" \
--header "Content-Type: application/vnd.api+json" \
"https://app.terraform.io/api/v2/workspaces/${TFC_WS_ID}/current-state-version" | jq '.data.attributes["hosted-state-download-url"]' -r)
OUTPUT_PATH="${1}.json"
curl -sfS "$STATE_DOWNLOAD_URL" -o "$OUTPUT_PATH"
SERIAL=$(cat "$OUTPUT_PATH" | jq '.serial' -r)
MD5=$(md5sum "$OUTPUT_PATH" | cut -d ' ' -f 1)
LINEAGE=$(cat "$OUTPUT_PATH" | jq '.lineage' -r)
B64_STATE=$(cat "$OUTPUT_PATH" | base64 -w 0)
echo "check existing scalr state"
EXISTING_SCALR_STATE=$(curl -sS \
--header 'Prefer: profile=preview' \
--header 'Content-Type: application/vnd.api+json' \
--header "Authorization: Bearer $SCALR_TOKEN" \
"https://ablyrealtime.scalr.io/api/iacp/v3/workspaces/${SCALR_WS_ID}/current-state-version")
SCALR_LINEAGE=$(echo "$EXISTING_SCALR_STATE" | jq '.data.attributes.lineage' -r)
SCALR_SERIAL=$(echo "$EXISTING_SCALR_STATE" | jq '.data.attributes.serial' -r)
SCALR_MD5=$(echo "$EXISTING_SCALR_STATE" | jq '.data.attributes.md5' -r)
if [ "$SCALR_LINEAGE" != "null" ] && [ "$SCALR_LINEAGE" != "$LINEAGE" ]; then
echo "scalr state has different lineage for ${1}, skipping"
return
fi
if [ "$SCALR_LINEAGE" != "null" ] && [ "$SERIAL" -eq "$SCALR_SERIAL" ]; then
echo "scalr state has the same serial for ${1}, skipping"
if [ "$SCALR_MD5" != "$MD5" ]; then
echo "WARNING state serials were the same but checksum is different for ${1}, what is different?"
fi
return
fi
if [ "$SCALR_LINEAGE" != "null" ] && [ "$SERIAL" -lt "$SCALR_SERIAL" ]; then
echo "scalr state has larger serial for ${1}, skipping"
return
fi
echo "uploading new state"
# Save to file because some states are big
# You can add force if needed \"force\" : \"true\", \
echo -n "{ \
\"data\": { \
\"type\": \"state-versions\", \
\"attributes\" : { \
\"serial\" : \"${SERIAL}\", \
\"md5\" : \"${MD5}\", \
\"lineage\" : \"${LINEAGE}\", \
\"state\" : \"${B64_STATE}\" \
} \
} \
}" >curl_payload.json
curl --fail-with-body \
--header "Authorization: Bearer $SCALR_TOKEN" \
--header "Content-Type: application/vnd.api+json" \
--request POST \
--data @curl_payload.json \
"https://ablyrealtime.scalr.io/api/tfe/v2/workspaces/${SCALR_WS_ID}/state-versions"
}
function main() {
# Top level folder containing your terraform projects (assumes monorepo setup)
BASE_DIR=REPLACE_ME
if [ -z "$TFC_TOKEN" ]; then
echo "TFC_TOKEN not set"
exit 1
fi
if [ -z "$SCALR_TOKEN" ]; then
echo "SCALR_TOKEN not set"
exit 1
fi
# Loops through folders in format BASE_DIR/TERRAFORM_PROJECT/ENVIRONMENT
for SUB_DIR in "BASE_DIR"/*/; do
if [ -d "$SUB_DIR" ]; then
for TENV in "$SUB_DIR"*/; do
if [ -d "$TENV" ] && test -f "${TENV}main.tf"; then
echo "$TENV"
WS_NAME=$(tr '\n' ' ' <"${TENV}main.tf" | sed -ne 's#^.*workspaces\s*{\s*name\s*=\s*"\([^"]*\)".*$#\1#p')
if [ -z "$WS_NAME" ]; then
echo "WS_NAME was null for $TENV, skipping"
else
echo "moving state"
move_state "$WS_NAME"
fi
fi
done
fi
done
echo "done"
}
main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment