feat: Added remote state storage in S3 bucket
This commit is contained in:
+95
-31
@@ -45,9 +45,10 @@ detect_tofu
|
||||
|
||||
# --- variable persistence --------------------------------------------------
|
||||
# Variables set via the "Set variables" menu (HCLOUD_TOKEN, BACKUP_S3_*,
|
||||
# DEPLOY_USER, ANSIBLE_SSH_PRIVATE_KEY_FILE) live in .control.env, gitignored,
|
||||
# so they survive between runs of this script. They are NOT exported into your
|
||||
# normal shell - only into control.sh's own process and whatever it runs.
|
||||
# TF_VAR_state_encryption_passphrase, DEPLOY_USER, ANSIBLE_SSH_PRIVATE_KEY_FILE)
|
||||
# live in .control.env, gitignored, so they survive between runs of this
|
||||
# script. They are NOT exported into your normal shell - only into control.sh's
|
||||
# own process and whatever it runs.
|
||||
load_control_env() {
|
||||
if [ -f "$CONTROL_ENV" ]; then
|
||||
set -a
|
||||
@@ -98,6 +99,8 @@ check_prereqs() {
|
||||
[ -n "${BACKUP_S3_ACCESS_KEY_ID:-}" ] && ok "BACKUP_S3_ACCESS_KEY_ID set" || warn "BACKUP_S3_ACCESS_KEY_ID not set"
|
||||
[ -n "${BACKUP_S3_SECRET_ACCESS_KEY:-}" ] && ok "BACKUP_S3_SECRET_ACCESS_KEY set" || warn "BACKUP_S3_SECRET_ACCESS_KEY not set"
|
||||
[ -n "${BACKUP_S3_ENDPOINT:-}" ] && ok "BACKUP_S3_ENDPOINT set" || warn "BACKUP_S3_ENDPOINT not set (omit only for real AWS S3)"
|
||||
[ -n "${TF_VAR_state_encryption_passphrase:-}" ] && ok "TF_VAR_state_encryption_passphrase set" \
|
||||
|| bad "TF_VAR_state_encryption_passphrase not set (needed to read/write tofu state - see Configuration > Set variables)"
|
||||
ok "DEPLOY_USER: ${DEPLOY_USER:-deploy} (default 'deploy' if unset)"
|
||||
|
||||
local key="${ANSIBLE_SSH_PRIVATE_KEY_FILE:-$HOME/.ssh/id_ed25519}"
|
||||
@@ -108,6 +111,8 @@ check_prereqs() {
|
||||
title "Files"
|
||||
[ -f "$INFRA_DIR/terraform.tfvars" ] && ok "infra/terraform.tfvars exists" \
|
||||
|| bad "infra/terraform.tfvars missing (Configuration > Copy terraform.tfvars.example)"
|
||||
[ -f "$INFRA_DIR/backend.hcl" ] && ok "infra/backend.hcl exists" \
|
||||
|| bad "infra/backend.hcl missing (Configuration > Copy backend.hcl.example)"
|
||||
[ -d "$INFRA_DIR/.terraform" ] && ok "infra/.terraform exists (tofu init has run)" \
|
||||
|| warn "infra/.terraform missing - run OpenTofu > init"
|
||||
if ansible-galaxy collection list hetzner.hcloud >/dev/null 2>&1; then ok "hetzner.hcloud collection installed"
|
||||
@@ -140,6 +145,7 @@ set_variables() {
|
||||
prompt_var BACKUP_S3_ACCESS_KEY_ID "S3 backup access key ID" true
|
||||
prompt_var BACKUP_S3_SECRET_ACCESS_KEY "S3 backup secret access key" true
|
||||
prompt_var BACKUP_S3_ENDPOINT "S3 backup endpoint (blank for real AWS S3)" false
|
||||
prompt_var TF_VAR_state_encryption_passphrase "OpenTofu state encryption passphrase (min 16 chars)" true
|
||||
ok "Variables saved."
|
||||
}
|
||||
|
||||
@@ -170,17 +176,69 @@ copy_tfvars() {
|
||||
fi
|
||||
}
|
||||
|
||||
# backend.hcl holds the S3 backend's bucket/key/endpoint (see
|
||||
# infra/backend.hcl.example) - it's the *same* bucket as BACKUP_S3_BUCKET,
|
||||
# just a different object key, so state never collides with backup archives.
|
||||
# Credentials aren't stored in it - see export_s3_backend_creds.
|
||||
copy_backend_hcl() {
|
||||
local example="$INFRA_DIR/backend.hcl.example"
|
||||
local target="$INFRA_DIR/backend.hcl"
|
||||
if [ -f "$target" ]; then
|
||||
warn "infra/backend.hcl already exists."
|
||||
confirm "Overwrite it with a fresh copy of the example?" || return 0
|
||||
fi
|
||||
cp "$example" "$target"
|
||||
ok "Copied backend.hcl.example -> backend.hcl"
|
||||
|
||||
if [ -n "${BACKUP_S3_BUCKET:-}" ]; then
|
||||
sed -i "s|^bucket = .*|bucket = \"${BACKUP_S3_BUCKET}\"|" "$target"
|
||||
ok "Set bucket = \"${BACKUP_S3_BUCKET}\" (from BACKUP_S3_BUCKET)"
|
||||
else
|
||||
warn "BACKUP_S3_BUCKET not set - bucket still has the example's placeholder value."
|
||||
fi
|
||||
if [ -n "${BACKUP_S3_ENDPOINT:-}" ]; then
|
||||
sed -i "s|^ s3 = .*| s3 = \"${BACKUP_S3_ENDPOINT}\"|" "$target"
|
||||
ok "Set endpoints.s3 = \"${BACKUP_S3_ENDPOINT}\" (from BACKUP_S3_ENDPOINT)"
|
||||
else
|
||||
warn "BACKUP_S3_ENDPOINT not set - endpoints.s3 still has the example's placeholder value."
|
||||
fi
|
||||
|
||||
local key
|
||||
read -rp "Object key/path for the state file within the bucket [tofu/terraform.tfstate]: " key
|
||||
key="${key:-tofu/terraform.tfstate}"
|
||||
sed -i "s|^key = .*|key = \"${key}\"|" "$target"
|
||||
ok "Set key = \"${key}\""
|
||||
|
||||
warn "Check infra/backend.hcl's region still matches your bucket's actual location before running tofu init."
|
||||
}
|
||||
|
||||
# --- OpenTofu actions -----------------------------------------------------
|
||||
# The s3 backend authenticates like the AWS CLI: standard AWS_ACCESS_KEY_ID /
|
||||
# AWS_SECRET_ACCESS_KEY env vars, not backend-block attributes. Re-use the
|
||||
# same backup bucket's credentials for both.
|
||||
export_s3_backend_creds() {
|
||||
[ -n "${BACKUP_S3_ACCESS_KEY_ID:-}" ] && export AWS_ACCESS_KEY_ID="$BACKUP_S3_ACCESS_KEY_ID"
|
||||
[ -n "${BACKUP_S3_SECRET_ACCESS_KEY:-}" ] && export AWS_SECRET_ACCESS_KEY="$BACKUP_S3_SECRET_ACCESS_KEY"
|
||||
}
|
||||
|
||||
require_tofu() {
|
||||
if [ -z "$TOFU" ]; then bad "No tofu/terraform binary found."; return 1; fi
|
||||
if [ ! -f "$INFRA_DIR/terraform.tfvars" ]; then
|
||||
bad "infra/terraform.tfvars is missing - copy terraform.tfvars.example first."; return 1
|
||||
fi
|
||||
if [ ! -f "$INFRA_DIR/backend.hcl" ]; then
|
||||
bad "infra/backend.hcl is missing - copy backend.hcl.example first."; return 1
|
||||
fi
|
||||
if [ -z "${TF_VAR_state_encryption_passphrase:-}" ]; then
|
||||
bad "TF_VAR_state_encryption_passphrase is not set - Configuration > Set variables."; return 1
|
||||
fi
|
||||
export_s3_backend_creds
|
||||
}
|
||||
tofu_init() { require_tofu && run_in "$INFRA_DIR" "$TOFU" init; }
|
||||
tofu_init() { require_tofu && run_in "$INFRA_DIR" "$TOFU" init -backend-config="$INFRA_DIR/backend.hcl"; }
|
||||
tofu_plan() { require_tofu && run_in "$INFRA_DIR" "$TOFU" plan; }
|
||||
tofu_apply() { require_tofu && run_in "$INFRA_DIR" "$TOFU" apply; }
|
||||
tofu_output() { require_tofu && run_in "$INFRA_DIR" "$TOFU" output; }
|
||||
tofu_refresh() { require_tofu && run_in "$INFRA_DIR" "$TOFU" apply -refresh-only; }
|
||||
tofu_destroy() {
|
||||
require_tofu || return 1
|
||||
confirm "${RED}Destroy ALL infra managed by OpenTofu?${RESET}" && run_in "$INFRA_DIR" "$TOFU" destroy
|
||||
@@ -218,7 +276,7 @@ accept SSH from the vpn server's public IP, so vpn must exist and you must be
|
||||
connected to it (WireGuard, via wg-easy) before dev/prod are reachable.
|
||||
|
||||
Steps, in order:
|
||||
0. Configuration: variables + terraform.tfvars (skip if already done)
|
||||
0. Configuration: variables + terraform.tfvars + backend.hcl (skip if already done)
|
||||
1. OpenTofu: init -> plan -> apply (creates servers, network, firewalls, DNS)
|
||||
2. Ansible: bootstrap + deploy + spinup for role_vpn
|
||||
3. YOU: visit https://vpn.luke-else.co.uk, complete wg-easy's setup,
|
||||
@@ -228,12 +286,15 @@ EOF
|
||||
confirm "Start the guided walkthrough?" || return 0
|
||||
|
||||
echo; title "0/4 Configuration"
|
||||
if confirm "Set/update variables now (HCLOUD_TOKEN, DEPLOY_USER, BACKUP_S3_*, SSH key path)?"; then
|
||||
if confirm "Set/update variables now (HCLOUD_TOKEN, DEPLOY_USER, BACKUP_S3_*, state passphrase, SSH key path)?"; then
|
||||
set_variables
|
||||
fi
|
||||
if [ ! -f "$INFRA_DIR/terraform.tfvars" ] && confirm "infra/terraform.tfvars is missing - create it from the example now?"; then
|
||||
copy_tfvars
|
||||
fi
|
||||
if [ ! -f "$INFRA_DIR/backend.hcl" ] && confirm "infra/backend.hcl is missing - create it from the example now?"; then
|
||||
copy_backend_hcl
|
||||
fi
|
||||
|
||||
echo; title "1/4 OpenTofu"
|
||||
if confirm "Run $TOFU init/plan/apply now?"; then
|
||||
@@ -270,19 +331,20 @@ menu() {
|
||||
echo " 2) Guided full setup (vpn -> connect -> dev/prod)"
|
||||
|
||||
title "Configuration"
|
||||
echo " 3) Set variables (HCLOUD_TOKEN, DEPLOY_USER, BACKUP_S3_*, SSH key path)"
|
||||
echo " 3) Set variables (HCLOUD_TOKEN, DEPLOY_USER, BACKUP_S3_*, state passphrase, SSH key path)"
|
||||
echo " 4) Copy terraform.tfvars.example -> terraform.tfvars"
|
||||
echo " 5) Copy backend.hcl.example -> backend.hcl"
|
||||
|
||||
title "OpenTofu (infra/)"
|
||||
echo " 5) init 6) plan 7) apply"
|
||||
echo " 8) output 9) destroy"
|
||||
echo " 6) init 7) plan 8) apply"
|
||||
echo " 9) output 10) refresh 11) destroy"
|
||||
|
||||
title "Ansible (ansible/)"
|
||||
echo " 10) Install collections 11) Show inventory"
|
||||
echo " 12) Bootstrap vpn 13) Deploy vpn 14) Spinup vpn"
|
||||
echo " 15) Bootstrap dev 16) Deploy dev 17) Spinup dev"
|
||||
echo " 18) Bootstrap prod 19) Deploy prod 20) Spinup prod"
|
||||
echo " 21) Spindown (choose target)"
|
||||
echo " 12) Install collections 13) Show inventory"
|
||||
echo " 14) Bootstrap vpn 15) Deploy vpn 16) Spinup vpn"
|
||||
echo " 17) Bootstrap dev 18) Deploy dev 19) Spinup dev"
|
||||
echo " 20) Bootstrap prod 21) Deploy prod 22) Spinup prod"
|
||||
echo " 23) Spindown (choose target)"
|
||||
|
||||
echo
|
||||
echo " 0) Quit"
|
||||
@@ -297,23 +359,25 @@ while true; do
|
||||
2) guided; pause ;;
|
||||
3) set_variables; pause ;;
|
||||
4) copy_tfvars; pause ;;
|
||||
5) tofu_init; pause ;;
|
||||
6) tofu_plan; pause ;;
|
||||
7) tofu_apply; pause ;;
|
||||
8) tofu_output; pause ;;
|
||||
9) tofu_destroy; pause ;;
|
||||
10) ansible_collections; pause ;;
|
||||
11) ansible_inventory; pause ;;
|
||||
12) ansible_bootstrap_vpn; pause ;;
|
||||
13) ansible_deploy_vpn; pause ;;
|
||||
14) ansible_spinup_vpn; pause ;;
|
||||
15) ansible_bootstrap_dev; pause ;;
|
||||
16) ansible_deploy_dev; pause ;;
|
||||
17) ansible_spinup_dev; pause ;;
|
||||
18) ansible_bootstrap_prod; pause ;;
|
||||
19) ansible_deploy_prod; pause ;;
|
||||
20) ansible_spinup_prod; pause ;;
|
||||
21) ansible_spindown; pause ;;
|
||||
5) copy_backend_hcl; pause ;;
|
||||
6) tofu_init; pause ;;
|
||||
7) tofu_plan; pause ;;
|
||||
8) tofu_apply; pause ;;
|
||||
9) tofu_output; pause ;;
|
||||
10) tofu_refresh; pause ;;
|
||||
11) tofu_destroy; pause ;;
|
||||
12) ansible_collections; pause ;;
|
||||
13) ansible_inventory; pause ;;
|
||||
14) ansible_bootstrap_vpn; pause ;;
|
||||
15) ansible_deploy_vpn; pause ;;
|
||||
16) ansible_spinup_vpn; pause ;;
|
||||
17) ansible_bootstrap_dev; pause ;;
|
||||
18) ansible_deploy_dev; pause ;;
|
||||
19) ansible_spinup_dev; pause ;;
|
||||
20) ansible_bootstrap_prod; pause ;;
|
||||
21) ansible_deploy_prod; pause ;;
|
||||
22) ansible_spinup_prod; pause ;;
|
||||
23) ansible_spindown; pause ;;
|
||||
0) echo "Bye."; exit 0 ;;
|
||||
"") ;;
|
||||
*) warn "Unknown option: $choice"; pause ;;
|
||||
|
||||
Reference in New Issue
Block a user