From 5ac068470228fcfd0fba5cb69d71172d0de7fa42 Mon Sep 17 00:00:00 2001 From: Timo Weiser <2826299+weisertimo@users.noreply.github.com> Date: Tue, 18 Feb 2025 08:04:49 +0100 Subject: [PATCH 1/2] rework infra setup --- .gitignore | 33 +- backend/docker.sh | 14 + infrastructure/env/sandbox/backend.tf | 6 + infrastructure/env/sandbox/main.tf | 29 ++ infrastructure/env/sandbox/variables.tf | 87 +++++ infrastructure/module/main.tf | 489 ++++++++++++++++++++++++ infrastructure/module/provider.tf | 26 ++ infrastructure/module/variables.tf | 87 +++++ 8 files changed, 770 insertions(+), 1 deletion(-) create mode 100644 backend/docker.sh create mode 100644 infrastructure/env/sandbox/backend.tf create mode 100644 infrastructure/env/sandbox/main.tf create mode 100644 infrastructure/env/sandbox/variables.tf create mode 100644 infrastructure/module/main.tf create mode 100644 infrastructure/module/provider.tf create mode 100644 infrastructure/module/variables.tf diff --git a/.gitignore b/.gitignore index 14edc29..f0a925b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,35 @@ node_modules launch.json # Env files -.env \ No newline at end of file +.env + +# Local .terraform directories +**/.terraform/ + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log + +# Exclude all .tfvars files, which are likely to contain sensitive data +*.tfvars +*.tfvars.json + +# Override .tf files if they are used to override resources locally +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore Terraform CLI configuration files +.terraformrc +terraform.rc + +# Terraform lock file +.terraform.lock.hcl + +# TFLint configuration (if used) +.tflint.hcl +.tflint.yaml diff --git a/backend/docker.sh b/backend/docker.sh new file mode 100644 index 0000000..22b8142 --- /dev/null +++ b/backend/docker.sh @@ -0,0 +1,14 @@ +#!/bin/bash +if [ "$1" == "dev" ]; then + ENV="dev" +elif [ "$1" == "prod" ]; then + ENV="prod" +elif [ "$1" == "sandbox" ]; then + ENV="sandbox" +else + echo "Invalid environment: $1" + exit 1 +fi + +docker buildx build --platform linux/amd64 -t europe-west1-docker.pkg.dev/driplet-core-$ENV/driplet-repository/driplet:latest . +docker push europe-west1-docker.pkg.dev/driplet-core-$ENV/driplet-repository/driplet:latest diff --git a/infrastructure/env/sandbox/backend.tf b/infrastructure/env/sandbox/backend.tf new file mode 100644 index 0000000..b2e2984 --- /dev/null +++ b/infrastructure/env/sandbox/backend.tf @@ -0,0 +1,6 @@ +terraform { + backend "gcs" { + bucket = "driplet-core-sandbox-tf" + prefix = "driplet/core/sandbox" + } +} diff --git a/infrastructure/env/sandbox/main.tf b/infrastructure/env/sandbox/main.tf new file mode 100644 index 0000000..6aa920a --- /dev/null +++ b/infrastructure/env/sandbox/main.tf @@ -0,0 +1,29 @@ +module "driplet_core" { + source = "../../module" + project_id = var.project_id + region = var.region + + # BigQuery Variables + dataset_id = var.dataset_id + table_id = var.table_id + deletion_protection = var.deletion_protection + schema = var.schema + labels = var.labels + + # Pub/Sub Variables + topic_name_client_events = var.topic_name_client_events + subscription_name = var.subscription_name + + # Networking Variables + domain = var.domain + subnetwork_cidr_range = var.subnetwork_cidr_range + cloud_run_service_name = var.cloud_run_service_name + + # Compute / Cloud Run Variables + driplet_image = var.driplet_image + database_user = var.database_user + oauth_run_callback_url = var.oauth_run_callback_url + + # Environment name (used for resource naming and labels) + env = var.env +} diff --git a/infrastructure/env/sandbox/variables.tf b/infrastructure/env/sandbox/variables.tf new file mode 100644 index 0000000..d250b35 --- /dev/null +++ b/infrastructure/env/sandbox/variables.tf @@ -0,0 +1,87 @@ +variable "project_id" { + description = "The GCP project ID for the sandbox environment" + type = string +} + +variable "region" { + description = "The GCP region for the sandbox environment" + type = string +} + +# BigQuery Variables +variable "dataset_id" { + description = "BigQuery dataset ID" + type = string +} + +variable "table_id" { + description = "BigQuery table ID" + type = string +} + +variable "deletion_protection" { + description = "Enable deletion protection for the BigQuery table" + type = bool + default = false +} + +variable "schema" { + description = "The JSON schema for the BigQuery table" + type = string +} + +variable "labels" { + description = "Optional labels for the BigQuery dataset" + type = map(string) + default = {} +} + +# Pub/Sub Variables +variable "topic_name_client_events" { + description = "Name of the Pub/Sub topic for client events" + type = string +} + +variable "subscription_name" { + description = "Name of the Pub/Sub subscription" + type = string +} + +# Networking Variables +variable "domain" { + description = "The domain name for the SSL certificate" + type = string +} + +variable "subnetwork_cidr_range" { + description = "The CIDR range for the subnetwork" + type = string +} + +variable "cloud_run_service_name" { + description = "The Cloud Run service name for the network endpoint group" + type = string +} + +# Compute / Cloud Run Variables +variable "driplet_image" { + description = "Container image for the Cloud Run service" + type = string +} + +variable "database_user" { + description = "Database username for connecting to Cloud SQL" + type = string +} + +variable "oauth_run_callback_url" { + description = "OAuth callback URL for Cloud Run" + type = string +} + +# New variable for environment name +variable "env" { + description = "Environment name (e.g., 'sandbox', 'dev', 'prod')" + type = string + default = "" +} diff --git a/infrastructure/module/main.tf b/infrastructure/module/main.tf new file mode 100644 index 0000000..d719064 --- /dev/null +++ b/infrastructure/module/main.tf @@ -0,0 +1,489 @@ +# ----------------------------------------------------------- +# Enable Required Google Cloud APIs +# ----------------------------------------------------------- + +resource "google_project_service" "cloud_run" { + project = var.project_id + service = "run.googleapis.com" + disable_on_destroy = false + + lifecycle { + prevent_destroy = true + } +} + +resource "google_project_service" "artifact_registry" { + project = var.project_id + service = "artifactregistry.googleapis.com" + disable_on_destroy = false + + lifecycle { + prevent_destroy = true + } +} + +resource "google_project_service" "cloud_sql" { + project = var.project_id + service = "sqladmin.googleapis.com" + disable_on_destroy = false + + lifecycle { + prevent_destroy = true + } +} + +resource "google_project_service" "secret_manager" { + project = var.project_id + service = "secretmanager.googleapis.com" + disable_on_destroy = false + + lifecycle { + prevent_destroy = true + } +} + +resource "google_project_service" "pubsub" { + project = var.project_id + service = "pubsub.googleapis.com" + disable_on_destroy = false + + lifecycle { + prevent_destroy = true + } +} + +resource "google_project_service" "bigquery" { + project = var.project_id + service = "bigquery.googleapis.com" + disable_on_destroy = false + + lifecycle { + prevent_destroy = true + } +} + +resource "google_project_service" "iam" { + project = var.project_id + service = "iam.googleapis.com" + disable_on_destroy = false + + lifecycle { + prevent_destroy = true + } +} + +resource "google_project_service" "service_networking" { + project = var.project_id + service = "servicenetworking.googleapis.com" + disable_on_destroy = false + + lifecycle { + prevent_destroy = true + } +} + +resource "google_project_service" "compute" { + project = var.project_id + service = "compute.googleapis.com" + disable_on_destroy = false + + lifecycle { + prevent_destroy = true + } +} + +# ----------------------------------------------------------- +# IAM Resources +# ----------------------------------------------------------- + +resource "google_service_account" "driplet_service_account" { + project = var.project_id + account_id = "driplet-service-account" + display_name = "Driplet Service Account" +} + +resource "google_project_iam_member" "driplet_service_cloudsql_connector" { + project = var.project_id + member = "serviceAccount:${google_service_account.driplet_service_account.email}" + role = "roles/cloudsql.client" +} + +resource "google_project_iam_member" "driplet_service_pubsub_publisher" { + project = var.project_id + member = "serviceAccount:${google_service_account.driplet_service_account.email}" + role = "roles/pubsub.admin" +} + +resource "google_project_iam_member" "pubsub_bigquery_access" { + project = var.project_id + role = "roles/bigquery.dataEditor" + member = "serviceAccount:${google_service_account.driplet_service_account.email}" +} + +resource "google_project_service_identity" "pubsub" { + provider = google-beta + project = var.project_id + service = "pubsub.googleapis.com" +} + +resource "google_project_iam_member" "pubsub_bigquery_writer" { + project = var.project_id + role = "roles/bigquery.dataEditor" + member = "serviceAccount:${google_project_service_identity.pubsub.email}" +} + +# ----------------------------------------------------------- +# Secret Manager Resources +# ----------------------------------------------------------- + +resource "google_secret_manager_secret" "session_secret" { + labels = { component = "driplet", managed = "terraform" } + project = var.project_id + secret_id = "session-secret" + + replication { + user_managed { + replicas { + location = var.region + } + } + } +} + +resource "google_secret_manager_secret_iam_member" "session_auth_secret" { + secret_id = google_secret_manager_secret.session_secret.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.driplet_service_account.email}" + depends_on = [google_secret_manager_secret.session_secret] +} + +resource "google_secret_manager_secret" "oauth2_client_secret" { + labels = { component = "driplet", managed = "terraform" } + project = var.project_id + secret_id = "oauth2-client-secret" + + replication { + user_managed { + replicas { + location = var.region + } + } + } +} + +resource "google_secret_manager_secret_iam_member" "oauth2_client_secret_member" { + secret_id = google_secret_manager_secret.oauth2_client_secret.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.driplet_service_account.email}" + depends_on = [google_secret_manager_secret.oauth2_client_secret] +} + +resource "google_secret_manager_secret" "oauth2_client_id" { + labels = { component = "driplet", managed = "terraform" } + project = var.project_id + secret_id = "oauth2-client-id" + + replication { + user_managed { + replicas { + location = var.region + } + } + } +} + +resource "google_secret_manager_secret_iam_member" "oauth2_client_id_member" { + secret_id = google_secret_manager_secret.oauth2_client_id.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.driplet_service_account.email}" + depends_on = [google_secret_manager_secret.oauth2_client_id] +} + +# ----------------------------------------------------------- +# Artifact Registry Repository (using google-beta for cleanup_policies) +# ----------------------------------------------------------- + +resource "google_artifact_registry_repository" "driplet_registry" { + provider = google-beta + project = var.project_id + location = var.region + repository_id = "driplet-repository" + description = "Driplet Repository" + format = "DOCKER" + + cleanup_policies { + id = "delete_old_images" + action = "DELETE" + condition { + older_than = "432000s" # 5 days + } + } + + cleanup_policies { + id = "keep_last_2_versions" + action = "KEEP" + most_recent_versions { + keep_count = 2 + } + } +} + +# ----------------------------------------------------------- +# BigQuery Resources +# ----------------------------------------------------------- + +resource "google_bigquery_dataset" "dataset" { + dataset_id = var.dataset_id + project = var.project_id + location = "EU" + labels = var.labels +} + +resource "google_bigquery_table" "table" { + table_id = var.table_id + dataset_id = google_bigquery_dataset.dataset.dataset_id + project = var.project_id + deletion_protection = var.deletion_protection + schema = var.schema +} + +# ----------------------------------------------------------- +# Pub/Sub Resources +# ----------------------------------------------------------- + +resource "google_pubsub_topic" "client_events" { + name = var.topic_name_client_events + project = var.project_id + message_retention_duration = "604800s" # 7 days +} + +resource "google_pubsub_subscription" "bigquery_subscription" { + name = var.subscription_name + topic = google_pubsub_topic.client_events.id + project = var.project_id + ack_deadline_seconds = 600 # 10 minutes + message_retention_duration = "604800s" # 7 days + + bigquery_config { + table = "${var.project_id}.${google_bigquery_dataset.dataset.dataset_id}.${var.table_id}" + write_metadata = false + drop_unknown_fields = true + } +} + +# ----------------------------------------------------------- +# Networking Resources +# ----------------------------------------------------------- + +resource "google_compute_managed_ssl_certificate" "driplet_ssl_certificate" { + name = "driplet-ssl-cert-${var.env}" + managed { + domains = [var.domain] + } +} + +resource "google_compute_network" "driplet_vpc_network" { + project = var.project_id + name = "driplet-vpc" + auto_create_subnetworks = false + routing_mode = "REGIONAL" +} + +resource "google_compute_subnetwork" "driplet_vpc_subnet" { + name = "driplet-subnet" + ip_cidr_range = var.subnetwork_cidr_range + region = var.region + network = google_compute_network.driplet_vpc_network.id +} + +resource "google_compute_firewall" "ingress" { + name = "allow-http-https" + network = google_compute_network.driplet_vpc_network.id + allow { + protocol = "tcp" + ports = ["80", "443"] + } + source_ranges = ["0.0.0.0/0"] +} + +resource "google_compute_global_address" "new_loadbalancer_ip" { + name = "loadbalancer-ip" +} + +resource "google_compute_url_map" "driplet_url_map" { + name = "driplet-url-map" + + host_rule { + hosts = ["*"] + path_matcher = "catch-all" + } + + path_matcher { + name = "catch-all" + default_service = google_compute_backend_service.driplet_backend.self_link + } + + default_url_redirect { + https_redirect = true + strip_query = true + } +} + +resource "google_compute_target_http_proxy" "driplet_http_proxy" { + name = "http-proxy" + url_map = google_compute_url_map.driplet_url_map.self_link +} + +resource "google_compute_global_forwarding_rule" "driplet_http_forwarding_rule" { + name = "driplet-http-forwarding-rule" + load_balancing_scheme = "EXTERNAL" + target = google_compute_target_http_proxy.driplet_http_proxy.self_link + port_range = "80" + ip_address = google_compute_global_address.new_loadbalancer_ip.address +} + +resource "google_compute_target_https_proxy" "driplet_https_proxy" { + name = "driplet-https-proxy" + url_map = google_compute_url_map.driplet_url_map.self_link + ssl_certificates = [google_compute_managed_ssl_certificate.driplet_ssl_certificate.self_link] +} + +resource "google_compute_global_forwarding_rule" "driplet_https_forwarding_rule" { + name = "driplet-https-forwarding-rule" + load_balancing_scheme = "EXTERNAL" + target = google_compute_target_https_proxy.driplet_https_proxy.self_link + port_range = "443" + ip_address = google_compute_global_address.new_loadbalancer_ip.address +} + +resource "google_compute_region_network_endpoint_group" "driplet_service_neg" { + name = "driplet-service-neg" + region = var.region + network_endpoint_type = "SERVERLESS" + cloud_run { + service = var.cloud_run_service_name + } +} + +resource "google_compute_backend_service" "driplet_backend" { + name = "driplet-cloud-run-backend" + load_balancing_scheme = "EXTERNAL" + protocol = "HTTP" + backend { + group = google_compute_region_network_endpoint_group.driplet_service_neg.id + } +} + +# ----------------------------------------------------------- +# SQL Database Instance and SQL User +# ----------------------------------------------------------- + +resource "google_sql_database_instance" "driplet_application_db" { + database_version = "POSTGRES_17" + region = var.region + name = "driplet-application-db" + settings { + tier = "db-perf-optimized-N-2" + database_flags { + name = "cloudsql.iam_authentication" + value = "on" + } + } +} + +resource "random_password" "driplet_application_db_password" { + length = 64 + special = false + min_upper = 5 + min_lower = 5 +} + +resource "google_sql_user" "driplet_application_db_user" { + name = "driplet_application_user" + password = random_password.driplet_application_db_password.result + instance = google_sql_database_instance.driplet_application_db.name +} + +# ----------------------------------------------------------- +# Compute / Cloud Run Resources +# ----------------------------------------------------------- + +resource "google_cloud_run_v2_service" "driplet_service" { + name = "driplet-service" + location = var.region + ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" + deletion_protection = false + template { + containers { + image = var.driplet_image + ports { + container_port = 1991 + } + volume_mounts { + name = "cloudsql" + mount_path = "/cloudsql" + } + env { + name = "POSTGRES_CONNECTION_STRING" + value = "postgresql://${var.database_user}:${random_password.driplet_application_db_password.result}@/postgres?host=/cloudsql/${google_sql_database_instance.driplet_application_db.connection_name}" + } + env { + name = "GOOGLE_PUBSUB_TOPIC_CLIENT_EVENTS" + value = "client-events" + } + env { + name = "GOOGLE_PROJECT_ID" + value = var.project_id + } + env { + name = "GOOGLE_CALLBACK_URL" + value = var.oauth_run_callback_url + } + env { + name = "PUBSUB_PROJECT_ID" + value = var.project_id + } + env { + name = "SESSION_SECRET" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.session_secret.secret_id + version = "latest" + } + } + } + env { + name = "GOOGLE_CLIENT_ID" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.oauth2_client_id.secret_id + version = "latest" + } + } + } + env { + name = "GOOGLE_CLIENT_SECRET" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.oauth2_client_secret.secret_id + version = "latest" + } + } + } + } + volumes { + name = "cloudsql" + cloud_sql_instance { + instances = [google_sql_database_instance.driplet_application_db.connection_name] + } + } + service_account = google_service_account.driplet_service_account.email + } +} + +resource "google_cloud_run_service_iam_member" "service_invoker" { + service = google_cloud_run_v2_service.driplet_service.name + location = var.region + role = "roles/run.invoker" + member = "allUsers" +} diff --git a/infrastructure/module/provider.tf b/infrastructure/module/provider.tf new file mode 100644 index 0000000..ce6eaba --- /dev/null +++ b/infrastructure/module/provider.tf @@ -0,0 +1,26 @@ +terraform { + required_providers { + google = { + source = "hashicorp/google" + version = "6.9.0" + } + google-beta = { + source = "hashicorp/google-beta" + # No explicit version pin here; it will inherit from the google provider's major version. + } + random = { + source = "hashicorp/random" + version = "3.6.3" + } + } +} + +provider "google" { + project = var.project_id + region = var.region +} + +provider "google-beta" { + project = var.project_id + region = var.region +} diff --git a/infrastructure/module/variables.tf b/infrastructure/module/variables.tf new file mode 100644 index 0000000..5fe3c83 --- /dev/null +++ b/infrastructure/module/variables.tf @@ -0,0 +1,87 @@ +variable "project_id" { + description = "The GCP project ID" + type = string +} + +variable "region" { + description = "The GCP region" + type = string +} + +# BigQuery Variables +variable "dataset_id" { + description = "BigQuery dataset ID" + type = string +} + +variable "table_id" { + description = "BigQuery table ID" + type = string +} + +variable "deletion_protection" { + description = "Enable deletion protection for the BigQuery table" + type = bool + default = false +} + +variable "schema" { + description = "The JSON schema for the BigQuery table" + type = string +} + +variable "labels" { + description = "Optional labels for the BigQuery dataset" + type = map(string) + default = {} +} + +# Pub/Sub Variables +variable "topic_name_client_events" { + description = "Name of the Pub/Sub topic for client events" + type = string +} + +variable "subscription_name" { + description = "Name of the Pub/Sub subscription" + type = string +} + +# Networking Variables +variable "domain" { + description = "The domain name for the SSL certificate" + type = string +} + +variable "subnetwork_cidr_range" { + description = "The CIDR range for the subnetwork" + type = string +} + +variable "cloud_run_service_name" { + description = "The Cloud Run service name for the network endpoint group" + type = string +} + +# Compute / Cloud Run Variables +variable "driplet_image" { + description = "Container image for the Cloud Run service" + type = string +} + +variable "database_user" { + description = "Database username for connecting to Cloud SQL" + type = string +} + +variable "oauth_run_callback_url" { + description = "OAuth callback URL for Cloud Run" + type = string +} + +# New variable for environment name (used for resource naming and labels) +variable "env" { + description = "Environment name (e.g., 'sandbox', 'dev', 'prod')" + type = string + default = "" +} From 11ee9d93a450793a8f86bd9dd4a1e1ee63ba8709 Mon Sep 17 00:00:00 2001 From: Timo Weiser <2826299+weisertimo@users.noreply.github.com> Date: Wed, 19 Feb 2025 17:16:24 +0100 Subject: [PATCH 2/2] updated cloud run job --- infrastructure/env/sandbox/main.tf | 7 +- infrastructure/env/sandbox/variables.tf | 5 + infrastructure/module/main.tf | 136 ++++++++++++++++++------ infrastructure/module/variables.tf | 5 + 4 files changed, 117 insertions(+), 36 deletions(-) diff --git a/infrastructure/env/sandbox/main.tf b/infrastructure/env/sandbox/main.tf index 6aa920a..bc1d505 100644 --- a/infrastructure/env/sandbox/main.tf +++ b/infrastructure/env/sandbox/main.tf @@ -20,9 +20,10 @@ module "driplet_core" { cloud_run_service_name = var.cloud_run_service_name # Compute / Cloud Run Variables - driplet_image = var.driplet_image - database_user = var.database_user - oauth_run_callback_url = var.oauth_run_callback_url + driplet_image = var.driplet_image + driplet_scheduler_image = var.driplet_scheduler_image + database_user = var.database_user + oauth_run_callback_url = var.oauth_run_callback_url # Environment name (used for resource naming and labels) env = var.env diff --git a/infrastructure/env/sandbox/variables.tf b/infrastructure/env/sandbox/variables.tf index d250b35..7a7925d 100644 --- a/infrastructure/env/sandbox/variables.tf +++ b/infrastructure/env/sandbox/variables.tf @@ -69,6 +69,11 @@ variable "driplet_image" { type = string } +variable "driplet_scheduler_image" { + description = "Container image for the Cloud Run service" + type = string +} + variable "database_user" { description = "Database username for connecting to Cloud SQL" type = string diff --git a/infrastructure/module/main.tf b/infrastructure/module/main.tf index d719064..da9859d 100644 --- a/infrastructure/module/main.tf +++ b/infrastructure/module/main.tf @@ -2,6 +2,16 @@ # Enable Required Google Cloud APIs # ----------------------------------------------------------- +resource "google_project_service" "cloud_scheduler" { + project = var.project_id + service = "cloudscheduler.googleapis.com" + disable_on_destroy = false + + lifecycle { + prevent_destroy = true + } +} + resource "google_project_service" "cloud_run" { project = var.project_id service = "run.googleapis.com" @@ -407,51 +417,39 @@ resource "google_sql_user" "driplet_application_db_user" { # ----------------------------------------------------------- # Compute / Cloud Run Resources # ----------------------------------------------------------- - resource "google_cloud_run_v2_service" "driplet_service" { name = "driplet-service" location = var.region ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" deletion_protection = false + template { containers { image = var.driplet_image + + # Add startup script to run migrations then start the app + command = ["/bin/sh"] + args = ["-c", "echo 'Starting database migrations...' && ./api migrate up && echo 'Migrations completed successfully' && echo 'Starting application...' && ./api run"] + ports { - container_port = 1991 + container_port = 9000 } + volume_mounts { name = "cloudsql" mount_path = "/cloudsql" } + env { - name = "POSTGRES_CONNECTION_STRING" - value = "postgresql://${var.database_user}:${random_password.driplet_application_db_password.result}@/postgres?host=/cloudsql/${google_sql_database_instance.driplet_application_db.connection_name}" - } - env { - name = "GOOGLE_PUBSUB_TOPIC_CLIENT_EVENTS" - value = "client-events" - } - env { - name = "GOOGLE_PROJECT_ID" - value = var.project_id - } - env { - name = "GOOGLE_CALLBACK_URL" - value = var.oauth_run_callback_url + name = "ENV" + value = "development" } + env { name = "PUBSUB_PROJECT_ID" - value = var.project_id - } - env { - name = "SESSION_SECRET" - value_source { - secret_key_ref { - secret = google_secret_manager_secret.session_secret.secret_id - version = "latest" - } - } + value = "driplet-core-sandbox" } + env { name = "GOOGLE_CLIENT_ID" value_source { @@ -461,26 +459,98 @@ resource "google_cloud_run_v2_service" "driplet_service" { } } } + env { - name = "GOOGLE_CLIENT_SECRET" - value_source { - secret_key_ref { - secret = google_secret_manager_secret.oauth2_client_secret.secret_id - version = "latest" - } - } + name = "DATABASE_URL" + value = "postgresql://${var.database_user}:${random_password.driplet_application_db_password.result}@/postgres?host=/cloudsql/${google_sql_database_instance.driplet_application_db.connection_name}" } } + volumes { name = "cloudsql" cloud_sql_instance { instances = [google_sql_database_instance.driplet_application_db.connection_name] } } + service_account = google_service_account.driplet_service_account.email } } +resource "google_cloud_run_v2_job" "scheduler_job" { + name = "scheduler-points-calculation" + location = var.region + + template { + template { + containers { + image = var.driplet_scheduler_image + + command = ["/bin/sh"] + args = ["-c", "./scheduler calc-points"] + + volume_mounts { + name = "cloudsql" + mount_path = "/cloudsql" + } + + env { + name = "ENV" + value = "development" + } + + env { + name = "PUBSUB_PROJECT_ID" + value = "driplet-core-sandbox" + } + + env { + name = "GOOGLE_CLIENT_ID" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.oauth2_client_id.secret_id + version = "latest" + } + } + } + + env { + name = "DATABASE_URL" + value = "postgresql://${var.database_user}:${random_password.driplet_application_db_password.result}@/postgres?host=/cloudsql/${google_sql_database_instance.driplet_application_db.connection_name}" + } + } + + volumes { + name = "cloudsql" + cloud_sql_instance { + instances = [google_sql_database_instance.driplet_application_db.connection_name] + } + } + + service_account = google_service_account.driplet_service_account.email + } + } +} + +# Cloud Scheduler Job to trigger the Cloud Run Job +resource "google_cloud_scheduler_job" "daily_job" { + depends_on = [google_project_service.cloud_scheduler] + + name = "trigger-points-calculation" + description = "Triggers daily points calculation job" + schedule = "0 0 * * *" # Runs at midnight every day + time_zone = "UTC" + + http_target { + http_method = "POST" + uri = "https://${var.region}-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/${var.project_id}/jobs/${google_cloud_run_v2_job.scheduler_job.name}:run" + + oauth_token { + service_account_email = google_service_account.driplet_service_account.email + } + } +} + resource "google_cloud_run_service_iam_member" "service_invoker" { service = google_cloud_run_v2_service.driplet_service.name location = var.region diff --git a/infrastructure/module/variables.tf b/infrastructure/module/variables.tf index 5fe3c83..999d1b3 100644 --- a/infrastructure/module/variables.tf +++ b/infrastructure/module/variables.tf @@ -69,6 +69,11 @@ variable "driplet_image" { type = string } +variable "driplet_scheduler_image" { + description = "Container image for the Cloud Run service" + type = string +} + variable "database_user" { description = "Database username for connecting to Cloud SQL" type = string