From fc7d76abd8027ffc10959e7004a14ea4b9255a81 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Sat, 30 Mar 2024 14:47:21 +0100 Subject: [PATCH 1/2] Add terraform configuration --- .gitignore | 1 + terraform/build.gradle.kts | 29 ++++ terraform/main.tf | 334 +++++++++++++++++++++++++++++++++++++ 3 files changed, 364 insertions(+) create mode 100644 terraform/build.gradle.kts create mode 100644 terraform/main.tf diff --git a/.gitignore b/.gitignore index ca819535..06c6829a 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ apollo.key.unused keystore.properties api-*.json *.db +.terraform diff --git a/terraform/build.gradle.kts b/terraform/build.gradle.kts new file mode 100644 index 00000000..598e4ecd --- /dev/null +++ b/terraform/build.gradle.kts @@ -0,0 +1,29 @@ +val file = layout.buildDirectory.file("service-account.json").get().asFile + +val gcpServiceAccountJson by lazy { + System.getenv("GOOGLE_APPLICATION_CREDENTIALS_CONTENT") ?: error("No GOOGLE_APPLICATION_CREDENTIALS_CONTENT found") +} +val createGcpCredentials = tasks.register("createGcpCredentials") { + doLast { + file.parentFile.mkdirs() + file.writeText(gcpServiceAccountJson) + } +} + +val init = tasks.register("init", Exec::class.java) { + dependsOn(createGcpCredentials) + environment("GOOGLE_APPLICATION_CREDENTIALS", file.absolutePath) + commandLine("terraform", "init") +} + +tasks.register("apply", Exec::class.java) { + dependsOn(init) + environment("GOOGLE_APPLICATION_CREDENTIALS", file.absolutePath) + commandLine("terraform", "apply", "-auto-approve") +} + +tasks.register("plan", Exec::class.java) { + dependsOn(init) + environment("GOOGLE_APPLICATION_CREDENTIALS", file.absolutePath) + commandLine("terraform", "plan") +} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 00000000..10217b2f --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,334 @@ +/** + * BOOTSTRAPPING + * Sadly, there are still manual steps needed to bootstrap a terraform configuration (See also https://github.com/terraform-google-modules/terraform-example-foundation/blob/master/0-bootstrap/README.md) + * + * - create var.project in GCP + * - enable billing + * - create service account and grant "Editor" + "Cloud Run Admin" role. This might be fine tuned in the future + * - create "androidmakers-tfstate" bucket + * - create var.domain in Gandi + * - + */ +// These resources must be created manually before the first terraform apply +variable "project" { + default = "androidmakers-a6883" +} +variable "domain" { + default = "androidmakers.fr" +} +# Also create "androidmakers-tfstate" as it can sadly not be a variable +# Typically use the same resource as for tfstate-bucket above (but doest have to) +variable "region" { + default = "europe-west9" +} + +terraform { + backend "gcs" { + bucket = "androidmakers-tfstate" + prefix = "terraform/state" + } +} + +provider google-beta { + project = var.project + region = var.region +} + +resource "google_project_service" "api_compute" { + provider = google-beta + service = "compute.googleapis.com" +} + +resource "google_project_service" "api_artifact_registry" { + provider = google-beta + service = "artifactregistry.googleapis.com" +} + +resource "google_project_service" "api_cloud_run" { + provider = google-beta + service = "run.googleapis.com" +} + +resource "google_compute_url_map" "default" { + name = "default" + provider = google-beta + default_service = google_compute_backend_bucket.static_content.id + + host_rule { + hosts = [var.domain] + path_matcher = "default" + } + + path_matcher { + name = "default" + default_service = google_compute_backend_bucket.static_content.id + + path_rule { + paths = [ + "/graphiql", + "/graphql", + "/images/*" + ] + service = google_compute_backend_service.graphql.id + } + path_rule { + paths = [ + "/update/*" + ] + service = google_compute_backend_service.import.id + } + } +} + +resource "google_compute_backend_bucket" "static_content" { + provider = google-beta + name = "static-content" + bucket_name = google_storage_bucket.static_content.name + enable_cdn = true +} + +resource "google_compute_backend_service" "graphql" { + provider = google-beta + name = "graphql" + enable_cdn = true + + custom_response_headers = ["X-Cache-Hit: {cdn_cache_status}"] + + log_config { + enable = true + sample_rate = 1 + } + + backend { + group = google_compute_region_network_endpoint_group.cloudrungraphql.id + } + + cdn_policy { + cache_mode = "USE_ORIGIN_HEADERS" + + cache_key_policy { + include_protocol = true + include_host = true + include_query_string = true + include_http_headers = ["conference"] + } + } + compression_mode = "DISABLED" +} + +resource "google_compute_backend_service" "import" { + provider = google-beta + name = "import" + enable_cdn = true + + custom_response_headers = ["X-Cache-Hit: {cdn_cache_status}"] + + log_config { + enable = true + sample_rate = 1 + } + + backend { + group = google_compute_region_network_endpoint_group.cloudrunimport.id + } + + cdn_policy { + cache_mode = "USE_ORIGIN_HEADERS" + + cache_key_policy { + include_protocol = true + include_host = true + include_query_string = true + include_http_headers = ["conference"] + } + } + compression_mode = "DISABLED" +} + +resource "google_compute_region_network_endpoint_group" "cloudrungraphql" { + provider = google-beta + name = "cloudrungraphql" + region = var.region + network_endpoint_type = "SERVERLESS" + + cloud_run { + service = "graphql" + } +} + +resource "google_compute_region_network_endpoint_group" "cloudrunimport" { + provider = google-beta + name = "cloudrunimport" + region = var.region + network_endpoint_type = "SERVERLESS" + + cloud_run { + service = "import" + } +} + +resource "google_compute_managed_ssl_certificate" "default2" { + name = "default2" + provider = google-beta + + managed { + domains = [var.domain] + } +} + +resource "google_compute_global_address" "default" { + provider = google-beta + name = "default" +} + +resource "google_compute_target_https_proxy" "default" { + provider = google-beta + name = "default" + url_map = google_compute_url_map.default.id + ssl_certificates = [google_compute_managed_ssl_certificate.default2.id] +} + +resource "google_compute_target_http_proxy" "default" { + provider = google-beta + name = "default" + url_map = google_compute_url_map.default.id +} + +resource "google_compute_global_forwarding_rule" "https" { + name = "https" + provider = google-beta + ip_protocol = "TCP" + load_balancing_scheme = "EXTERNAL" + port_range = "443" + target = google_compute_target_https_proxy.default.id + ip_address = google_compute_global_address.default.id +} + +resource "google_compute_global_forwarding_rule" "http" { + name = "http" + provider = google-beta + ip_protocol = "TCP" + load_balancing_scheme = "EXTERNAL" + port_range = "80" + target = google_compute_target_http_proxy.default.id + ip_address = google_compute_global_address.default.id +} + +resource "google_artifact_registry_repository" "graphql-images" { + repository_id = "graphql-images" + provider = google-beta + description = "images for the GraphQL API" + format = "DOCKER" + cleanup_policies { + id = "keep-minimum-versions" + action = "KEEP" + most_recent_versions { + # Delete old images automatically + keep_count = 5 + } + } +} + +resource "google_cloud_run_v2_service" "graphql" { + name = "graphql" + provider = google-beta + ingress = "INGRESS_TRAFFIC_ALL" + location = var.region + + + template { + containers { + image = "us-docker.pkg.dev/cloudrun/container/placeholder" + resources { + cpu_idle = true + startup_cpu_boost = true + } + } + } +} + +resource "google_cloud_run_service_iam_binding" "graphql" { + provider = google-beta + location = google_cloud_run_v2_service.graphql.location + service = google_cloud_run_v2_service.graphql.name + role = "roles/run.invoker" + members = [ + "allUsers" + ] +} + +resource "google_artifact_registry_repository" "import-images" { + repository_id = "import-images" + provider = google-beta + description = "images for the Import API" + format = "DOCKER" + cleanup_policies { + id = "keep-minimum-versions" + action = "KEEP" + most_recent_versions { + # Delete old images automatically + keep_count = 5 + } + } +} + +resource "google_cloud_run_v2_service" "import" { + name = "import" + provider = google-beta + ingress = "INGRESS_TRAFFIC_ALL" + location = var.region + + template { + containers { + image = "us-docker.pkg.dev/cloudrun/container/placeholder" + resources { + cpu_idle = true + startup_cpu_boost = true + } + } + } +} + +resource "google_cloud_run_service_iam_binding" "import" { + provider = google-beta + location = google_cloud_run_v2_service.import.location + service = google_cloud_run_v2_service.import.name + role = "roles/run.invoker" + members = [ + "allUsers" + ] +} + +# This was created outside of terraform, import it +import { + id = "androidmakers-tfstate" + to = google_storage_bucket.tfstate +} + +resource "google_storage_bucket" "tfstate" { + provider = google-beta + name = "androidmakers-tfstate" + force_destroy = false + location = var.region + storage_class = "STANDARD" + versioning { + enabled = true + } +} + +resource "google_storage_bucket" "static_content" { + provider = google-beta + name = "androidmakers-static-content" + # This bucket was created before everything was in terraform and uses a multi-region instead of var.region + location = "US" + storage_class = "STANDARD" + + website { + main_page_suffix = "index.html" + not_found_page = "404.html" + } +} + +output "ip_addr" { + value = google_compute_global_address.default.address +} From 866e8c4bee2ae44594eb0e2e637cc1a05f44334b Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Sat, 30 Mar 2024 14:49:00 +0100 Subject: [PATCH 2/2] Tweak gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 06c6829a..ac542aaf 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ keystore.properties api-*.json *.db .terraform +.terraform.lock.hcl \ No newline at end of file