diff --git a/tofu/state/README.md b/tofu/state/README.md new file mode 100644 index 0000000..c1a1c5b --- /dev/null +++ b/tofu/state/README.md @@ -0,0 +1,34 @@ +# Google Cloud Storage + +Example configuration for creating [Google Cloud Storage](https://cloud.google.com/storage) bucket for storing an +encrypted Tofu state in. + +## Prerequisites + +Google Cloud Platform project with an active Billing Account. + +## Getting started + +1. Install `gcloud` — Instructions: https://cloud.google.com/sdk/docs/install-sdk +2. Run `gcloud init` +3. Authenticate using `gcloud auth application-default login` + +Before initialising, disable the GCS backend in `providers.tofu` (by e.g. commenting it out) since we can't store the +state in something that doesn't exist yet. + +After the initial `tofy apply` to create the bucket, the state can be migrated by adding + +```terraform +backend "gcs" { + bucket = var.bucket_name + prefix = var.state_prefix +} +``` + +back in and running + +```shell +tofu init -migrate-state +``` + +to migrate the state to the remote GCS bucket. \ No newline at end of file diff --git a/tofu/state/gcs/gcs.tofu b/tofu/state/gcs/gcs.tofu new file mode 100644 index 0000000..596edfc --- /dev/null +++ b/tofu/state/gcs/gcs.tofu @@ -0,0 +1,44 @@ +resource "google_service_account" "tofu_dev_sa" { + account_id = "tofu-dev-sa" + display_name = "Tofu Dev Service Account" + description = "Service account for development tasks using Tofu" +} + +# Bucket names must be globally unique. +resource "google_storage_bucket" "tofu_remote_state" { + name = var.bucket_name + location = var.gcp.region + storage_class = "STANDARD" + + uniform_bucket_level_access = true + public_access_prevention = "enforced" + + versioning { + enabled = true + } + + # `force_destroy = true` must be applied before removing bucket + force_destroy = true + + # Add a lifecycle rule to delete noncurrent versions after 90 days + lifecycle_rule { + action { + type = "Delete" + } + condition { + # Delete noncurrent versions older than 90 days + age = 30 + # Keep up to 100 newer versions + num_newer_versions = 100 + # Apply this rule to noncurrent versions + with_state = "ARCHIVED" + } + } +} + +# This grants the tofu-dev_sa service account administrative permissions over objects in the bucket. +resource "google_storage_bucket_iam_member" "tofu_remote_state_object_admin" { + bucket = google_storage_bucket.tofu_remote_state.name + role = "roles/storage.objectAdmin" + member = "serviceAccount:${google_service_account.tofu_dev_sa.email}" +} diff --git a/tofu/state/gcs/output.tofu b/tofu/state/gcs/output.tofu new file mode 100644 index 0000000..9bfaf19 --- /dev/null +++ b/tofu/state/gcs/output.tofu @@ -0,0 +1,7 @@ +output "tofu_service_account" { + value = { + id = google_service_account.tofu_dev_sa.id + name = google_service_account.tofu_dev_sa.name + email = google_service_account.tofu_dev_sa.email + } +} diff --git a/tofu/state/gcs/providers.tofu b/tofu/state/gcs/providers.tofu new file mode 100644 index 0000000..84f3063 --- /dev/null +++ b/tofu/state/gcs/providers.tofu @@ -0,0 +1,24 @@ +terraform { + required_providers { + google = { + source = "hashicorp/google" + version = "~> 6.44.0" + } + } + encryption { + key_provider "pbkdf2" "my_passphrase" { + passphrase = var.tofu_encryption_passphrase + } + method "aes_gcm" "my_method" { + keys = key_provider.pbkdf2.my_passphrase + } + state { + method = method.aes_gcm.my_method + enforced = true + } + plan { + method = method.aes_gcm.my_method + enforced = true + } + } +} diff --git a/tofu/state/gcs/variables.tofu b/tofu/state/gcs/variables.tofu new file mode 100644 index 0000000..2d7be9f --- /dev/null +++ b/tofu/state/gcs/variables.tofu @@ -0,0 +1,12 @@ +variable "bucket_name" { + description = "Globally unique name for the GCS bucket" + type = string +} + +variable "gcp" { + description = "Google Cloud project ID and region to deploy resources into" + type = object({ + project_id = string, + region = string + }) +} diff --git a/tofu/state/main.tofu b/tofu/state/main.tofu new file mode 100644 index 0000000..9ef47fd --- /dev/null +++ b/tofu/state/main.tofu @@ -0,0 +1,6 @@ +module "gcs" { + source = "./gcs" + + gcp = var.gcp + bucket_name = var.bucket_name +} diff --git a/tofu/state/output.tofu b/tofu/state/output.tofu new file mode 100644 index 0000000..a87abfe --- /dev/null +++ b/tofu/state/output.tofu @@ -0,0 +1,4 @@ +output "tofu_dev_service_account_email" { + description = "The email of the service account that GitHub Actions will impersonate. Use this for the GCP_SERVICE_ACCOUNT_EMAIL GitHub secret." + value = module.gcs.tofu_service_account.email +} diff --git a/tofu/state/providers.tofu b/tofu/state/providers.tofu new file mode 100644 index 0000000..a39fc6b --- /dev/null +++ b/tofu/state/providers.tofu @@ -0,0 +1,33 @@ +terraform { + required_providers { + google = { + source = "hashicorp/google" + version = "~> 6.44.0" + } + } + encryption { + key_provider "pbkdf2" "encryption_passphrase" { + passphrase = var.encryption_passphrase + } + method "aes_gcm" "encryption_method" { + keys = key_provider.pbkdf2.encryption_passphrase + } + state { + method = method.aes_gcm.encryption_method + enforced = true + } + plan { + method = method.aes_gcm.encryption_method + enforced = true + } + } + backend "gcs" { + bucket = var.bucket_name + prefix = var.state_prefix + } +} + +provider "google" { + project = var.gcp.project_id + region = var.gcp.region +} diff --git a/tofu/state/variables.auto.tfvars b/tofu/state/variables.auto.tfvars new file mode 100644 index 0000000..a9cfd32 --- /dev/null +++ b/tofu/state/variables.auto.tfvars @@ -0,0 +1,4 @@ +gcp = { + project_id = "homelab-359819", + region = "europe-west1" +} diff --git a/tofu/state/variables.tofu b/tofu/state/variables.tofu new file mode 100644 index 0000000..342eadc --- /dev/null +++ b/tofu/state/variables.tofu @@ -0,0 +1,24 @@ +variable "encryption_passphrase" { + description = "Encryption passphrase for Tofu state encryption" + type = string + sensitive = true +} + +variable "gcp" { + description = "Google Cloud project ID and region to deploy resources into" + type = object({ + project_id = string, + region = string + }) +} + +variable "bucket_name" { + description = "Globally unique name for GCS bucket" + type = string +} + +variable "state_prefix" { + description = "State prefix in GCS bucket" + type = string + default = "tofu/gcs-state" +}