diff --git a/terraform/apps/mysql-catalog.tf b/terraform/apps/mysql-catalog.tf new file mode 100644 index 0000000..6876b8e --- /dev/null +++ b/terraform/apps/mysql-catalog.tf @@ -0,0 +1,55 @@ +# ───────────────────────────────────────────────────────────── +# mysql-app-catalog +# Single contract between firebase-cloud and personal-cloud. +# firebase-cloud writes a JSON map of apps that need OCI MySQL DBs; +# personal-cloud reads it and provisions databases + populates per-app +# secret versions. Adding a new app to this map (and adding a matching +# module "_db" call) is the only edit needed to provision a DB. +# ───────────────────────────────────────────────────────────── + +locals { + mysql_apps = { + shehryar = module.shehryar_db.app_entry + } +} + +resource "google_secret_manager_secret" "mysql_app_catalog" { + project = var.project_id + secret_id = "mysql-app-catalog" + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "mysql_app_catalog" { + secret = google_secret_manager_secret.mysql_app_catalog.id + secret_data = jsonencode(local.mysql_apps) +} + +# ───────────────────────────────────────────────────────────── +# Bootstrap IAM: grant personal-cloud's deploy SA reader on the +# secrets it needs to bring up its terraform stack (catalog + admin +# creds + tfstate creds). Managed here rather than in personal-cloud +# to avoid a chicken-and-egg (personal-cloud cannot self-grant the +# access it needs to apply). +# ───────────────────────────────────────────────────────────── + +locals { + personal_cloud_bootstrap_secrets = toset([ + "mysql-app-catalog", + "db-admin-user", + "db-admin-pass", + "oci-tf-aws-access-key-id", + "oci-tf-aws-secret-access-key", + ]) +} + +resource "google_secret_manager_secret_iam_member" "personal_cloud_bootstrap_access" { + for_each = local.personal_cloud_bootstrap_secrets + project = var.project_id + secret_id = each.value + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${var.personal_cloud_deploy_sa}" + + depends_on = [google_secret_manager_secret.mysql_app_catalog] +} diff --git a/terraform/apps/outputs.tf b/terraform/apps/outputs.tf index d7a3910..523099b 100644 --- a/terraform/apps/outputs.tf +++ b/terraform/apps/outputs.tf @@ -233,3 +233,34 @@ output "crea8ivedesign_required_dns" { description = "DNS records required at the registrar to verify and serve the custom domain" value = google_firebase_hosting_custom_domain.crea8ivedesign.required_dns_updates } + +# ── shehryar Outputs ──────────────────────────────────────── +output "shehryar_wif_provider" { + description = "WIF_PROVIDER for shehryar repo GitHub secrets" + value = module.shehryar_identity.wif_provider +} + +output "shehryar_gcp_sa_email" { + description = "GCP_SA_EMAIL for shehryar repo GitHub secrets" + value = module.shehryar_identity.ci_cd_sa_email +} + +output "shehryar_hosting_url" { + description = "Firebase Hosting URL" + value = module.shehryar_hosting.site_url +} + +output "shehryar_custom_domain" { + description = "Custom domain for shehryar" + value = google_firebase_hosting_custom_domain.shehryar.custom_domain +} + +output "shehryar_required_dns" { + description = "DNS records required at the registrar to verify and serve the custom domain" + value = google_firebase_hosting_custom_domain.shehryar.required_dns_updates +} + +output "shehryar_api_url" { + description = "Cloud Run URL for the shehryar chatapp backend" + value = google_cloud_run_v2_service.shehryar_api.uri +} diff --git a/terraform/apps/shehryar.tf b/terraform/apps/shehryar.tf new file mode 100644 index 0000000..e719824 --- /dev/null +++ b/terraform/apps/shehryar.tf @@ -0,0 +1,192 @@ +# ───────────────────────────────────────────────────────────── +# shehryar — Static site + PHP chatapp on Cloud Run + OCI HeatWave MySQL +# ───────────────────────────────────────────────────────────── +# Firebase Hosting serves the static site. /chatapp/** is rewritten +# to a Cloud Run service (shehryar-api) running the PHP backend. +# The backend talks to OCI HeatWave MySQL (free tier) over the public +# internet; the daily keep-alive (mysql-keepalive.tf) prevents Oracle +# from marking the instance INACTIVE after 7 days of silence. +# ───────────────────────────────────────────────────────────── + +# ── Identity: SA + WIF Provider ───────────────────────────── +module "shehryar_identity" { + source = "../modules/app-identity" + + project_id = var.project_id + app_name = "shehryar" + github_org = var.github_org + github_repo = "shehryar" + wif_pool_id = var.wif_pool_id + wif_pool_name = var.wif_pool_name + + ci_cd_roles = [ + "roles/firebasehosting.admin", + "roles/firebase.admin", + "roles/run.admin", + "roles/artifactregistry.writer", + "roles/iam.serviceAccountUser", + "roles/serviceusage.serviceUsageConsumer", + ] + + runtime_roles = [ + "roles/secretmanager.secretAccessor", + "roles/logging.logWriter", + "roles/monitoring.metricWriter", + "roles/cloudtrace.agent", + ] +} + +# ── Firebase Hosting ──────────────────────────────────────── +module "shehryar_hosting" { + source = "../modules/hosting" + + project_id = var.project_id + site_id = "shehryar" +} + +resource "google_firebase_hosting_custom_domain" "shehryar" { + provider = google-beta + project = var.project_id + site_id = module.shehryar_hosting.site_id + custom_domain = "shehryar.junaid.guru" + + wait_dns_verification = false +} + +# ── Per-app DB + secret shells ────────────────────────────── +# Owned by the app-with-mysql module: creates shehryar-db-{user,pass,name} +# shells, grants runtime SA reader on them and on db-host, and emits an +# app_entry consumed by mysql-catalog.tf. Versions are populated by +# personal-cloud terraform on its next apply. +module "shehryar_db" { + source = "../modules/app-with-mysql" + + project_id = var.project_id + app_name = "shehryar" + database_name = "rn_chatapp" + runtime_sa_email = module.shehryar_identity.runtime_sa_email +} + +# ── Cloud Run (inlined to wire Secret Manager refs) ───────── +resource "google_cloud_run_v2_service" "shehryar_api" { + provider = google-beta + project = var.project_id + name = "shehryar-api" + location = var.region + + deletion_protection = false + ingress = "INGRESS_TRAFFIC_ALL" + + template { + service_account = module.shehryar_identity.runtime_sa_email + + scaling { + min_instance_count = 0 + max_instance_count = 1 + } + + containers { + image = "${var.region}-docker.pkg.dev/${var.project_id}/firebase-cloud/shehryar-api:latest" + + resources { + limits = { + cpu = "1" + memory = "512Mi" + } + cpu_idle = true + startup_cpu_boost = true + } + + env { + name = "APP_ENV" + value = "production" + } + + env { + name = "DB_HOST" + value_source { + secret_key_ref { + secret = "db-host" + version = "latest" + } + } + } + + env { + name = "DB_USER" + value_source { + secret_key_ref { + secret = "shehryar-db-user" + version = "latest" + } + } + } + + env { + name = "DB_PASS" + value_source { + secret_key_ref { + secret = "shehryar-db-pass" + version = "latest" + } + } + } + + env { + name = "DB_NAME" + value_source { + secret_key_ref { + secret = "shehryar-db-name" + version = "latest" + } + } + } + + startup_probe { + http_get { + path = "/chatapp/health.php" + } + initial_delay_seconds = 5 + period_seconds = 10 + failure_threshold = 3 + timeout_seconds = 3 + } + + liveness_probe { + http_get { + path = "/" + } + period_seconds = 30 + failure_threshold = 3 + timeout_seconds = 3 + } + } + + timeout = "300s" + execution_environment = "EXECUTION_ENVIRONMENT_GEN2" + } + + traffic { + type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" + percent = 100 + } + + lifecycle { + ignore_changes = [ + template[0].containers[0].image, + traffic, + ] + } + + depends_on = [ + module.shehryar_db, + ] +} + +resource "google_cloud_run_v2_service_iam_member" "shehryar_api_public" { + project = var.project_id + location = var.region + name = google_cloud_run_v2_service.shehryar_api.name + role = "roles/run.invoker" + member = "allUsers" +} diff --git a/terraform/apps/variables.tf b/terraform/apps/variables.tf index 6dabd8e..30d8110 100644 --- a/terraform/apps/variables.tf +++ b/terraform/apps/variables.tf @@ -78,3 +78,9 @@ variable "openguessr_google_maps_api_key" { type = string sensitive = true } + +# ── Cross-stack: personal-cloud (OCI MySQL provisioning) ──── +variable "personal_cloud_deploy_sa" { + description = "personal-cloud workflow's deploy SA email; granted reader on bootstrap GSM secrets so it can authenticate to OCI tfstate and read MySQL admin creds" + type = string +} diff --git a/terraform/main.tf b/terraform/main.tf index d79782b..6ddec68 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -116,5 +116,7 @@ module "apps" { openguessr_google_maps_api_key = var.openguessr_google_maps_api_key + personal_cloud_deploy_sa = var.personal_cloud_deploy_sa + depends_on = [module.project_setup] } diff --git a/terraform/modules/app-with-mysql/main.tf b/terraform/modules/app-with-mysql/main.tf new file mode 100644 index 0000000..b8d8eae --- /dev/null +++ b/terraform/modules/app-with-mysql/main.tf @@ -0,0 +1,36 @@ +locals { + shell_kinds = ["user", "pass", "name"] +} + +# Per-app secret shells. Versions are populated by personal-cloud terraform. +resource "google_secret_manager_secret" "shells" { + for_each = toset(local.shell_kinds) + project = var.project_id + secret_id = "${var.app_name}-db-${each.value}" + replication { + auto {} + } +} + +# Grant the runtime SA reader on each per-app shell. +resource "google_secret_manager_secret_iam_member" "app_shells" { + for_each = google_secret_manager_secret.shells + project = var.project_id + secret_id = each.value.secret_id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${var.runtime_sa_email}" +} + +# The shared db-host secret is owned out-of-module by mysql-keepalive.tf. +# We just look it up and grant the runtime SA reader on it. +data "google_secret_manager_secret" "db_host" { + project = var.project_id + secret_id = "db-host" +} + +resource "google_secret_manager_secret_iam_member" "db_host" { + project = var.project_id + secret_id = data.google_secret_manager_secret.db_host.secret_id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${var.runtime_sa_email}" +} diff --git a/terraform/modules/app-with-mysql/outputs.tf b/terraform/modules/app-with-mysql/outputs.tf new file mode 100644 index 0000000..b348cda --- /dev/null +++ b/terraform/modules/app-with-mysql/outputs.tf @@ -0,0 +1,7 @@ +output "app_entry" { + description = "Entry to add to the mysql-app-catalog locals; consumed by personal-cloud terraform" + value = { + database = var.database_name + sa_email = var.runtime_sa_email + } +} diff --git a/terraform/modules/app-with-mysql/variables.tf b/terraform/modules/app-with-mysql/variables.tf new file mode 100644 index 0000000..3a62251 --- /dev/null +++ b/terraform/modules/app-with-mysql/variables.tf @@ -0,0 +1,19 @@ +variable "app_name" { + type = string + description = "App name; used as prefix for GSM secret IDs (e.g. shehryar-db-user) and as the catalog map key" +} + +variable "database_name" { + type = string + description = "MySQL database name to provision for this app on the OCI MySQL HeatWave cluster" +} + +variable "runtime_sa_email" { + type = string + description = "Email of the runtime service account that will read the per-app DB secrets at runtime" +} + +variable "project_id" { + type = string + description = "GCP project ID hosting the secrets" +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf index 2e4d932..ff27fea 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -245,3 +245,34 @@ output "crea8ivedesign_required_dns" { description = "DNS records required at the registrar to verify and serve the crea8ivedesign custom domain" value = module.apps.crea8ivedesign_required_dns } + +# ── shehryar ──────────────────────────────────────────────── +output "shehryar_wif_provider" { + description = "WIF_PROVIDER for shehryar repo GitHub secrets" + value = module.apps.shehryar_wif_provider +} + +output "shehryar_gcp_sa_email" { + description = "GCP_SA_EMAIL for shehryar repo GitHub secrets" + value = module.apps.shehryar_gcp_sa_email +} + +output "shehryar_hosting_url" { + description = "shehryar Firebase Hosting URL" + value = module.apps.shehryar_hosting_url +} + +output "shehryar_custom_domain" { + description = "shehryar custom domain" + value = module.apps.shehryar_custom_domain +} + +output "shehryar_required_dns" { + description = "DNS records required at the registrar to verify and serve the shehryar custom domain" + value = module.apps.shehryar_required_dns +} + +output "shehryar_api_url" { + description = "shehryar Cloud Run API URL" + value = module.apps.shehryar_api_url +} diff --git a/terraform/variables.tf b/terraform/variables.tf index 09d52c6..8467899 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -88,3 +88,12 @@ variable "openguessr_google_maps_api_key" { type = string sensitive = true } + +# ───────────────────────────────────────────────────────────── +# Cross-stack: personal-cloud (OCI MySQL provisioning) +# ───────────────────────────────────────────────────────────── +variable "personal_cloud_deploy_sa" { + description = "personal-cloud workflow's deploy SA email; granted reader on bootstrap GSM secrets so it can authenticate to OCI tfstate and read MySQL admin creds" + type = string + default = "personal-cloud-ci-cd@firebase-cloud-491613.iam.gserviceaccount.com" +}