#!/bin/bash # # Terraform Cloud State Backup # Downloads all tfstate files from an organization's workspaces # Usage: ./backup-tfstate.sh [MAX_PARALLEL] # if [ $# -lt 2 ]; then echo "Usage: $0 [MAX_PARALLEL]" exit 1 fi command -v jq >/dev/null || { echo "Error: jq is required"; exit 1; } API_TOKEN="$1" ORG_NAME="$2" MAX_PARALLEL="${3:-10}" API_BASE="https://app.terraform.io/api/v2" BACKUP_DIR="./tfstate-backups-${ORG_NAME}-$(date +%Y%m%d-%H%M%S)" mkdir -p "$BACKUP_DIR" RESULTS_FILE="$BACKUP_DIR/.results" WORKSPACES_FILE="$BACKUP_DIR/.workspaces" > "$RESULTS_FILE" > "$WORKSPACES_FILE" echo "Fetching workspaces for: $ORG_NAME" page=1 while true; do url="${API_BASE}/organizations/${ORG_NAME}/workspaces?page%5Bnumber%5D=${page}&page%5Bsize%5D=100" response=$(curl -s -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/vnd.api+json" "$url") echo "$response" | jq -r '.data[] | "\(.id)\t\(.attributes.name)"' >> "$WORKSPACES_FILE" next=$(echo "$response" | jq -r '.meta.pagination["next-page"] // empty') [ -z "$next" ] && break page=$next done total=$(wc -l < "$WORKSPACES_FILE" | tr -d ' ') echo "Found $total workspaces" echo "Downloading states (max $MAX_PARALLEL parallel)..." echo "" download_state() { local ws_id="$1" local ws_name="$2" local safe_name=$(echo "$ws_name" | tr '/ ' '__') local output_file="${BACKUP_DIR}/${ORG_NAME}_${safe_name}.tfstate" local tmp_file="/tmp/state_${ws_id}.json" local state_http=$(curl -s -o "$tmp_file" -w "%{http_code}" \ -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/vnd.api+json" \ "${API_BASE}/workspaces/${ws_id}/current-state-version") if [ "$state_http" != "200" ]; then echo "[skip:$state_http] $ws_name" echo "skip" >> "$RESULTS_FILE" rm -f "$tmp_file" return fi local download_url=$(jq -r '.data.attributes["hosted-state-download-url"] // empty' "$tmp_file") rm -f "$tmp_file" if [ -z "$download_url" ]; then echo "[skip:no-url] $ws_name" echo "skip" >> "$RESULTS_FILE" return fi local http_code=$(curl -s -L -o "$output_file" -w "%{http_code}" -H "Authorization: Bearer $API_TOKEN" "$download_url") if [ "$http_code" = "200" ]; then echo "[done] $ws_name" echo "success" >> "$RESULTS_FILE" else echo "[fail:$http_code] $ws_name" echo "fail" >> "$RESULTS_FILE" rm -f "$output_file" fi } export -f download_state export API_TOKEN API_BASE BACKUP_DIR RESULTS_FILE ORG_NAME while IFS=$'\t' read -r ws_id ws_name; do download_state "$ws_id" "$ws_name" & while [ $(jobs -r | wc -l) -ge "$MAX_PARALLEL" ]; do sleep 0.1 done done < "$WORKSPACES_FILE" wait success_count=$(grep -c "success" "$RESULTS_FILE" 2>/dev/null) || success_count=0 skip_count=$(grep -c "skip" "$RESULTS_FILE" 2>/dev/null) || skip_count=0 fail_count=$(grep -c "fail" "$RESULTS_FILE" 2>/dev/null) || fail_count=0 echo "" echo "Complete: $BACKUP_DIR" echo " Total: $total" echo " Success: $success_count" echo " Skipped: $skip_count" echo " Failed: $fail_count" rm -f "$RESULTS_FILE" "$WORKSPACES_FILE"