state: add SOPS-encrypted terraform state to git

- SOPS + age encrypts all 101 .tfstate files (JSON-aware: keys visible, values encrypted)
- scripts/state-sync: encrypt/decrypt/commit wrapper
- scripts/tg: auto-decrypt before ops, auto-encrypt+commit after apply/destroy
- terragrunt.hcl: -backup=- prevents backup file accumulation
- .gitignore: track .tfstate.enc, ignore plaintext .tfstate
- Cleaned 964MB of stale backups (state/backups/, .backup files)
This commit is contained in:
Viktor Barzin 2026-03-17 22:37:56 +00:00 committed by Viktor Barzin
parent 7ec627f365
commit 3583994efe
106 changed files with 279468 additions and 6 deletions

60
scripts/state-sync Executable file
View file

@ -0,0 +1,60 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
STATE_DIR="$REPO_ROOT/state/stacks"
cmd="${1:-help}"
stack="${2:-}" # optional: operate on single stack
encrypt_state() {
local dir="$1"
local src="$dir/terraform.tfstate"
local dst="$dir/terraform.tfstate.enc"
[ -f "$src" ] || return 0
# Only re-encrypt if state is newer than encrypted version
if [ ! -f "$dst" ] || [ "$src" -nt "$dst" ]; then
sops -e --input-type json --output-type json "$src" > "$dst"
fi
}
decrypt_state() {
local dir="$1"
local src="$dir/terraform.tfstate.enc"
local dst="$dir/terraform.tfstate"
[ -f "$src" ] || return 0
sops -d --input-type json --output-type json "$src" > "$dst"
}
case "$cmd" in
encrypt)
if [ -n "$stack" ]; then
encrypt_state "$STATE_DIR/$stack"
else
for dir in "$STATE_DIR"/*/; do
encrypt_state "$dir"
done
fi
;;
decrypt)
if [ -n "$stack" ]; then
decrypt_state "$STATE_DIR/$stack"
else
for dir in "$STATE_DIR"/*/; do
decrypt_state "$dir"
done
fi
;;
commit)
# Encrypt all changed state, then git add + commit
"$0" encrypt
cd "$REPO_ROOT"
git add state/stacks/*/terraform.tfstate.enc
if ! git diff --cached --quiet; then
git commit -m "state: update encrypted terraform state"
fi
;;
help)
echo "Usage: state-sync {encrypt|decrypt|commit} [stack-name]"
;;
esac

View file

@ -1,10 +1,36 @@
#!/usr/bin/env bash
# scripts/tg — wrapper: inject -auto-approve for non-interactive apply
# scripts/tg — wrapper: decrypt state before, encrypt+commit after mutating ops
# Usage: scripts/tg apply --non-interactive
# scripts/tg run --all -- plan
# Auth: `vault login -method=oidc` (token at ~/.vault-token)
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SYNC="$REPO_ROOT/scripts/state-sync"
# Determine stack name from cwd (relative to stacks/)
STACK_NAME=""
cwd="$(pwd)"
stacks_dir="$REPO_ROOT/stacks"
if [[ "$cwd" == "$stacks_dir"/* ]]; then
# Get first path component relative to stacks/
rel="${cwd#$stacks_dir/}"
STACK_NAME="${rel%%/*}"
fi
# Decrypt state before any operation
if [ -n "$STACK_NAME" ] && [ -f "$REPO_ROOT/state/stacks/$STACK_NAME/terraform.tfstate.enc" ]; then
"$SYNC" decrypt "$STACK_NAME"
fi
# Detect if this is a mutating operation
is_mutating=false
for arg in "$@"; do
case "$arg" in
apply|destroy|import|state) is_mutating=true ;;
esac
done
# If running apply with --non-interactive, add -auto-approve for Terraform
args=("$@")
has_apply=false
@ -25,7 +51,17 @@ if $has_apply && $has_non_interactive; then
new_args+=("-auto-approve")
fi
done
exec terragrunt "${new_args[@]}"
terragrunt "${new_args[@]}"
else
exec terragrunt "$@"
terragrunt "$@"
fi
# After mutating operations, encrypt and commit
if $is_mutating && [ -n "$STACK_NAME" ]; then
"$SYNC" encrypt "$STACK_NAME"
cd "$REPO_ROOT"
git add "state/stacks/$STACK_NAME/terraform.tfstate.enc"
if ! git diff --cached --quiet; then
git commit -m "state($STACK_NAME): update encrypted state"
fi
fi