Skip to content

Commit a35f434

Browse files
committed
feat: Add gcloud tofu code to automatically create gcs buckets to use as remote backend. Also setup workload identity federation, and store it to infisical, to use it in github actions.
Signed-off-by: Karteek <[email protected]>
1 parent 4c1ad3e commit a35f434

File tree

12 files changed

+358
-0
lines changed

12 files changed

+358
-0
lines changed

tofu/gcs-state/README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
## Overview
2+
3+
This OpenTofu module is responsible for provisioning the foundational infrastructure for managing OpenTofu state within Google Cloud Platform (GCP) and enabling secure CI/CD operations via GitHub Actions.
4+
5+
Key resources created and managed:
6+
7+
* **GCS Bucket for Tofu Remote State**: A Google Cloud Storage (GCS) bucket is created to serve as a centralized and secure backend for storing OpenTofu state files. This includes versioning and lifecycle rules for state backups.
8+
* **Service Account (`tofu-dev-sa`)**: A dedicated Google Cloud Service Account named `tofu-dev-sa` is provisioned.
9+
* **Purpose**: This service account is granted administrative permissions (`roles/storage.objectAdmin`) specifically on the GCS state bucket.
10+
* It is also configured to be impersonated by authorized users and, importantly, by GitHub Actions workflows via Workload Identity Federation.
11+
* **Secrets Management with Infisical**: This module interacts with Infisical for managing sensitive configurations. It's important to distinguish between secrets managed *by this module* and secrets that *must be manually set up by the user* in Infisical:
12+
* **User-Managed Secrets (to be created manually by the user in Infisical under the `/tofu` path in the `prod` Infisical environment):**
13+
* `TF_VAR_tofu_encryption_passphrase`: The passphrase for OpenTofu state encryption. (Refer to "Instructions" section on how to generate and where to store).
14+
* `TF_VAR_gcp_sa_dev_emails`: A JSON string array of user emails that are granted permission to impersonate the `tofu-dev-sa` service account (e.g., `'["[email protected]","[email protected]"]'`). (Refer to "Instructions" section on where to store).
15+
* **Module-Managed Secrets (automatically created/updated by this Tofu module in Infisical under the path specified by `var.infisical_rw_secrets_path` (default: `/tofu_rw`) in the `prod` Infisical environment or set a different path in .env file in root for TF_VAR_infisical_rw_secrets_path):**
16+
* `GCP_WORKLOAD_IDENTITY_PROVIDER`: The full Google Cloud resource name of the Workload Identity Provider created for GitHub Actions. This is used by GitHub Actions to authenticate to GCP.
17+
* `GCP_SERVICE_ACCOUNT_EMAIL`: The email address of the `tofu-dev-sa` service account. This is also used by GitHub Actions during authentication to specify which service account to impersonate.
18+
19+
## Workload Identity Federation for GitHub Actions
20+
21+
This configuration (primarily in `wif.tofu`) sets up Google Cloud Workload Identity Federation, offering significant benefits:
22+
23+
* **Enhanced Security:** Allows GitHub Actions to authenticate to Google Cloud and access resources (like the GCS state bucket) **without needing long-lived service account keys** stored as GitHub secrets. This is the Google-recommended best practice.
24+
* **Fine-grained Permissions:** The `tofu-dev-sa` service account has specific permissions (e.g., to manage the GCS state bucket). GitHub Actions only inherit these necessary permissions when they impersonate this service account.
25+
* **Auditable:** The impersonation events can be audited in Google Cloud.
26+
* **Infrastructure as Code:** The entire authentication mechanism for GitHub Actions is managed via OpenTofu, making it version-controlled, repeatable, and transparent.
27+
28+
After this OpenTofu configuration is applied, the `workload_identity_provider_name` and `tofu_dev_service_account_email` are pushed to Infisical (as `GCP_WORKLOAD_IDENTITY_PROVIDER` and `GCP_SERVICE_ACCOUNT_EMAIL` respectively, under the path defined by `var.infisical_rw_secrets_path`). Your GitHub Actions workflows (like `cf_adblock.yaml`) should then be configured to fetch these values from Infisical and use them in the `google-github-actions/auth` step for secure authentication to Google Cloud.
29+
30+
## Instructions
31+
32+
1. Setup [gcloud cli](/DEVCONTAINER.md).
33+
2. Setup *.auto.tfvars files.
34+
3. Setup .env file in root folder and commit it to git.
35+
4. Setup TF_VAR_tofu_encryption_passphrase as per instruction below and save them to infisical in `/tofu` directory (or the directory defined in TF_VAR_infisical_ro_secrets_path in .env file in root).
36+
```shell
37+
# TF_VAR_tofu_encryption_passphrase generation command
38+
openssl rand -base64 32
39+
```
40+
5. Setup TF_VAR_gcp_sa_dev_emails = ["[email protected]","[email protected]"] (emails you want to grant access to) below and save them to infisical in `/tofu` directory (or the directory defined in TF_VAR_infisical_ro_secrets_path in .env file in root).
41+
6. Follow devcontainers docs [here](/DEVCONTAINER.md). If done properly, all secrets from infisical will be available in the container environment.
42+
43+
44+
```shell
45+
# Initialize tofu
46+
tofu init
47+
```
48+
49+
```shell
50+
# Run tofu apply to create GCS bucket and permissions
51+
tofu apply
52+
```
53+
54+
```shell
55+
# Copy GCS backend file
56+
cp samples/backend_gcs.tofu.sample ./backend.tofu
57+
```
58+
59+
```shell
60+
# Re-initialize tofu to migrate state to GCS backend
61+
# Double check TF_VAR_gcs_env is properly set to your env - prod/staging/dev - everytime you checkout a new branch.
62+
tofu init -migrate-state
63+
```

tofu/gcs-state/gcs.tofu

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# 1. Create a Service Account named tofu-dev-sa
2+
resource "google_service_account" "tofu_dev_sa" {
3+
account_id = "tofu-dev-sa"
4+
display_name = "Tofu Dev Service Account"
5+
description = "Service account for development tasks using Tofu"
6+
}
7+
8+
# 2. Grant users the roles/iam.serviceAccountTokenCreator role on the tofu_dev_sa service account
9+
# This allows the specified users to impersonate the tofu-dev-sa service account.
10+
# Using for_each to iterate over the list of user email addresses.
11+
resource "google_service_account_iam_member" "tofu_dev_sa_token_creator" {
12+
# Use for_each to create an instance of this resource for each email in the list.
13+
# toset() is used to ensure a set of unique keys for for_each.
14+
for_each = toset(jsondecode(var.gcp_sa_dev_emails))
15+
service_account_id = google_service_account.tofu_dev_sa.name
16+
role = "roles/iam.serviceAccountTokenCreator"
17+
# Use each.value to reference the current email address in the iteration.
18+
member = "user:${each.value}"
19+
}
20+
21+
# 3. Create a GCS bucket
22+
# Bucket names must be globally unique.
23+
resource "google_storage_bucket" "tofu_remote_state" {
24+
name = var.bucket_name
25+
location = var.gcp_region
26+
storage_class = "STANDARD"
27+
28+
uniform_bucket_level_access = true
29+
# Enable public access prevention for the bucket
30+
public_access_prevention = "enforced"
31+
32+
# Enable versioning for objects in the bucket
33+
versioning {
34+
enabled = true
35+
}
36+
37+
# Add a lifecycle rule to delete noncurrent versions after 90 days
38+
lifecycle_rule {
39+
action {
40+
type = "Delete"
41+
}
42+
condition {
43+
age = 30 # Delete noncurrent versions older than 90 days
44+
num_newer_versions = 100 # Keep up to 100 newer versions
45+
with_state = "ARCHIVED" # Apply this rule to noncurrent versions
46+
}
47+
}
48+
}
49+
50+
# 4. Add Storage Object Admin permission for this bucket for tofu-dev-sa
51+
# This grants the tofu-dev_sa service account administrative permissions over objects in the bucket.
52+
resource "google_storage_bucket_iam_member" "tofu_remote_state_object_admin" { # Renamed the resource here
53+
bucket = google_storage_bucket.tofu_remote_state.name # Updated reference here
54+
role = "roles/storage.objectAdmin"
55+
member = "serviceAccount:${google_service_account.tofu_dev_sa.email}"
56+
}

tofu/gcs-state/infisical.tofu

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
provider "infisical" {
2+
host = var.infisical_domain
3+
auth = {
4+
universal = {
5+
client_id = var.infisical_client_id
6+
client_secret = var.infisical_client_secret
7+
}
8+
}
9+
}
10+
11+
locals {
12+
# Define the secrets to be created in Infisical
13+
# Keys are the names the secrets will have in Infisical
14+
# Values are the Tofu expressions for their content
15+
gcp_wif_secrets_to_infisical = {
16+
# Name the secrets as GitHub Actions will expect them
17+
"GCP_WORKLOAD_IDENTITY_PROVIDER" = google_iam_workload_identity_pool_provider.github_provider.name
18+
"GCP_SERVICE_ACCOUNT_EMAIL" = google_service_account.tofu_dev_sa.email
19+
}
20+
}
21+
22+
resource "infisical_secret" "gcp_wif_details" {
23+
for_each = local.gcp_wif_secrets_to_infisical
24+
25+
name = each.key # The name of the secret in Infisical
26+
value = each.value # The value of the secret
27+
28+
env_slug = "prod"
29+
folder_path = var.infisical_rw_secrets_path
30+
workspace_id = var.infisical_project_id
31+
32+
# Ensure this runs after the resources providing the values are created/updated
33+
depends_on = [
34+
google_iam_workload_identity_pool_provider.github_provider,
35+
google_service_account.tofu_dev_sa
36+
]
37+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
variable "infisical_domain" {
2+
description = "Infisical Domain"
3+
type = string
4+
default = "https://app.infisical.com"
5+
}
6+
7+
variable "infisical_client_id" {
8+
description = "Infisical Client ID"
9+
type = string
10+
default = null
11+
}
12+
13+
variable "infisical_project_id" {
14+
description = "Infisical Project ID"
15+
type = string
16+
default = null
17+
}
18+
19+
variable "infisical_rw_secrets_path" {
20+
description = "Infisical Client Secret"
21+
type = string
22+
default = "/tofu_rw"
23+
}
24+
25+
variable "infisical_client_secret" {
26+
description = "Infisical Client Secret"
27+
type = string
28+
sensitive = true
29+
default = null
30+
}

tofu/gcs-state/outputs.tofu

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# --- Outputs for Workload Identity Federation ---
2+
3+
output "workload_identity_provider_name" {
4+
description = "The full resource name of the Workload Identity Provider for GitHub Actions. Use this for the GCP_WORKLOAD_IDENTITY_PROVIDER GitHub secret."
5+
value = google_iam_workload_identity_pool_provider.github_provider.name
6+
# Example output: projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/YOUR_POOL_ID/providers/YOUR_PROVIDER_ID
7+
}
8+
9+
output "tofu_dev_service_account_email" {
10+
description = "The email of the service account that GitHub Actions will impersonate. Use this for the GCP_SERVICE_ACCOUNT_EMAIL GitHub secret."
11+
value = google_service_account.tofu_dev_sa.email
12+
}

tofu/gcs-state/providers.tofu

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
terraform {
2+
required_providers {
3+
google = {
4+
source = "hashicorp/google"
5+
version = "~> 6.33.0"
6+
}
7+
infisical = {
8+
source = "infisical/infisical"
9+
version = ">= 0.15.7"
10+
}
11+
external = {
12+
source = "hashicorp/external"
13+
version = ">= 2.3.4"
14+
}
15+
}
16+
encryption {
17+
key_provider "pbkdf2" "my_passphrase" {
18+
passphrase = var.tofu_encryption_passphrase
19+
}
20+
method "aes_gcm" "my_method" {
21+
keys = key_provider.pbkdf2.my_passphrase
22+
}
23+
state {
24+
method = method.aes_gcm.my_method
25+
enforced = true
26+
}
27+
plan {
28+
method = method.aes_gcm.my_method
29+
enforced = true
30+
}
31+
}
32+
}
33+
34+
# Configure the Google Cloud provider
35+
provider "google" {
36+
project = var.gcp_project_id
37+
region = var.gcp_region
38+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
terraform {
2+
backend "gcs" {
3+
bucket = var.bucket_name
4+
prefix = "gcs-state/${var.gcs_prefix_env}"
5+
}
6+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# The Google Cloud project ID to deploy resources into.
2+
gcp_project_id = "homelab-454718"
3+
4+
# Ideally, set TF_VAR_bucket_name in .env at root.
5+
# bucket_name = "kj-homelab-tf-state"
6+
7+
# The Google Cloud region to deploy resources into.
8+
gcp_region = "us-east1"

tofu/gcs-state/variables.tofu

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Define a variable for the Google Cloud project ID
2+
variable "gcp_project_id" {
3+
description = "The Google Cloud project ID to deploy resources into."
4+
type = string
5+
}
6+
# Define a variable for the Google Cloud region
7+
variable "gcp_region" {
8+
description = "The Google Cloud region to deploy resources into."
9+
type = string
10+
}
11+
12+
# Define a variable for the GCS bucket name.
13+
# Set this in .env file in root, which should automatically set it in devcontainer env.
14+
variable "bucket_name" {
15+
description = "The globally unique name for the GCS bucket."
16+
type = string
17+
}
18+
19+
# Define a variable for part of gcs prefix.
20+
# This should be set automatically based on branch logic in devcontainer.
21+
variable "gcs_env" {
22+
description = "Part of GCS prefix."
23+
type = string
24+
}
25+
26+
variable "tofu_encryption_passphrase" {
27+
description = "The encryption passphrase for tofu state encryption."
28+
type = string
29+
sensitive = true
30+
}
31+
32+
variable "gcp_sa_dev_emails" {
33+
description = "GCP SA Dev emails"
34+
type = string
35+
}

tofu/gcs-state/wif.auto.tfvars

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Workload Identity Federation Configuration for GitHub Actions
2+
wif_config = {
3+
github_owner = "karteekiitg" # Replace with your github username / org name
4+
github_repository = "homelab" # Replace with your GitHub repository name
5+
pool_id = "gh-actions-pool" # Choose a suitable ID for the WIF pool
6+
pool_display_name = "GitHub Actions WIF Pool" # Choose a display name for the pool
7+
provider_id = "gh-actions-provider" # Choose a suitable ID for the WIF provider
8+
provider_display_name = "GitHub Actions WIF Provider" # Choose a display name for the provider
9+
}

0 commit comments

Comments
 (0)