strip cluster bootstraps
All checks were successful
AI Code Review / ai-review (pull_request) Successful in 59s

This commit is contained in:
2026-04-27 21:34:11 +02:00
parent 0353803d4f
commit 96dde22884
42 changed files with 65 additions and 2338 deletions

View File

@@ -12,30 +12,6 @@ resource "google_project_service" "container" {
disable_on_destroy = false
}
resource "google_project_service" "sqladmin" {
project = var.project_id
service = "sqladmin.googleapis.com"
disable_on_destroy = false
}
resource "google_project_service" "servicenetworking" {
project = var.project_id
service = "servicenetworking.googleapis.com"
disable_on_destroy = false
}
resource "google_project_service" "redis" {
project = var.project_id
service = "redis.googleapis.com"
disable_on_destroy = false
}
resource "google_project_service" "iam" {
project = var.project_id
service = "iam.googleapis.com"
disable_on_destroy = false
}
# ─── Networking ───────────────────────────────────────────────────────
resource "google_compute_network" "main" {
@@ -65,31 +41,11 @@ resource "google_compute_subnetwork" "main" {
}
}
# Private IP range for Cloud SQL VPC peering
resource "google_compute_global_address" "private_ip_range" {
project = var.project_id
name = "${var.prefix}-private-ip-range"
purpose = "VPC_PEERING"
address_type = "INTERNAL"
prefix_length = 20
network = google_compute_network.main.id
depends_on = [google_project_service.compute]
}
resource "google_service_networking_connection" "private_vpc_connection" {
network = google_compute_network.main.id
service = "servicenetworking.googleapis.com"
reserved_peering_ranges = [google_compute_global_address.private_ip_range.name]
depends_on = [google_project_service.servicenetworking]
}
# ─── GKE Cluster ──────────────────────────────────────────────────────
#
# Regional cluster (3 control-plane replicas) for HA.
# Workload Identity enabled — allows K8s service accounts to impersonate
# Google Service Accounts for keyless GCS access.
# Google Service Accounts for keyless access to GCP services.
resource "google_container_cluster" "main" {
project = var.project_id
@@ -157,285 +113,3 @@ resource "google_container_node_pool" "main" {
auto_upgrade = true
}
}
# ─── Cloud SQL PostgreSQL ─────────────────────────────────────────────
#
# Private IP only — reachable from GKE via VPC peering.
# NOTE: Cloud SQL doesn't support Terraform-managed local user creation.
# Users (keycloak, gitlab) must be created post-provision via psql.
# Use: kubectl run pg-init --rm -it --image=postgres:16 -- psql -h <private_ip> -U pgadmin
resource "random_password" "pg_admin" {
length = 32
special = false
}
resource "random_password" "pg_keycloak" {
length = 32
special = false
}
resource "random_password" "pg_gitlab" {
length = 32
special = false
}
resource "google_sql_database_instance" "main" {
project = var.project_id
name = "${var.prefix}-postgresql"
region = var.region
database_version = var.pg_database_version
settings {
tier = var.pg_tier
availability_type = var.pg_availability_type
disk_size = var.pg_disk_size_gb
disk_autoresize = true
ip_configuration {
ipv4_enabled = false # private IP only
private_network = google_compute_network.main.id
enable_private_path_for_google_cloud_services = true
}
backup_configuration {
enabled = var.pg_backup_enabled
}
database_flags {
name = "max_connections"
value = "200"
}
}
deletion_protection = var.pg_deletion_protection
depends_on = [google_service_networking_connection.private_vpc_connection]
}
resource "google_sql_user" "pg_admin" {
project = var.project_id
name = "pgadmin"
instance = google_sql_database_instance.main.name
password = random_password.pg_admin.result
}
resource "google_sql_database" "keycloak" {
project = var.project_id
name = "keycloak"
instance = google_sql_database_instance.main.name
}
resource "google_sql_database" "gitlab" {
project = var.project_id
name = "gitlabhq_production"
instance = google_sql_database_instance.main.name
}
# ─── Cloud Memorystore (Redis) ────────────────────────────────────────
#
# Private IP within VPC. Auth enabled (password via AUTH command).
# The auth_string is output and must be stored in a K8s secret for GitLab.
resource "google_redis_instance" "main" {
project = var.project_id
name = "${var.prefix}-redis"
region = var.region
tier = var.redis_tier
memory_size_gb = var.redis_memory_size_gb
authorized_network = google_compute_network.main.id
# Redis AUTH password — keyless access is not supported by Memorystore
auth_enabled = true
labels = var.labels
depends_on = [google_project_service.redis]
}
# ─── GCS Buckets (GitLab Object Storage) ─────────────────────────────
#
# GitLab supports GCS natively via the Fog/Google provider.
# Workload Identity is used for keyless access — no access key required.
# NOTE: GCS bucket names are globally unique. If "${prefix}-gitlab-*" conflicts,
# adjust var.prefix to include a project-specific component.
locals {
gcs_bucket_prefix = "${var.prefix}-gitlab"
}
resource "google_storage_bucket" "gitlab_artifacts" {
project = var.project_id
name = "${local.gcs_bucket_prefix}-artifacts"
location = var.region
storage_class = var.gcs_storage_class
force_destroy = true
uniform_bucket_level_access = true
labels = var.labels
}
resource "google_storage_bucket" "gitlab_uploads" {
project = var.project_id
name = "${local.gcs_bucket_prefix}-uploads"
location = var.region
storage_class = var.gcs_storage_class
force_destroy = true
uniform_bucket_level_access = true
labels = var.labels
}
resource "google_storage_bucket" "gitlab_packages" {
project = var.project_id
name = "${local.gcs_bucket_prefix}-packages"
location = var.region
storage_class = var.gcs_storage_class
force_destroy = true
uniform_bucket_level_access = true
labels = var.labels
}
resource "google_storage_bucket" "gitlab_lfs" {
project = var.project_id
name = "${local.gcs_bucket_prefix}-lfs"
location = var.region
storage_class = var.gcs_storage_class
force_destroy = true
uniform_bucket_level_access = true
labels = var.labels
}
resource "google_storage_bucket" "gitlab_registry" {
project = var.project_id
name = "${local.gcs_bucket_prefix}-registry"
location = var.region
storage_class = var.gcs_storage_class
force_destroy = true
uniform_bucket_level_access = true
labels = var.labels
}
resource "google_storage_bucket" "gitlab_backups" {
project = var.project_id
name = "${local.gcs_bucket_prefix}-backups"
location = var.region
storage_class = var.gcs_storage_class
force_destroy = true
uniform_bucket_level_access = true
labels = var.labels
}
# ─── Google Identity Provider for Keycloak ────────────────────────────
#
# Keycloak federates with Google — users authenticate via "Sign in with Google"
# through Keycloak, which remains the single OIDC issuer for all services.
#
# IMPORTANT: The Google OAuth 2.0 client (Web Application type) must be
# created MANUALLY in Google Cloud Console:
# APIs & Services → Credentials → Create OAuth client ID → Web application
# Authorized redirect URIs: https://keycloak.<domain>/realms/devops/broker/google/endpoint
#
# After creation, fill in k8s/scripts/gcp-{dev,prod}/gcp-idp.env:
# GOOGLE_IDP_CLIENT_ID=<client-id>
# GOOGLE_IDP_CLIENT_SECRET=<client-secret>
#
# Then run: ./setup-keycloak.sh --env gcp-dev idp
# Enable Google Identity Platform API for documentation purposes
resource "google_project_service" "oauth2" {
project = var.project_id
service = "oauth2.googleapis.com"
disable_on_destroy = false
}
# ─── Workload Identity for GitLab ─────────────────────────────────────
#
# Allows GitLab pods (webservice, sidekiq) to access GCS buckets without
# a service account key. The K8s service account "gitlab" in the "gitlab"
# namespace exchanges its projected OIDC token for a Google token.
#
# GKE must have workload_identity_config set (done above).
resource "google_service_account" "gitlab" {
project = var.project_id
account_id = "${var.prefix}-gitlab"
display_name = "GitLab Service Account (Workload Identity)"
depends_on = [google_project_service.iam]
}
# Grant the GSA Object Admin on all GitLab buckets
resource "google_storage_bucket_iam_member" "gitlab_artifacts" {
bucket = google_storage_bucket.gitlab_artifacts.name
role = "roles/storage.objectAdmin"
member = "serviceAccount:${google_service_account.gitlab.email}"
}
resource "google_storage_bucket_iam_member" "gitlab_uploads" {
bucket = google_storage_bucket.gitlab_uploads.name
role = "roles/storage.objectAdmin"
member = "serviceAccount:${google_service_account.gitlab.email}"
}
resource "google_storage_bucket_iam_member" "gitlab_packages" {
bucket = google_storage_bucket.gitlab_packages.name
role = "roles/storage.objectAdmin"
member = "serviceAccount:${google_service_account.gitlab.email}"
}
resource "google_storage_bucket_iam_member" "gitlab_lfs" {
bucket = google_storage_bucket.gitlab_lfs.name
role = "roles/storage.objectAdmin"
member = "serviceAccount:${google_service_account.gitlab.email}"
}
resource "google_storage_bucket_iam_member" "gitlab_registry" {
bucket = google_storage_bucket.gitlab_registry.name
role = "roles/storage.objectAdmin"
member = "serviceAccount:${google_service_account.gitlab.email}"
}
resource "google_storage_bucket_iam_member" "gitlab_backups" {
bucket = google_storage_bucket.gitlab_backups.name
role = "roles/storage.objectAdmin"
member = "serviceAccount:${google_service_account.gitlab.email}"
}
# Bind the K8s service account "gitlab/gitlab" to the GSA via Workload Identity.
# The GitLab Helm chart creates the "gitlab" SA when global.serviceAccount.enabled=true.
resource "google_service_account_iam_member" "gitlab_workload_identity" {
service_account_id = google_service_account.gitlab.name
role = "roles/iam.workloadIdentityUser"
member = "serviceAccount:${var.project_id}.svc.id.goog[gitlab/gitlab]"
}
# ─── External-DNS Workload Identity ──────────────────────────────────
# Allows external-dns to manage Cloud DNS records for the cluster's domain.
# The K8s service account "external-dns/external-dns" exchanges its OIDC token
# for a Google token via Workload Identity.
resource "google_service_account" "external_dns" {
project = var.project_id
account_id = "${var.prefix}-external-dns"
display_name = "External-DNS Service Account (Workload Identity)"
depends_on = [google_project_service.iam]
}
resource "google_project_iam_member" "external_dns_dns_admin" {
project = var.project_id
role = "roles/dns.admin"
member = "serviceAccount:${google_service_account.external_dns.email}"
}
resource "google_service_account_iam_member" "external_dns_workload_identity" {
service_account_id = google_service_account.external_dns.name
role = "roles/iam.workloadIdentityUser"
member = "serviceAccount:${var.project_id}.svc.id.goog[external-dns/external-dns]"
}

View File

@@ -14,75 +14,3 @@ output "region" {
description = "GCP region"
value = var.region
}
# ─── PostgreSQL ───────────────────────────────────────────────────────
output "pg_host" {
description = "Cloud SQL private IP address (reachable from GKE via VPC)"
value = google_sql_database_instance.main.private_ip_address
}
output "pg_port" {
description = "PostgreSQL port"
value = 5432
}
output "pg_admin_login" {
description = "PostgreSQL administrator login"
value = google_sql_user.pg_admin.name
}
output "pg_admin_password" {
description = "PostgreSQL administrator password"
value = random_password.pg_admin.result
sensitive = true
}
output "pg_keycloak_password" {
description = "Pre-generated password for keycloak DB user — create user post-provision"
value = random_password.pg_keycloak.result
sensitive = true
}
output "pg_gitlab_password" {
description = "Pre-generated password for gitlab DB user — create user post-provision"
value = random_password.pg_gitlab.result
sensitive = true
}
# ─── Redis ────────────────────────────────────────────────────────────
output "redis_host" {
description = "Memorystore Redis host (private IP within VPC)"
value = google_redis_instance.main.host
}
output "redis_port" {
description = "Memorystore Redis port"
value = google_redis_instance.main.port
}
output "redis_auth_string" {
description = "Memorystore Redis AUTH string — store in gitlab-redis-secret K8s secret"
value = google_redis_instance.main.auth_string
sensitive = true
}
# ─── GCS ─────────────────────────────────────────────────────────────
output "gitlab_gcs_bucket_prefix" {
description = "GCS bucket name prefix — buckets are {prefix}-artifacts, {prefix}-uploads, etc."
value = local.gcs_bucket_prefix
}
# ─── Workload Identity ────────────────────────────────────────────────
output "gitlab_gsa_email" {
description = "GitLab Google Service Account email — annotate the K8s service account with this value"
value = google_service_account.gitlab.email
}
output "external_dns_gsa_email" {
description = "External-DNS Google Service Account email — written to config.yaml by sync-tofu-outputs.sh"
value = google_service_account.external_dns.email
}

View File

@@ -4,9 +4,5 @@ terraform {
source = "hashicorp/google"
version = "~> 6.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
}
}

View File

@@ -11,7 +11,7 @@ variable "region" {
}
variable "prefix" {
description = "Prefix for resource names (e.g., devhub-dev)"
description = "Prefix for resource names (e.g., clst-dev)"
type = string
}
@@ -39,63 +39,6 @@ variable "deletion_protection" {
default = false
}
# ─── Cloud SQL (PostgreSQL) ───────────────────────────────────────────
variable "pg_database_version" {
description = "PostgreSQL version (e.g., POSTGRES_16)"
type = string
default = "POSTGRES_16"
}
variable "pg_tier" {
description = "Cloud SQL machine tier (e.g., db-g1-small, db-n1-standard-2)"
type = string
}
variable "pg_disk_size_gb" {
description = "Cloud SQL disk size in GB"
type = number
default = 20
}
variable "pg_availability_type" {
description = "Cloud SQL availability: ZONAL or REGIONAL (REGIONAL = HA)"
type = string
default = "ZONAL"
}
variable "pg_backup_enabled" {
description = "Enable automated Cloud SQL backups"
type = bool
default = true
}
variable "pg_deletion_protection" {
description = "Prevent Cloud SQL instance deletion"
type = bool
default = false
}
# ─── Cloud Memorystore (Redis) ────────────────────────────────────────
variable "redis_tier" {
description = "Memorystore Redis tier: BASIC or STANDARD_HA"
type = string
}
variable "redis_memory_size_gb" {
description = "Redis memory size in GB"
type = number
}
# ─── GCS (Object Storage) ────────────────────────────────────────────
variable "gcs_storage_class" {
description = "GCS storage class: STANDARD, NEARLINE, COLDLINE, ARCHIVE"
type = string
default = "STANDARD"
}
# ─── Labels ──────────────────────────────────────────────────────────
variable "labels" {