From d7b159d51682a1e7cd4999fe84252b860823bd05 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Tue, 21 May 2024 02:08:46 +0200 Subject: [PATCH 1/6] add terraform support --- .gitignore | 7 + sql_extensions/00000_init.sql | 1 + sql_extensions/apply-sql-extensions.sh | 10 +- terraform/main.tf | 76 ++++++++ terraform/modules/db/main.tf | 197 ++++++++++++++++++++ terraform/modules/db/outputs.tf | 31 +++ terraform/modules/db/run-migrations.sh | 8 + terraform/modules/db/variables.tf | 5 + terraform/modules/grafana/main.tf | 168 +++++++++++++++++ terraform/modules/grafana/outputs.tf | 3 + terraform/modules/grafana/variables.tf | 15 ++ terraform/modules/mqtt/main.tf | 39 ++++ terraform/modules/mqtt/outputs.tf | 3 + terraform/modules/mqtt/variables.tf | 7 + terraform/modules/no_auth_policy/main.tf | 8 + terraform/modules/no_auth_policy/outputs.tf | 3 + terraform/modules/postgrest/main.tf | 40 ++++ terraform/modules/postgrest/outputs.tf | 3 + terraform/modules/postgrest/variables.tf | 11 ++ terraform/modules/processor/main.tf | 35 ++++ terraform/modules/processor/variables.tf | 15 ++ terraform/outputs.tf | 90 +++++++++ terraform/variables.tf | 33 ++++ 23 files changed, 804 insertions(+), 4 deletions(-) create mode 100644 terraform/main.tf create mode 100644 terraform/modules/db/main.tf create mode 100644 terraform/modules/db/outputs.tf create mode 100644 terraform/modules/db/run-migrations.sh create mode 100644 terraform/modules/db/variables.tf create mode 100644 terraform/modules/grafana/main.tf create mode 100644 terraform/modules/grafana/outputs.tf create mode 100644 terraform/modules/grafana/variables.tf create mode 100644 terraform/modules/mqtt/main.tf create mode 100644 terraform/modules/mqtt/outputs.tf create mode 100644 terraform/modules/mqtt/variables.tf create mode 100644 terraform/modules/no_auth_policy/main.tf create mode 100644 terraform/modules/no_auth_policy/outputs.tf create mode 100644 terraform/modules/postgrest/main.tf create mode 100644 terraform/modules/postgrest/outputs.tf create mode 100644 terraform/modules/postgrest/variables.tf create mode 100644 terraform/modules/processor/main.tf create mode 100644 terraform/modules/processor/variables.tf create mode 100644 terraform/outputs.tf create mode 100644 terraform/variables.tf diff --git a/.gitignore b/.gitignore index 4c49bd7..215dc73 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,8 @@ .env +terraform/creds.json +terraform/.terraform +terraform/.terraform.lock.hcl +terraform/.terraform.tfstate.lock.info +terraform/*.tfvars +terraform/*.tfstate +terraform/*.backup diff --git a/sql_extensions/00000_init.sql b/sql_extensions/00000_init.sql index 81cf87b..2c9d5c4 100644 --- a/sql_extensions/00000_init.sql +++ b/sql_extensions/00000_init.sql @@ -4,5 +4,6 @@ CREATE TABLE sql_extensions ( CREATE USER web_anon NOLOGIN; +GRANT web_anon TO postgres; GRANT SELECT ON ALL TABLES IN SCHEMA public TO web_anon; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO web_anon; diff --git a/sql_extensions/apply-sql-extensions.sh b/sql_extensions/apply-sql-extensions.sh index 1657afc..af98753 100644 --- a/sql_extensions/apply-sql-extensions.sh +++ b/sql_extensions/apply-sql-extensions.sh @@ -2,10 +2,12 @@ sleep 1 -if [ -d "migrations" ]; then - for file in 00000_init.sql $(ls migrations/*.sql);do - if [ "$(psql $DATABASE_URL --csv -t -c "SELECT COUNT(*) FROM sql_extensions WHERE name = '$file'" 2>/dev/null)" != "1" ];then - psql $DATABASE_URL --single-transaction -f "$file" -c "INSERT INTO sql_extensions VALUES ('$file');" +script_dir=$(dirname -- "$(readlink -f -- "$BASH_SOURCE")") + +if [ -d "$script_dir/migrations" ]; then + for file in "$script_dir/00000_init.sql" $(ls -Q "$script_dir/migrations/*.sql");do + if [ "$(psql $DATABASE_URL --csv -t -c "SELECT COUNT(*) FROM sql_extensions WHERE name = '($(basename $file))'" 2>/dev/null)" != "1" ];then + psql $DATABASE_URL --single-transaction -f "$file" -c "INSERT INTO sql_extensions VALUES ('$(basename $file)');" fi done fi diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..83e1e66 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,76 @@ +terraform { + required_providers { + google = { + source = "hashicorp/google" + version = "5.8.0" + } + } + required_version = ">= 0.12, < 2.0.0" +} + +provider "google" { + credentials = "creds.json" + project = var.project_id + region = var.region + zone = var.zone +} + +provider "google-beta" { + credentials = "creds.json" + project = var.project_id + region = var.region + zone = var.zone +} + +module "db" { + db_root_password = var.db_root_password + credentials_file = var.credentials_file + region = var.region + source = "./modules/db" +} + +module "processor" { + db_conn_str_private = module.db.db_conn_str_private + contract_address = var.contract_address + migrations_complete = module.db.migrations_complete + grpc_auth_token = var.grpc_auth_token + grpc_data_service_url = var.grpc_data_service_url + source = "./modules/processor" + sql_network_id = module.db.sql_network_id + starting_version = var.starting_version + zone = var.zone +} + +module "no_auth_policy" { + source = "./modules/no_auth_policy" +} + +module "postgrest" { + db_conn_str_private = module.db.db_conn_str_private + migrations_complete = module.db.migrations_complete + no_auth_policy_data = module.no_auth_policy.policy_data + postgrest_max_rows = var.postgrest_max_rows + region = var.region + source = "./modules/postgrest" + sql_vpc_connector_id = module.db.sql_vpc_connector_id +} + +module "mqtt" { + db_conn_str_private = module.db.db_conn_str_private + mosquitto_password = var.mosquitto_password + source = "./modules/mqtt" + sql_network_id = module.db.sql_network_id + zone = var.zone +} + +module "grafana" { + db_conn_str_private_grafana = module.db.db_conn_str_private_grafana + db_private_ip_and_port = module.db.db_private_ip_and_port + grafana_admin_password = var.grafana_admin_password + grafana_public_password = var.grafana_public_password + migrations_complete = module.db.migrations_complete + no_auth_policy_data = module.no_auth_policy.policy_data + region = var.region + source = "./modules/grafana" + sql_vpc_connector_id = module.db.sql_vpc_connector_id +} diff --git a/terraform/modules/db/main.tf b/terraform/modules/db/main.tf new file mode 100644 index 0000000..369cfb7 --- /dev/null +++ b/terraform/modules/db/main.tf @@ -0,0 +1,197 @@ +# Enabling Private IP: +# https://stackoverflow.com/questions/54278828 +# Destroying VPC peering: +# https://github.com/hashicorp/terraform-provider-google/issues/16275#issuecomment-1825752152 +terraform { + required_providers { + google-beta-sql-network-workaround = { + source = "hashicorp/google-beta" + version = "~>4" + } + } +} + +locals { + db_connection_name = google_sql_database_instance.postgres.connection_name + db_conn_str_auth_proxy = replace( + local.db_conn_str_base, + "IP_ADDRESS", + "127.0.0.1" + ) + db_conn_str_base = join("", [ + "postgres://postgres:", + var.db_root_password, + "@IP_ADDRESS:5432/inbox" + ]) + db_conn_str_private = replace( + local.db_conn_str_base, + "IP_ADDRESS", + "${google_sql_database_instance.postgres.private_ip_address}" + ) + db_conn_str_private_grafana = join("", [ + trimsuffix(local.db_conn_str_private, "inbox"), + "grafana" + ]) + db_private_ip_and_port = join("", [ + google_sql_database_instance.postgres.private_ip_address, + ":5432" + ]) +} + +resource "google_sql_database_instance" "postgres" { + database_version = "POSTGRES_14" + deletion_protection = false + depends_on = [google_service_networking_connection.sql_network_connection] + provider = google-beta + root_password = var.db_root_password + settings { + # Prevents large backfill operations from erroring out. + database_flags { + name = "temp_file_limit" + value = "2147483647" + } + insights_config { + query_insights_enabled = true + query_plans_per_minute = 20 + query_string_length = 4500 + } + ip_configuration { + ipv4_enabled = true + private_network = google_compute_network.sql_network.id + } + tier = "db-custom-4-16384" + } +} + +resource "google_sql_database" "database" { + deletion_policy = "ABANDON" + instance = google_sql_database_instance.postgres.name + name = "inbox" +} + +resource "google_sql_database" "grafana_state" { + deletion_policy = "ABANDON" + instance = google_sql_database_instance.postgres.name + name = "grafana" +} + +resource "google_compute_global_address" "postgres_private_ip_address" { + address_type = "INTERNAL" + name = "postgres-private-ip-address" + network = google_compute_network.sql_network.id + prefix_length = 16 + provider = google-beta + purpose = "VPC_PEERING" +} + +resource "google_compute_network" "sql_network" { + name = "sql-network" + provider = google-beta +} + +resource "google_compute_firewall" "default" { + name = "allow-mqtt" + network = google_compute_network.sql_network.name + + allow { + protocol = "tcp" + ports = ["21883", "21884"] + } + + source_ranges = ["0.0.0.0/0"] +} + +resource "google_service_networking_connection" "sql_network_connection" { + network = google_compute_network.sql_network.id + provider = google-beta-sql-network-workaround + reserved_peering_ranges = [google_compute_global_address.postgres_private_ip_address.name] + service = "servicenetworking.googleapis.com" +} + +# Run migrations for the first time. +resource "terraform_data" "run_migrations" { + depends_on = [google_sql_database.database] + provisioner "local-exec" { + # Relative to DSS terraform project root. + command = file("modules/db/run-migrations.sh") + environment = { + DATABASE_URL = local.db_conn_str_auth_proxy, + DB_CONNECTION_NAME = local.db_connection_name, + CREDENTIALS_FILE = var.credentials_file + } + } +} + +# Re-run migrations after database initialization. +# +# Tracked as a separate resource so that followup migrations can be run +# by simply destroying and re-applying this resource. The destroy/re-apply +# approach doesn't work for the initial migrations resource since other +# resources depend on initial migrations and they would have to be deleted +# too if initial migrations were, hence this duplicate. +# +# Upon database creation, migrations will be run twice, but this is not a +# problem because diesel only runs new migrations upon subsequent calls to the +# same database. +resource "terraform_data" "re_run_migrations" { + depends_on = [terraform_data.run_migrations] + provisioner "local-exec" { + command = file("modules/db/run-migrations.sh") + environment = { + DATABASE_URL = local.db_conn_str_auth_proxy, + DB_CONNECTION_NAME = local.db_connection_name, + CREDENTIALS_FILE = var.credentials_file + } + } +} + +resource "google_compute_subnetwork" "sql_connector_subnetwork" { + name = "sql-connector-subnetwork" + ip_cidr_range = "10.8.0.0/28" + region = var.region + network = google_compute_network.sql_network.id +} + +resource "google_project_service" "vpc" { + provider = google-beta + service = "vpcaccess.googleapis.com" + disable_on_destroy = false +} + +resource "google_vpc_access_connector" "sql_vpc_connector" { + depends_on = [terraform_data.run_migrations, google_project_service.vpc] + name = "sql-vpc-connector" + subnet { + name = google_compute_subnetwork.sql_connector_subnetwork.name + } +} + +resource "google_compute_router" "default" { + provider = google-beta + name = "cr-static-ip-router" + network = google_compute_network.sql_network.name + region = google_compute_subnetwork.sql_connector_subnetwork.region +} + +resource "google_compute_address" "default" { + provider = google-beta + name = "cr-static-ip-addr" + region = google_compute_subnetwork.sql_connector_subnetwork.region +} + +resource "google_compute_router_nat" "default" { + provider = google-beta + name = "cr-static-nat" + router = google_compute_router.default.name + region = google_compute_subnetwork.sql_connector_subnetwork.region + + nat_ip_allocate_option = "MANUAL_ONLY" + nat_ips = [google_compute_address.default.self_link] + + source_subnetwork_ip_ranges_to_nat = "LIST_OF_SUBNETWORKS" + subnetwork { + name = google_compute_subnetwork.sql_connector_subnetwork.id + source_ip_ranges_to_nat = ["ALL_IP_RANGES"] + } +} + diff --git a/terraform/modules/db/outputs.tf b/terraform/modules/db/outputs.tf new file mode 100644 index 0000000..72b3579 --- /dev/null +++ b/terraform/modules/db/outputs.tf @@ -0,0 +1,31 @@ +output "db_connection_name" { + value = local.db_connection_name +} + +output "db_conn_str_auth_proxy" { + value = local.db_conn_str_auth_proxy +} + +output "db_conn_str_private" { + value = local.db_conn_str_private +} + +output "db_conn_str_private_grafana" { + value = local.db_conn_str_private_grafana +} + +output "db_private_ip_and_port" { + value = local.db_private_ip_and_port +} + +output "migrations_complete" { + value = terraform_data.run_migrations +} + +output "sql_vpc_connector_id" { + value = google_vpc_access_connector.sql_vpc_connector.id +} + +output "sql_network_id" { + value = google_compute_network.sql_network.id +} diff --git a/terraform/modules/db/run-migrations.sh b/terraform/modules/db/run-migrations.sh new file mode 100644 index 0000000..59c108f --- /dev/null +++ b/terraform/modules/db/run-migrations.sh @@ -0,0 +1,8 @@ +# Run Cloud SQL Auth Proxy in background, run migrations, kill proxy. +cloud-sql-proxy $DB_CONNECTION_NAME --credentials-file $CREDENTIALS_FILE & +ls +sleep 5 # Give proxy time to start up. +echo "Running..." +bash ../sql_extensions/apply-sql-extensions.sh +# https://unix.stackexchange.com/a/104825 +kill $(pgrep cloud-sql-proxy) diff --git a/terraform/modules/db/variables.tf b/terraform/modules/db/variables.tf new file mode 100644 index 0000000..8970dbf --- /dev/null +++ b/terraform/modules/db/variables.tf @@ -0,0 +1,5 @@ +variable "db_root_password" {} + +variable "credentials_file" {} + +variable "region" {} diff --git a/terraform/modules/grafana/main.tf b/terraform/modules/grafana/main.tf new file mode 100644 index 0000000..edd76ee --- /dev/null +++ b/terraform/modules/grafana/main.tf @@ -0,0 +1,168 @@ +locals { + api_base_url = "${google_cloud_run_v2_service.grafana.uri}/api" + # Do not use a space in this name, since the destroy-time provisioner + # for the data source depends on a URL encode function that uses `+` + # instead of `%20`, which the relevant API relies on. + data_source_name = "DSS" + # SQL migrations define read-only role, + # compromised password okay for private networking + grafana_role_pw = "grafana" + mock_public_email = "public@public.com" + startup_delay_seconds = 120 +} + +resource "google_cloud_run_v2_service" "grafana" { + depends_on = [var.migrations_complete] + location = var.region + name = "grafana" + template { + containers { + image = "grafana/grafana-enterprise" + env { + name = "GF_DATABASE_TYPE" + value = "postgres" + } + env { + name = "GF_DATABASE_URL" + value = var.db_conn_str_private_grafana + } + env { + name = "GF_SECURITY_ADMIN_PASSWORD" + value = var.grafana_admin_password + } + ports { + container_port = 3000 + } + } + scaling { + min_instance_count = 1 + max_instance_count = 1 + } + vpc_access { + connector = var.sql_vpc_connector_id + egress = "ALL_TRAFFIC" + } + + } + ingress = "INGRESS_TRAFFIC_ALL" +} + +# After Grafana starts, wait until HTTP API is available. +resource "terraform_data" "startup_delay" { + depends_on = [google_cloud_run_v2_service.grafana] + provisioner "local-exec" { + command = "sleep ${local.startup_delay_seconds}" + } +} + +resource "terraform_data" "data_source" { + depends_on = [terraform_data.startup_delay] + input = { + delete_url = join("", [ + "${local.api_base_url}/datasources/name/", + urlencode(local.data_source_name) + ]) + admin_password = var.grafana_admin_password + } + provisioner "local-exec" { + command = join(" ", [ + "curl -X 'POST'", + "${local.api_base_url}/datasources", + "-H 'accept: application/json'", + "-H 'Content-Type: application/json'", + "--user admin:${var.grafana_admin_password}", + "-d", + join("", [ + "'", + jsonencode({ + access = "proxy" + isDefault = true + jsonData = { + database = "inbox" + sslmode = "disable" + postgresVersion = 1400 + } + secureJsonData = { + password = local.grafana_role_pw + } + name = local.data_source_name + type = "postgres" + url = var.db_private_ip_and_port + user = "grafana" + }), + "'" + ]) + ]) + } + provisioner "local-exec" { + command = join(" ", [ + "curl -X 'DELETE'", + self.output.delete_url, + "-H 'accept: application/json'", + "--user admin:${self.output.admin_password}", + ]) + when = destroy + } +} + +resource "terraform_data" "public_user" { + depends_on = [terraform_data.startup_delay] + input = { + admin_password = var.grafana_admin_password + api_base_url = local.api_base_url + email = urlencode(local.mock_public_email) + } + provisioner "local-exec" { + command = join(" ", [ + "curl -X 'POST'", + "${local.api_base_url}/admin/users", + "-H 'accept: application/json'", + "-H 'Content-Type: application/json'", + "--user admin:${var.grafana_admin_password}", + "-d", + join("", [ + "'", + jsonencode({ + email = local.mock_public_email + login = "public" + password = var.grafana_public_password + }), + "'" + ]) + ]) + } + provisioner "local-exec" { + command = join(" && ", [ + # Lookup user ID from email. + join("", [ + "USER_ID=$(", + join(" ", [ + "curl -X 'GET'", + join("", [ + "${self.output.api_base_url}/users/lookup?loginOrEmail=", + self.output.email + ]), + "-H 'accept: application/json'", + "--user admin:${self.output.admin_password}", + "| jq -r '.id'" + ]), + ")" + ]), + # Delete specified user ID. + join(" ", [ + "curl -X 'DELETE'", + "${self.output.api_base_url}/admin/users/$USER_ID", + "-H 'accept: application/json'", + "--user admin:${self.output.admin_password}", + ]) + ]) + when = destroy + } +} + +resource "google_cloud_run_service_iam_policy" "no_auth_grafana" { + location = google_cloud_run_v2_service.grafana.location + project = google_cloud_run_v2_service.grafana.project + service = google_cloud_run_v2_service.grafana.name + policy_data = var.no_auth_policy_data +} diff --git a/terraform/modules/grafana/outputs.tf b/terraform/modules/grafana/outputs.tf new file mode 100644 index 0000000..bc6a2ea --- /dev/null +++ b/terraform/modules/grafana/outputs.tf @@ -0,0 +1,3 @@ +output "grafana_url" { + value = google_cloud_run_v2_service.grafana.uri +} \ No newline at end of file diff --git a/terraform/modules/grafana/variables.tf b/terraform/modules/grafana/variables.tf new file mode 100644 index 0000000..b127ed0 --- /dev/null +++ b/terraform/modules/grafana/variables.tf @@ -0,0 +1,15 @@ +variable "db_conn_str_private_grafana" {} + +variable "db_private_ip_and_port" {} + +variable "migrations_complete" {} + +variable "no_auth_policy_data" {} + +variable "region" {} + +variable "sql_vpc_connector_id" {} + +variable "grafana_admin_password" {} + +variable "grafana_public_password" {} diff --git a/terraform/modules/mqtt/main.tf b/terraform/modules/mqtt/main.tf new file mode 100644 index 0000000..bb27e12 --- /dev/null +++ b/terraform/modules/mqtt/main.tf @@ -0,0 +1,39 @@ +resource "terraform_data" "instance" { + # Store zone since variables not accessible at destroy time. + input = var.zone + provisioner "local-exec" { + command = join(" ", [ + "gcloud compute instances create-with-container mqtt", + "--container-env", + join(",", [ + "MQTT_PASSWORD=${var.mosquitto_password}", + "DATABASE_URL=${var.db_conn_str_private}" + ]), + "--container-image econialabs/inbox:mqtt", + "--network ${var.sql_network_id}", + "--zone ${var.zone}" + ]) + } + provisioner "local-exec" { + command = join("\n", [ + "result=$(gcloud compute instances list --filter NAME=mqtt)", + "if [ -n \"$result\" ]; then", + join(" ", [ + "gcloud compute instances delete mqtt", + "--quiet", + "--zone ${self.output}" + ]), + "fi" + ]) + when = destroy + } +} + +data "external" "ip" { + depends_on = [terraform_data.instance] + program = [ + "bash", + "-c", + "gcloud compute instances list --filter name=mqtt --format 'json(networkInterfaces[0].accessConfigs[0].natIP)' | jq '.[0].networkInterfaces[0].accessConfigs[0]'", + ] +} diff --git a/terraform/modules/mqtt/outputs.tf b/terraform/modules/mqtt/outputs.tf new file mode 100644 index 0000000..8afc8a0 --- /dev/null +++ b/terraform/modules/mqtt/outputs.tf @@ -0,0 +1,3 @@ +output "mqtt_ip" { + value = data.external.ip.result.natIP +} diff --git a/terraform/modules/mqtt/variables.tf b/terraform/modules/mqtt/variables.tf new file mode 100644 index 0000000..520dadb --- /dev/null +++ b/terraform/modules/mqtt/variables.tf @@ -0,0 +1,7 @@ +variable "db_conn_str_private" {} + +variable "mosquitto_password" {} + +variable "zone" {} + +variable "sql_network_id" {} diff --git a/terraform/modules/no_auth_policy/main.tf b/terraform/modules/no_auth_policy/main.tf new file mode 100644 index 0000000..14c43c1 --- /dev/null +++ b/terraform/modules/no_auth_policy/main.tf @@ -0,0 +1,8 @@ +data "google_iam_policy" "no_auth" { + binding { + role = "roles/run.invoker" + members = [ + "allUsers", + ] + } +} diff --git a/terraform/modules/no_auth_policy/outputs.tf b/terraform/modules/no_auth_policy/outputs.tf new file mode 100644 index 0000000..88099e9 --- /dev/null +++ b/terraform/modules/no_auth_policy/outputs.tf @@ -0,0 +1,3 @@ +output "policy_data" { + value = data.google_iam_policy.no_auth.policy_data +} \ No newline at end of file diff --git a/terraform/modules/postgrest/main.tf b/terraform/modules/postgrest/main.tf new file mode 100644 index 0000000..3e30530 --- /dev/null +++ b/terraform/modules/postgrest/main.tf @@ -0,0 +1,40 @@ +resource "google_cloud_run_v2_service" "postgrest" { + depends_on = [var.migrations_complete] + location = var.region + name = "postgrest" + template { + containers { + image = "postgrest/postgrest:v11.2.1" + env { + name = "PGRST_DB_ANON_ROLE" + value = "web_anon" + } + env { + name = "PGRST_DB_MAX_ROWS" + value = var.postgrest_max_rows + } + env { + name = "PGRST_DB_URI" + value = var.db_conn_str_private + } + ports { + container_port = 3000 + } + } + scaling { + min_instance_count = 1 + max_instance_count = 1 + } + vpc_access { + connector = var.sql_vpc_connector_id + egress = "ALL_TRAFFIC" + } + } +} + +resource "google_cloud_run_service_iam_policy" "no_auth_postgrest" { + location = google_cloud_run_v2_service.postgrest.location + project = google_cloud_run_v2_service.postgrest.project + service = google_cloud_run_v2_service.postgrest.name + policy_data = var.no_auth_policy_data +} diff --git a/terraform/modules/postgrest/outputs.tf b/terraform/modules/postgrest/outputs.tf new file mode 100644 index 0000000..9265ee5 --- /dev/null +++ b/terraform/modules/postgrest/outputs.tf @@ -0,0 +1,3 @@ +output "postgrest_url" { + value = google_cloud_run_v2_service.postgrest.uri +} \ No newline at end of file diff --git a/terraform/modules/postgrest/variables.tf b/terraform/modules/postgrest/variables.tf new file mode 100644 index 0000000..7152372 --- /dev/null +++ b/terraform/modules/postgrest/variables.tf @@ -0,0 +1,11 @@ +variable "db_conn_str_private" {} + +variable "migrations_complete" {} + +variable "no_auth_policy_data" {} + +variable "postgrest_max_rows" {} + +variable "region" {} + +variable "sql_vpc_connector_id" {} \ No newline at end of file diff --git a/terraform/modules/processor/main.tf b/terraform/modules/processor/main.tf new file mode 100644 index 0000000..89d7560 --- /dev/null +++ b/terraform/modules/processor/main.tf @@ -0,0 +1,35 @@ +# https://github.com/hashicorp/terraform-provider-google/issues/5832 +resource "terraform_data" "instance" { + depends_on = [var.migrations_complete] + # Store zone since variables not accessible at destroy time. + input = var.zone + provisioner "local-exec" { + command = join(" ", [ + "gcloud compute instances create-with-container processor", + "--container-env", + join(",", [ + "DATABASE_URL=${var.db_conn_str_private}", + "CONTRACT_ADDRESS=${var.contract_address}", + "GRPC_AUTH_TOKEN=${var.grpc_auth_token}", + "GRPC_DATA_SERVICE_URL=${var.grpc_data_service_url}", + "STARTING_VERSION=${var.starting_version}", + ]), + "--container-image econialabs/inbox:processor", + "--network ${var.sql_network_id}", + "--zone ${var.zone}" + ]) + } + provisioner "local-exec" { + command = join("\n", [ + "result=$(gcloud compute instances list --filter NAME=processor)", + "if [ -n \"$result\" ]; then", + join(" ", [ + "gcloud compute instances delete processor", + "--quiet", + "--zone ${self.output}" + ]), + "fi" + ]) + when = destroy + } +} diff --git a/terraform/modules/processor/variables.tf b/terraform/modules/processor/variables.tf new file mode 100644 index 0000000..84d3913 --- /dev/null +++ b/terraform/modules/processor/variables.tf @@ -0,0 +1,15 @@ +variable "db_conn_str_private" {} + +variable "contract_address" {} + +variable "grpc_auth_token" {} + +variable "grpc_data_service_url" {} + +variable "migrations_complete" {} + +variable "starting_version" {} + +variable "zone" {} + +variable "sql_network_id" {} diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000..be8a4bc --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,90 @@ +output "db_connection_name" { + value = module.db.db_connection_name +} + +output "db_conn_str_auth_proxy" { + sensitive = true + value = module.db.db_conn_str_auth_proxy +} + +output "db_conn_str_private" { + sensitive = true + value = module.db.db_conn_str_private +} + +output "organization_id" { + value = var.organization_id +} + +output "billing_account_id" { + value = var.billing_account_id +} + +output "project_id" { + value = var.project_id +} + +output "project_name" { + value = var.project_name +} + +output "region" { + value = var.region +} + +output "zone" { + value = var.zone +} + +output "db_root_password" { + sensitive = true + value = var.db_root_password +} + +output "contract_address" { + value = var.contract_address +} + +output "starting_version" { + value = var.starting_version +} + +output "grpc_data_service_url" { + value = var.grpc_data_service_url +} + +output "grpc_auth_token" { + sensitive = true + value = var.grpc_auth_token +} + +output "postgrest_max_rows" { + value = var.postgrest_max_rows +} + +output "postgrest_url" { + value = module.postgrest.postgrest_url +} + +output "grafana_url" { + value = module.grafana.grafana_url +} + +output "grafana_admin_password" { + sensitive = true + value = var.grafana_admin_password +} + +output "grafana_public_password" { + sensitive = true + value = var.grafana_public_password +} + +output "mosquitto_password" { + sensitive = true + value = var.mosquitto_password +} + +output "mqtt_ip" { + value = module.mqtt.mqtt_ip +} diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..43e594b --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,33 @@ +variable "credentials_file" { + default = "creds.json" +} + +variable "organization_id" {} + +variable "billing_account_id" {} + +variable "project_id" {} + +variable "project_name" {} + +variable "region" {} + +variable "zone" {} + +variable "db_root_password" {} + +variable "contract_address" {} + +variable "starting_version" {} + +variable "grpc_data_service_url" {} + +variable "grpc_auth_token" {} + +variable "postgrest_max_rows" {} + +variable "grafana_admin_password" {} + +variable "grafana_public_password" {} + +variable "mosquitto_password" {} From 7357438f4470829553b0638a34cbf63d87eb5acc Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Tue, 21 May 2024 02:10:56 +0200 Subject: [PATCH 2/6] update readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index a650ef3..23f20d8 100644 --- a/README.md +++ b/README.md @@ -112,4 +112,12 @@ CREATE TRIGGER notify_event This will emit an MQTT event with the topic as your event type for all your contract's events. +## Terraform + +You can deploy this repo on GCP using Terraform. + +To do so, you first need to create a GCP project and get a credentials file stored at `terraform/creds.json`. + +Then, simply run `terraform apply -var-file variables.tfvars`. + [emojicoin dot fun]: https://github.com/econia-labs/emojicoin-dot-fun From 8a6cda4ffa68fa9a64fba5323a42ac4c0458e692 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Tue, 21 May 2024 02:44:08 +0200 Subject: [PATCH 3/6] add init.sh script --- README.md | 4 +++- terraform/init.sh | 53 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100755 terraform/init.sh diff --git a/README.md b/README.md index 23f20d8..374ab55 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,9 @@ This will emit an MQTT event with the topic as your event type for all your cont You can deploy this repo on GCP using Terraform. -To do so, you first need to create a GCP project and get a credentials file stored at `terraform/creds.json`. +To do so, you first need to create a GCP project. + +Once done, run `PROJECT_ID= terraform/init.sh` to enable the required Google APIs, create a service account, and download the credentials file. Then, simply run `terraform apply -var-file variables.tfvars`. diff --git a/terraform/init.sh b/terraform/init.sh new file mode 100755 index 0000000..e83b690 --- /dev/null +++ b/terraform/init.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +set -e + +if [[ -z "$PROJECT_ID" ]]; then + echo "Must provide PROJECT_ID in environment" 1>&2 + exit 1 +fi + +echo "Setting project:" +gcloud config set project $PROJECT_ID + +echo "Enabling GCP APIs (be patient):" +gcloud services enable \ + artifactregistry.googleapis.com \ + cloudbuild.googleapis.com \ + cloudresourcemanager.googleapis.com \ + compute.googleapis.com \ + iam.googleapis.com \ + run.googleapis.com \ + servicenetworking.googleapis.com \ + sqladmin.googleapis.com \ + vpcaccess.googleapis.com + +echo "Creating service account:" +gcloud iam service-accounts create terraform + +service_account_name="terraform@$PROJECT_ID.iam.gserviceaccount.com" + +script_dir=$(dirname -- "$(readlink -f -- "$BASH_SOURCE")") + +gcloud iam service-accounts keys create \ + "$script_dir/creds.json" \ + --iam-account $service_account_name + +gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member serviceAccount:$service_account_name \ + --role roles/editor + +# https://stackoverflow.com/a/61250654 +gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member serviceAccount:$service_account_name \ + --role roles/run.admin + +# https://serverfault.com/questions/942115 +gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member serviceAccount:$service_account_name \ + --role roles/compute.networkAdmin + +# https://stackoverflow.com/a/54351644 +gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member serviceAccount:$service_account_name \ + --role roles/servicenetworking.serviceAgent From 580ebfe4def950c577334973024f71dd725e59dd Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Thu, 23 May 2024 00:22:25 +0200 Subject: [PATCH 4/6] run terraform fmt --- terraform/main.tf | 16 ++++++++-------- terraform/outputs.tf | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/terraform/main.tf b/terraform/main.tf index 83e1e66..827c413 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -17,9 +17,9 @@ provider "google" { provider "google-beta" { credentials = "creds.json" - project = var.project_id - region = var.region - zone = var.zone + project = var.project_id + region = var.region + zone = var.zone } module "db" { @@ -56,11 +56,11 @@ module "postgrest" { } module "mqtt" { - db_conn_str_private = module.db.db_conn_str_private - mosquitto_password = var.mosquitto_password - source = "./modules/mqtt" - sql_network_id = module.db.sql_network_id - zone = var.zone + db_conn_str_private = module.db.db_conn_str_private + mosquitto_password = var.mosquitto_password + source = "./modules/mqtt" + sql_network_id = module.db.sql_network_id + zone = var.zone } module "grafana" { diff --git a/terraform/outputs.tf b/terraform/outputs.tf index be8a4bc..d0665eb 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -82,7 +82,7 @@ output "grafana_public_password" { output "mosquitto_password" { sensitive = true - value = var.mosquitto_password + value = var.mosquitto_password } output "mqtt_ip" { From e89e21182f1bd134858b2116346bc0bbae3c3336 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Thu, 23 May 2024 00:37:16 +0200 Subject: [PATCH 5/6] address PR comments --- README.md | 5 +++++ cfg/cspell-dictionary.txt | 18 ++++++++++++++++++ terraform/init.sh | 5 ++++- terraform/modules/db/main.tf | 1 - terraform/modules/grafana/main.tf | 2 +- terraform/modules/grafana/outputs.tf | 2 +- terraform/modules/no_auth_policy/outputs.tf | 2 +- terraform/modules/postgrest/outputs.tf | 2 +- terraform/modules/postgrest/variables.tf | 2 +- 9 files changed, 32 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 374ab55..60613b4 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,11 @@ This will emit an MQTT event with the topic as your event type for all your cont You can deploy this repo on GCP using Terraform. +But first, make sure you have installed the required dependencies: + +- `jq` (JSON parsing CLI tool) +- `cloud-sql-proxy` (Google Cloud tool to connect to a database) + To do so, you first need to create a GCP project. Once done, run `PROJECT_ID= terraform/init.sh` to enable the required Google APIs, create a service account, and download the credentials file. diff --git a/cfg/cspell-dictionary.txt b/cfg/cspell-dictionary.txt index 4586c93..9e05ee0 100644 --- a/cfg/cspell-dictionary.txt +++ b/cfg/cspell-dictionary.txt @@ -3,19 +3,27 @@ Econia PGRST PostgREST PostgreSQL +artifactregistry autofix bigdecimal capitalisation chrono clippy +cloudbuild +cloudresourcemanager +creds devnet econialabs emojicoin eventloop eventpoll +gcloud +googleapis +gserviceaccount hadolint healthcheck isready +jsonencode libclang libpq libudev @@ -25,13 +33,23 @@ mosquitto mqtt mqttoptions notif +pgrep plpgsql psql readwrite rumqttc rustls serde +servicenetworking +sqladmin sqlfluff sqlx +sslmode +subnetwork +subnetworks testnet +tfstate +tfvars +trimsuffix +vpcaccess websockets diff --git a/terraform/init.sh b/terraform/init.sh index e83b690..3a2de98 100755 --- a/terraform/init.sh +++ b/terraform/init.sh @@ -2,13 +2,16 @@ set -e +which jq > /dev/null 2>&1 || echo "ERROR: cannot find jq in PATH." && exit 1 +which cloud-sql-proxy > /dev/null 2>&1 || echo "ERROR: cannot find cloud-sql-proxy in PATH." && exit 1 + if [[ -z "$PROJECT_ID" ]]; then echo "Must provide PROJECT_ID in environment" 1>&2 exit 1 fi echo "Setting project:" -gcloud config set project $PROJECT_ID +gcloud config set project "$PROJECT_ID" echo "Enabling GCP APIs (be patient):" gcloud services enable \ diff --git a/terraform/modules/db/main.tf b/terraform/modules/db/main.tf index 369cfb7..7a1ecde 100644 --- a/terraform/modules/db/main.tf +++ b/terraform/modules/db/main.tf @@ -194,4 +194,3 @@ resource "google_compute_router_nat" "default" { source_ip_ranges_to_nat = ["ALL_IP_RANGES"] } } - diff --git a/terraform/modules/grafana/main.tf b/terraform/modules/grafana/main.tf index edd76ee..1cfa1ce 100644 --- a/terraform/modules/grafana/main.tf +++ b/terraform/modules/grafana/main.tf @@ -3,7 +3,7 @@ locals { # Do not use a space in this name, since the destroy-time provisioner # for the data source depends on a URL encode function that uses `+` # instead of `%20`, which the relevant API relies on. - data_source_name = "DSS" + data_source_name = "inbox" # SQL migrations define read-only role, # compromised password okay for private networking grafana_role_pw = "grafana" diff --git a/terraform/modules/grafana/outputs.tf b/terraform/modules/grafana/outputs.tf index bc6a2ea..7be3d07 100644 --- a/terraform/modules/grafana/outputs.tf +++ b/terraform/modules/grafana/outputs.tf @@ -1,3 +1,3 @@ output "grafana_url" { value = google_cloud_run_v2_service.grafana.uri -} \ No newline at end of file +} diff --git a/terraform/modules/no_auth_policy/outputs.tf b/terraform/modules/no_auth_policy/outputs.tf index 88099e9..f3f7467 100644 --- a/terraform/modules/no_auth_policy/outputs.tf +++ b/terraform/modules/no_auth_policy/outputs.tf @@ -1,3 +1,3 @@ output "policy_data" { value = data.google_iam_policy.no_auth.policy_data -} \ No newline at end of file +} diff --git a/terraform/modules/postgrest/outputs.tf b/terraform/modules/postgrest/outputs.tf index 9265ee5..9c20fd6 100644 --- a/terraform/modules/postgrest/outputs.tf +++ b/terraform/modules/postgrest/outputs.tf @@ -1,3 +1,3 @@ output "postgrest_url" { value = google_cloud_run_v2_service.postgrest.uri -} \ No newline at end of file +} diff --git a/terraform/modules/postgrest/variables.tf b/terraform/modules/postgrest/variables.tf index 7152372..ba203f0 100644 --- a/terraform/modules/postgrest/variables.tf +++ b/terraform/modules/postgrest/variables.tf @@ -8,4 +8,4 @@ variable "postgrest_max_rows" {} variable "region" {} -variable "sql_vpc_connector_id" {} \ No newline at end of file +variable "sql_vpc_connector_id" {} From 5d8ecc8a529b15135e3cb488c77e3513a50abc0f Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Thu, 23 May 2024 03:08:12 +0200 Subject: [PATCH 6/6] make get mqtt ip command more readable Co-authored-by: alnoki <43892045+alnoki@users.noreply.github.com> --- terraform/modules/mqtt/main.tf | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/terraform/modules/mqtt/main.tf b/terraform/modules/mqtt/main.tf index bb27e12..5d619f1 100644 --- a/terraform/modules/mqtt/main.tf +++ b/terraform/modules/mqtt/main.tf @@ -34,6 +34,15 @@ data "external" "ip" { program = [ "bash", "-c", - "gcloud compute instances list --filter name=mqtt --format 'json(networkInterfaces[0].accessConfigs[0].natIP)' | jq '.[0].networkInterfaces[0].accessConfigs[0]'", + join(" ", [ + # Query gcloud CLI for natural IP address field of MQTT instance. + join(" ", [ + "gcloud compute instances list", + "--filter name=mqtt", + "--format json(networkInterfaces[0].accessConfigs[0].natIP)", + ]), + # Parse natural IP address field from JSON output. + "| jq '.[0].networkInterfaces[0].accessConfigs[0]'", + ]), ] }