diff --git a/.gitignore b/.gitignore index a56a23c..7c68b0c 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,17 @@ yarn-error.log* # Schema Spy schemaspy/output/ + +# Terraform +.terraform/ +*.tfstate +*.tfstate.backup +*.tfvars +*.tfvars.json +override.tf +override.tf.json +_override.tf +_override.tf.json +.terraformrc +terraform.rc + diff --git a/terraform-gcp/.terraform.lock.hcl b/terraform-gcp/.terraform.lock.hcl new file mode 100644 index 0000000..576e316 --- /dev/null +++ b/terraform-gcp/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/google" { + version = "5.45.2" + constraints = "~> 5.0" + hashes = [ + "h1:k8taQAdfHrv2F/AiGV5BZBZfI+1uaq8g6O8dWzjx42c=", + "zh:0d09c8f20b556305192cdbe0efa6d333ceebba963a8ba91f9f1714b5a20c4b7a", + "zh:117143fc91be407874568df416b938a6896f94cb873f26bba279cedab646a804", + "zh:16ccf77d18dd2c5ef9c0625f9cf546ebdf3213c0a452f432204c69feed55081e", + "zh:3e555cf22a570a4bd247964671f421ed7517970cd9765ceb46f335edc2c6f392", + "zh:688bd5b05a75124da7ae6e885b2b92bd29f4261808b2b78bd5f51f525c1052ca", + "zh:6db3ef37a05010d82900bfffb3261c59a0c247e0692049cb3eb8c2ef16c9d7bf", + "zh:70316fde75f6a15d72749f66d994ccbdde5f5ed4311b6d06b99850f698c9bbf9", + "zh:84b8e583771a4f2bd514e519d98ed7fd28dce5efe0634e973170e1cfb5556fb4", + "zh:9d4b8ef0a9b6677935c604d94495042e68ff5489932cfd1ec41052e094a279d3", + "zh:a2089dd9bd825c107b148dd12d6b286f71aa37dfd4ca9c35157f2dcba7bc19d8", + "zh:f03d795c0fd9721e59839255ee7ba7414173017dc530b4ce566daf3802a0d6dd", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/terraform-gcp/cloudrun.tf b/terraform-gcp/cloudrun.tf new file mode 100644 index 0000000..2a3ed98 --- /dev/null +++ b/terraform-gcp/cloudrun.tf @@ -0,0 +1,117 @@ +resource "google_cloud_run_v2_service" "backend" { + name = "tasknote-api" + location = var.region + ingress = "INGRESS_TRAFFIC_ALL" + + depends_on = [google_project_service.run] + + template { + service_account = google_service_account.cloudrun_sa.email + vpc_access { + connector = google_vpc_access_connector.connector.id + egress = "ALL_TRAFFIC" + } + + containers { + image = var.backend_image + ports { + container_port = 8585 + } + env { + name = "POSTGRES_DB" + value = var.db_name + } + env { + name = "POSTGRES_HOST" + value = google_sql_database_instance.instance.private_ip_address + } + env { + name = "POSTGRES_USER" + value = var.db_user + } + env { + name = "POSTGRES_PASSWORD" + value = var.db_password + } + env { + name = "POSTGRES_PORT" + value = "5432" + } + env { + name = "CORS_ALLOWED_ORIGINS" + value = var.cors_allowed_origins + } + env { + name = "SERVER_SERVLET_CONTEXT_PATH" + value = "/" + } + env { + name = "TARGET_ENV" + value = "production" + } + env { + name = "SECURITY_KEY" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.security_key.secret_id + version = "latest" + } + } + } + env { + name = "MAILGUN_APIKEY" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.mailgun_apikey.secret_id + version = "latest" + } + } + } + } + } +} + +resource "google_cloud_run_v2_service" "frontend" { + name = "tasknote-app" + location = var.region + ingress = "INGRESS_TRAFFIC_ALL" + + depends_on = [google_project_service.run] + + template { + service_account = google_service_account.cloudrun_sa.email + + containers { + image = var.frontend_image + ports { + container_port = 5000 + } + env { + name = "VITE_BACKEND_SERVER" + value = google_cloud_run_v2_service.backend.uri + } + } + } +} + +# Allow unauthenticated access to both services +resource "google_cloud_run_service_iam_member" "backend_public" { + location = google_cloud_run_v2_service.backend.location + service = google_cloud_run_v2_service.backend.name + role = "roles/run.invoker" + member = "allUsers" +} + +resource "google_cloud_run_service_iam_member" "frontend_public" { + location = google_cloud_run_v2_service.frontend.location + service = google_cloud_run_v2_service.frontend.name + role = "roles/run.invoker" + member = "allUsers" +} + +# Grant Cloud SQL Client role to the service account +resource "google_project_iam_member" "cloudsql_client" { + project = var.project_id + role = "roles/cloudsql.client" + member = "serviceAccount:${google_service_account.cloudrun_sa.email}" +} diff --git a/terraform-gcp/database.tf b/terraform-gcp/database.tf new file mode 100644 index 0000000..d7a102d --- /dev/null +++ b/terraform-gcp/database.tf @@ -0,0 +1,32 @@ +resource "google_sql_database_instance" "instance" { + name = "tasknote-db-instance" + region = var.region + database_version = "POSTGRES_15" + + depends_on = [ + google_service_networking_connection.private_vpc_connection, + google_project_service.sqladmin, + google_project_service.servicenetworking + ] + + settings { + tier = "db-f1-micro" # Smallest tier for dev/small app + ip_configuration { + ipv4_enabled = false + private_network = google_compute_network.vpc_network.id + } + } + + deletion_protection = false # Set to true for production +} + +resource "google_sql_database" "database" { + name = var.db_name + instance = google_sql_database_instance.instance.name +} + +resource "google_sql_user" "users" { + name = var.db_user + instance = google_sql_database_instance.instance.name + password = var.db_password +} diff --git a/terraform-gcp/network.tf b/terraform-gcp/network.tf new file mode 100644 index 0000000..5bd899c --- /dev/null +++ b/terraform-gcp/network.tf @@ -0,0 +1,35 @@ +resource "google_compute_network" "vpc_network" { + name = "tasknote-vpc" + auto_create_subnetworks = false +} + +resource "google_compute_subnetwork" "subnet" { + name = "tasknote-subnet" + ip_cidr_range = "10.0.0.0/24" + region = var.region + network = google_compute_network.vpc_network.id +} + +# Private IP for Cloud SQL +resource "google_compute_global_address" "private_ip_address" { + name = "tasknote-private-ip" + purpose = "VPC_PEERING" + address_type = "INTERNAL" + prefix_length = 16 + network = google_compute_network.vpc_network.id +} + +resource "google_service_networking_connection" "private_vpc_connection" { + network = google_compute_network.vpc_network.id + service = "servicenetworking.googleapis.com" + reserved_peering_ranges = [google_compute_global_address.private_ip_address.name] +} + +# Serverless VPC Access Connector +resource "google_vpc_access_connector" "connector" { + name = "tasknote-vpc-connector" + region = var.region + ip_cidr_range = "10.8.0.0/28" + network = google_compute_network.vpc_network.name + depends_on = [google_project_service.vpcaccess, google_project_service.compute] +} diff --git a/terraform-gcp/outputs.tf b/terraform-gcp/outputs.tf new file mode 100644 index 0000000..3ef3225 --- /dev/null +++ b/terraform-gcp/outputs.tf @@ -0,0 +1,15 @@ +output "backend_url" { + value = google_cloud_run_v2_service.backend.uri +} + +output "frontend_url" { + value = google_cloud_run_v2_service.frontend.uri +} + +output "cloud_sql_instance_ip" { + value = google_sql_database_instance.instance.private_ip_address +} + +output "artifact_registry_repo" { + value = google_artifact_registry_repository.repo.name +} diff --git a/terraform-gcp/providers.tf b/terraform-gcp/providers.tf new file mode 100644 index 0000000..ae5972a --- /dev/null +++ b/terraform-gcp/providers.tf @@ -0,0 +1,25 @@ +terraform { + required_providers { + google = { + source = "hashicorp/google" + version = "~> 5.0" + } + } + + backend "s3" { + bucket = "tasknote" + key = "gcp/terraform.tfstate" + region = "auto" + endpoints = { s3 = "https://d17eb09b6bce2f90e16e800bb2a6baf9.r2.cloudflarestorage.com" } + skip_credentials_validation = true + skip_region_validation = true + skip_requesting_account_id = true + skip_metadata_api_check = true + skip_s3_checksum = true + } +} + +provider "google" { + project = var.project_id + region = var.region +} diff --git a/terraform-gcp/registry.tf b/terraform-gcp/registry.tf new file mode 100644 index 0000000..e13a415 --- /dev/null +++ b/terraform-gcp/registry.tf @@ -0,0 +1,7 @@ +resource "google_artifact_registry_repository" "repo" { + location = var.region + repository_id = "tasknote-repo" + description = "Docker repository for TaskNote images" + format = "DOCKER" + depends_on = [google_project_service.artifactregistry] +} diff --git a/terraform-gcp/secrets.tf b/terraform-gcp/secrets.tf new file mode 100644 index 0000000..7ac9a65 --- /dev/null +++ b/terraform-gcp/secrets.tf @@ -0,0 +1,49 @@ +resource "google_service_account" "cloudrun_sa" { + account_id = "tasknote-cloudrun-sa" + display_name = "TaskNote Cloud Run Service Account" +} + +resource "google_secret_manager_secret" "security_key" { + secret_id = "security-key" + replication { + user_managed { + replicas { + location = var.region + } + } + } +} + +resource "google_secret_manager_secret_version" "security_key_version" { + secret = google_secret_manager_secret.security_key.id + secret_data = var.security_key +} + +resource "google_secret_manager_secret" "mailgun_apikey" { + secret_id = "mailgun-apikey" + replication { + user_managed { + replicas { + location = var.region + } + } + } + depends_on = [google_project_service.secretmanager] +} + +resource "google_secret_manager_secret_version" "mailgun_apikey_version" { + secret = google_secret_manager_secret.mailgun_apikey.id + secret_data = var.mailgun_apikey +} + +resource "google_secret_manager_secret_iam_member" "security_key_access" { + secret_id = google_secret_manager_secret.security_key.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.cloudrun_sa.email}" +} + +resource "google_secret_manager_secret_iam_member" "mailgun_apikey_access" { + secret_id = google_secret_manager_secret.mailgun_apikey.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.cloudrun_sa.email}" +} diff --git a/terraform-gcp/services.tf b/terraform-gcp/services.tf new file mode 100644 index 0000000..8640268 --- /dev/null +++ b/terraform-gcp/services.tf @@ -0,0 +1,34 @@ +resource "google_project_service" "compute" { + service = "compute.googleapis.com" + disable_on_destroy = false +} + +resource "google_project_service" "sqladmin" { + service = "sqladmin.googleapis.com" + disable_on_destroy = false +} + +resource "google_project_service" "run" { + service = "run.googleapis.com" + disable_on_destroy = false +} + +resource "google_project_service" "secretmanager" { + service = "secretmanager.googleapis.com" + disable_on_destroy = false +} + +resource "google_project_service" "vpcaccess" { + service = "vpcaccess.googleapis.com" + disable_on_destroy = false +} + +resource "google_project_service" "artifactregistry" { + service = "artifactregistry.googleapis.com" + disable_on_destroy = false +} + +resource "google_project_service" "servicenetworking" { + service = "servicenetworking.googleapis.com" + disable_on_destroy = false +} diff --git a/terraform-gcp/terraform.tfvars.example b/terraform-gcp/terraform.tfvars.example new file mode 100644 index 0000000..aa50aaa --- /dev/null +++ b/terraform-gcp/terraform.tfvars.example @@ -0,0 +1,10 @@ +project_id = "your-gcp-project-id" +region = "us-central1" +db_user = "tasknoteuser" +db_password = "your-db-password" +db_name = "tasknote" +security_key = "your-security-key" +mailgun_apikey = "your-mailgun-apikey" +backend_image = "ghcr.io/rmcampos/tasknote/api:latest" +frontend_image = "ghcr.io/rmcampos/tasknote/app:latest" +cors_allowed_origins = "*" diff --git a/terraform-gcp/variables.tf b/terraform-gcp/variables.tf new file mode 100644 index 0000000..bd89ea2 --- /dev/null +++ b/terraform-gcp/variables.tf @@ -0,0 +1,50 @@ +variable "project_id" { + type = string + description = "The GCP Project ID" +} + +variable "region" { + type = string + default = "us-central1" + description = "The GCP region to deploy resources" +} + +variable "db_user" { + type = string + sensitive = true +} + +variable "db_password" { + type = string + sensitive = true +} + +variable "db_name" { + type = string + default = "tasknote" +} + +variable "security_key" { + type = string + sensitive = true +} + +variable "mailgun_apikey" { + type = string + sensitive = true +} + +variable "backend_image" { + type = string + default = "ghcr.io/rmcampos/tasknote/api:latest" +} + +variable "frontend_image" { + type = string + default = "ghcr.io/rmcampos/tasknote/app:latest" +} + +variable "cors_allowed_origins" { + type = string + default = "*" +}