diff --git a/.gitignore b/.gitignore index 9dcd6cc..3b8a05c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.bck + # sops /secrets.yaml @@ -21,12 +23,13 @@ hetzner.nix crash.log crash.*.log -# Ignore CLI configuration files -.terraformrc -terraform.rc - # Ignore local .tfvars *.tfvars # generated terraform files *.json + +# Ignore CLI configuration files +*.tfrc +.terraformrc +terraform.rc diff --git a/.woodpecker.yml b/.woodpecker.yml index 8cae8dc..f355799 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,19 +1,9 @@ steps: validate: - image: alpine:3.16 + image: nixos/nix:2.19.2 # when: # event: pull_request commands: - - apk add --no-cache terraform - - terraform version - - | - cat << EOF > terraform.rc - credentials "app.terraform.io" { - token = "$TERRAFORM_CLOUD_TOKEN" - } - EOF - - terraform init - - terraform validate - - terraform plan -var "" - secrets: [ terraform_cloud_token ] + - NIX_CONFIG="experimental-features = nix-command flakes" nix run .#plan + secrets: [ sops_age_key ] diff --git a/README.md b/README.md index 6cb5561..ac18ad7 100644 --- a/README.md +++ b/README.md @@ -5,24 +5,95 @@ Contains [Terraform](https://terraform.io/) code used to manage our infrastructu ## Prerequisites - [Nix](https://nix.dev/) with [Flakes](https://nixos.wiki/wiki/Flakes) enabled -- [Hetzner Cloud API token](https://docs.hetzner.com/cloud/api/getting-started/generating-api-token) -- [Terraform Cloud](https://app.terraform.io/) to use shared state +- Credentials (see [configuring](#configuring)), if not using the [shared secrets](#secrets): + - `tf_cloud_token`: [Terraform Cloud](https://app.terraform.io/) token to use shared state + - `hcloud_api_token`: [Hetzner Cloud API token](https://docs.hetzner.com/cloud/api/getting-started/generating-api-token) -### Usage +## Usage -- Run `nix develop -c $SHELL` to enter the development environment if not using [`direnv`](https://zero-to-flakes.com/direnv). -- Run `tofu login app.terraform.io` to log in to the Terraform Cloud backend -- Run `nix run` to apply changes. -- Run `nix flake update` to update dependencies. +- Before issuing any other commands, enter the development environment (if not using [`direnv`](https://zero-to-flakes.com/direnv)): + + ```sh + nix develop -c $SHELL + ``` + +- Applying changes: + + ```sh + nix run + ``` + +- Validating logic: + + ```sh + nix run .#check + ``` + +- Showing the generated plan: + + ```sh + nix run .#plan + ``` + +- Applying changes, approving automatically: + + ```sh + nix run .#cd + ``` + +- Removing local state and derived credentials: + + ```sh + nix run .#destroy + ``` + +- Updating dependencies: + + ```sh + nix flake update + ``` + +- Simulating a CI test ([substituting](#secrets) ``): + + ```sh + woodpecker-cli exec --env "SOPS_AGE_KEY=" + ``` ### Secrets - if you want to reset secrets: - - generate an [`age`](https://age-encryption.org/) key pair, using [`rage`](https://github.com/str4d/rage) installed as part of the nix shell: `rage-keygen -o keys.txt` + - generate an [`age`](https://age-encryption.org/) key pair, using [`rage`](https://github.com/str4d/rage) installed as part of the nix shell: + + ```sh + rage-keygen -o keys.txt + ``` + - list it in [`sops`](https://getsops.io/) config file `.sops.yaml` - key setup: set environment variable `SOPS_AGE_KEY_FILE` or `SOPS_AGE_KEY` so `sops` can locate the secret key to an `age` key pair that has its public key listed in `.sops.yaml` -- encoding secrets: `sops -e secrets.yaml > secrets.enc.yaml` -- decoding secrets: `sops -d secrets.enc.yaml > secrets.yaml` +- encoding secrets: + + ```sh + sops -e secrets.yaml > secrets.enc.yaml + ``` + +- decoding secrets: + + ```sh + sops -d secrets.enc.yaml > secrets.yaml + ``` + +- setting Terraform Cloud credentials, either by: + - reusing the shared session: + + ```sh + source login.sh + ``` + + - log in to the Terraform Cloud backend: + + ```sh + tofu login app.terraform.io + ``` ### Configuring diff --git a/flake.nix b/flake.nix index 0e68de7..3203fa3 100644 --- a/flake.nix +++ b/flake.nix @@ -46,6 +46,8 @@ treefmt sops rage + woodpecker-cli + jq inputs.terranix.defaultPackage.${system} (opentofu.withPlugins (p: with p; [ sops # https://registry.terraform.io/providers/carlpett/sops/latest/docs @@ -57,6 +59,13 @@ apps = let tfCommand = cmd: '' if [[ -e config.tf.json ]]; then rm -f config.tf.json; fi; + export TERRAFORM_CLOUD_TOKEN=$(${pkgs.sops}/bin/sops -d --extract '["tf_cloud_token"]' secrets.enc.yaml) + export TF_CLI_CONFIG_FILE="ci.tfrc" + cat << EOF > "$TF_CLI_CONFIG_FILE" + credentials "app.terraform.io" { + token = "$TERRAFORM_CLOUD_TOKEN" + } + EOF cp ${terraformConfiguration} config.tf.json \ && ${tf} init \ && ${tf} ${cmd} @@ -78,6 +87,8 @@ ${tfCommand "destroy"} rm ${toString ./.}/config.tf.json rm ${toString ./.}/terraform.tfstate* + rm ${toString ./.}/secrets.yaml + rm ${toString ./.}/ci.tfrc ''; }; diff --git a/secrets.enc.yaml b/secrets.enc.yaml index d1483ff..e135161 100644 --- a/secrets.enc.yaml +++ b/secrets.enc.yaml @@ -1,4 +1,5 @@ -hcloud_api_token: ENC[AES256_GCM,data:vXsyffsjp5yiMWepyq8KNR8fJNbMB1sj1wAvc7eJm5smysww+Jm8sNCso8dn0X3M4eMCUx9SZUJx3qVh6Mr6Kw==,iv:RQK3mvKPtUmFSZjvlkh9Iffv8vYeEV+G95JcCp88W1o=,tag:CSsVCl35muqlTq5VYvsZIQ==,type:str] +tf_cloud_token: ENC[AES256_GCM,data:3vx1n4s7eQxMR2ntOlmnASUuCMxhMMHKLuhf644mNLWbv99aPLsqoUQ+cP01hW/Ra98v3U0C0uYZWfkFn/X8CaVIeu1QPv12D1+XSJB0SJal8NZHJTNVTgzL,iv:W0H4lftTD96/ENjV8tA2a8QqAGI2z/jRvgMtQmaGeB0=,tag:MuXGtyDTbRlNW1xshtCH0g==,type:str] +hcloud_api_token: ENC[AES256_GCM,data:HojFdI9gGnO8IkfOREx4bTqrCNBsCDxnUUOmb+VuLMNIEWEifo9tBhm25I+xAogRd0TuYcY4fkARboGL9qsgrw==,iv:18QLpHdNnG82603FxLL38KJaB9sPJ9gj0vmqQWNb1e0=,tag:/6QPhVZy5P5dvP92HUQR6g==,type:str] sops: kms: [] gcp_kms: [] @@ -8,14 +9,14 @@ sops: - recipient: age1d53yeje0ggysc93uptlpufyhpchyyfs006368j8mw9r20uyeeydse3n7aw enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzdytpNTZ3M3JpWlBKR1Nr - S0xXK2krNDVvRk5vYzl1dEhUYkdtZDN4S3pnCkFFanIraGZlelJTVGpwOXBKM1dO - Q0lXcnZPU0dtUDNUV2NIMHhDL3pPUmMKLS0tIDlpV25BRnlXMm1pNk81bDFySk5B - M1Q2Q2V4RUxxVGpIRVc4aHZCUzVHaXcKePAXhBPzLQnfzhklXqY2uM9vBQqh4ZAS - EglXonol4QVUJNBj6hMwepUeeeyLw5foWxJbBwzfdyPWdeSM+zkctw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHa2EzMFlLdTQvMUtzL3FL + Rk5JZEdoVkpEQkk3eTR6QkFKY01CaGVwUnpFCm00OWg2bmJ3U2xRMExyeFZ6WVRB + UWVjVzY5Y01EOUpDNHYrMFYzVE9GUUEKLS0tIE1LYm80b3V3OUkrNWxQVTRaRGhk + TkxRZlprc0I3Q3dQRS82bEd4b1VxTUkKvHZc4c7+9Tsny8w5Cm5L6H+enU1R0tY4 + 9OcNPXGv8II5OJp1eT14U/sNecPbiBaQSeK4xHaRDKbGyqx92DtQ8A== -----END AGE ENCRYPTED FILE----- - lastmodified: "2024-01-16T16:09:37Z" - mac: ENC[AES256_GCM,data:H1IXPgNzGwnbz27kc/M9kfsWLFWX7pWLfpPU3F6LDz3c76Ap8kgjlwc52r2thOfQhky14iaZgP+9EqAL7wP7WK3ZcN18mq9PHePsqAIQBkb8+80YFcEBel8yUPJFUFpeJGq4Ty+JBbADY9hbJKteLvkoOA0BaeIckMkAQXNB7nU=,iv:+XgndQValWgwSL+16Zr/q7aeQpxWvmNQ2ECD7298MX8=,tag:txLiO/MilTx9H6GoSCHxZg==,type:str] + lastmodified: "2024-01-16T21:29:11Z" + mac: ENC[AES256_GCM,data:eIoSEDuND1O5XPisSs/kq7N1UsiZMer9+Ok43o+8HwfH/HAoElM/0fXNhKQWcQQVUdwLIQnJZzHEXIJ77Uh5sDsWynj3ihJBhruDPu3FxOXTvRHBcdxU31b3iQGliaChRD19L2GDhsNO2Pfvhpoovsy2PHoFtpqtYt4+7UmcOCw=,iv:Zz1czzz+3Tb5f81o6adhO7eJSSr+ksXhMQwendPAhM0=,tag:bjF2pXTkGxef9+1kKw0FlQ==,type:str] pgp: [] unencrypted_suffix: _unencrypted version: 3.8.1