diff --git a/.env.example b/.env.example index 7a857d3..0e0a283 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ export AWS_ACCESS_KEY_ID=<> export AWS_SECRET_ACCESS_KEY=<> +export OP_SERVICE_ACCOUNT_TOKEN=<> diff --git a/AGENTS.md b/AGENTS.md index 135a15f..8785bfb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,8 @@ This repo provisions Hetzner Cloud servers (web + accessories), networking, fire | `cdn.tf` | Bunny CDN pull zones and custom hostnames | | `outputs.tf` | Server IPs and SSH config output | | `cloudinit/` | Cloud-init templates (base.yml, web.yml, accessories.yml) | +| `scripts/setup` | Local bootstrap: tooling check, `.env` from 1Password, `terraform init` | +| `scripts/maintenance` | Toggle maintenance mode (relaxes LB health checks) | | `tests/` | Terraform native tests (`.tftest.hcl`) | ## Commands @@ -67,11 +69,11 @@ terraform fmt ### Sensitive Data - Secrets are managed via **1Password** using the Terraform `onepassword` provider -- Data sources in `secrets.tf` fetch secrets from the `fleetyards-infra` vault at plan/apply time -- Locally: requires 1Password desktop app running or `op signin` +- Data sources in `secrets.tf` fetch secrets from the `Fleetyards` vault at plan/apply time +- Locally: requires the 1Password desktop app with CLI integration enabled, or `op signin` - CI: uses `OP_SERVICE_ACCOUNT_TOKEN` GitHub Secret to authenticate with 1Password - AWS S3 backend credentials remain in GitHub Secrets (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) since the backend initializes before providers -- Locally, AWS S3 backend credentials are loaded from a `.env` file (gitignored) +- Locally, AWS S3 backend credentials and `OP_SERVICE_ACCOUNT_TOKEN` are loaded from a `.env` file (gitignored) — run `scripts/setup` to generate it from 1Password - `.tfstate` files are stored in the remote S3 backend - `.tfvars` files are gitignored — no secrets are committed to the repository diff --git a/README.md b/README.md index 4bb1b16..584afb6 100644 --- a/README.md +++ b/README.md @@ -50,15 +50,37 @@ All machines are `CX22`: 2 AMD vCPUs, 2 GB of RAM and 40 GB of SSD storage, runn The default setup of 1 web server and 1 accessory server will cost you around 9 EUR/month. -### How to use it - -1. Clone -2. Create a file `terraform.tfvars` with your credentials, it will look like this: -```terraform -hetzner_api_key = "your-api-key" -github_username = "your-github-username" # Make sure to add your SSH key to your GitHub account -ssh_key_name = "your-ssh-key-name" # The name of the SSH key on cloud.hetzner.de to be used for server access +### Setup + +All credentials live in the `Fleetyards` 1Password vault. No `terraform.tfvars` file is needed — provider credentials are fetched at plan/apply time by the `onepassword` provider. + +1. Install the tooling and sign in to 1Password: + +```bash +brew install terraform 1password-cli +# then, in the 1Password desktop app: +# Settings -> Developer -> "Integrate with 1Password CLI" ``` -4. Run `terraform init` -5. Run `terraform plan` (optional) -6. Run `terraform apply` + +2. Clone the repo and run the bootstrap script: + +```bash +scripts/setup +``` + +It verifies the tooling, writes a gitignored `.env` with the S3 backend credentials and the 1Password service account token, and runs `terraform init`. + +3. Load the credentials and pick a workspace: + +```bash +source .env +terraform workspace select stage # or live +terraform plan +terraform apply +``` + +The `.env` file is needed because the S3 backend initializes before any provider runs, so those two credentials cannot come from the `onepassword` provider itself. + +#### SSH access to existing servers + +Server logins are provisioned by cloud-init via `ssh_import_id: gh:`, which only runs when a server is **created** — and `user_data` changes are ignored by lifecycle rules. A new SSH key added to GitHub will therefore not reach servers that already exist. Either reuse your existing key or append the new public key to `~/.ssh/authorized_keys` for the `kamal` user on each server. diff --git a/scripts/setup b/scripts/setup new file mode 100755 index 0000000..9ce1d64 --- /dev/null +++ b/scripts/setup @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +set -euo pipefail + +VAULT="${OP_VAULT:-Fleetyards}" +S3_ITEM="HETZNER_S3" +TOKEN_ITEM="Service Account Auth Token: Fleetyards Write" + +cd "$(dirname "$0")/.." + +FORCE=false + +usage() { + echo "Usage: scripts/setup [--force]" + echo "" + echo " Bootstraps a local checkout: checks tooling, writes .env from 1Password," + echo " and runs terraform init." + echo "" + echo " --force - Overwrite an existing .env without asking" + echo "" + echo "Environment variables:" + echo " OP_VAULT - 1Password vault to read from (default: Fleetyards)" + exit 1 +} + +case "${1:-}" in + --force) FORCE=true ;; + "") ;; + *) usage ;; +esac + +require() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "==> Missing '$1'. Install it with: $2" + exit 1 + fi +} + +check_tooling() { + echo "==> Checking tooling..." + require terraform "brew install terraform" + require op "brew install 1password-cli" + + # Doubles as the auth check: with desktop integration this is what raises the + # unlock prompt, and 'op whoami' stays unsigned-in even once that succeeds. + local error + if ! error=$(op vault get "$VAULT" 2>&1 >/dev/null); then + echo "==> Cannot reach the '$VAULT' vault with the 1Password CLI:" + echo " ${error:-unknown error}" + echo "" + echo " Approve the unlock prompt in the 1Password desktop app, or enable" + echo " Settings -> Developer -> 'Integrate with 1Password CLI' and retry." + exit 1 + fi +} + +read_field() { + local item="$1" field="$2" value + value=$(op item get "$item" --vault "$VAULT" --fields "$field" --reveal 2>/dev/null) || true + if [ -z "$value" ]; then + echo "==> Could not read '$field' from '$item' in vault '$VAULT'." >&2 + exit 1 + fi + printf '%s' "$value" +} + +write_env() { + if [ -f .env ] && [ "$FORCE" = false ]; then + read -r -p "==> .env already exists. Overwrite? [y/N] " reply + case "$reply" in + y|Y) ;; + *) echo "==> Keeping existing .env."; return ;; + esac + fi + + echo "==> Writing .env from 1Password vault '$VAULT'..." + local access_key secret_key token + access_key=$(read_field "$S3_ITEM" access_key_id) + secret_key=$(read_field "$S3_ITEM" secret_access_key) + token=$(read_field "$TOKEN_ITEM" credential) + + cat > .env < Running terraform init..." + set -a + # shellcheck disable=SC1091 + . ./.env + set +a + terraform init -input=false +} + +check_tooling +write_env +init_terraform + +echo "" +echo "==> Done. Load the credentials into your shell and pick a workspace:" +echo " source .env" +echo " terraform workspace select stage # or live" +echo " terraform plan" diff --git a/terraform.tfvars.example b/terraform.tfvars.example deleted file mode 100644 index 279a9fa..0000000 --- a/terraform.tfvars.example +++ /dev/null @@ -1,3 +0,0 @@ -hetzner_api_key = "<>" -github_username = "<>" -ssh_key_name = "<>"