chore(infra): Split terraform files into folders and add domain to production app (#4172)

This commit is contained in:
Andrew Dryga
2024-03-16 11:54:06 -06:00
committed by GitHub
parent a85b9ab185
commit 114696c0ba
68 changed files with 1509 additions and 1553 deletions

View File

@@ -28,9 +28,9 @@ jobs:
- name: Validate cloud-init
run: |
sudo apt-get install -y cloud-init
sudo cloud-init schema --config-file terraform/modules/relay-app/templates/cloud-init.yaml
sudo cloud-init schema --config-file terraform/modules/elixir-app/templates/cloud-init.yaml
sudo cloud-init schema --config-file terraform/modules/gateway-google-cloud-compute/templates/cloud-init.yaml
sudo cloud-init schema --config-file terraform/modules/google-cloud/apps/relay/templates/cloud-init.yaml
sudo cloud-init schema --config-file terraform/modules/google-cloud/apps/elixir/templates/cloud-init.yaml
sudo cloud-init schema --config-file terraform/modules/google-cloud/apps/gateway-region-instance-group/templates/cloud-init.yaml
- name: Check Formatting
working-directory: terraform
run: |

View File

@@ -406,7 +406,7 @@ services:
api:
condition: "service_healthy"
ports:
# XXX: Only 111 ports are used for local dev / testing because Docker Desktop
# NOTE: Only 111 ports are used for local dev / testing because Docker Desktop
# allocates a userland proxy process for each forwarded port X_X.
#
# Large ranges here will bring your machine to its knees.

View File

@@ -106,9 +106,9 @@ product documentation, organized as follows:
- [terraform/examples/gcp/nat_gateway](../terraform/examples/gcp/nat_gateway):
Example Terraform configurations for deploying a cluster of Firezone
gateways behind a NAT gateway on GCP with single egress IP.
- [terraform/modules/gateway-google-cloud-compute](../terraform/modules/gateway-google-cloud-compute):
- [terraform/modules/google-cloud/apps/gateway-region-instance-group](../terraform/modules/google-cloud/apps/gateway-region-instance-group):
Production-ready Terraform module for deploying regional Firezone gateways
to Google Cloud Compute.
to Google Cloud Compute using Regional Instance Groups.
## Quickstart

View File

@@ -24,7 +24,7 @@ defmodule API.ChannelCase do
setup tags do
for presence <- @presences, pid <- presence.fetchers_pids() do
# XXX: If we start using Presence.fetch/2 callback we might want to
# TODO: If we start using Presence.fetch/2 callback we might want to
# contribute to Phoenix.Presence a way to propagate sandbox access from
# the parent to the task supervisor it spawns in start_link/1 every time
# it's used. Because this would not work as is:

View File

@@ -32,7 +32,6 @@ defmodule Domain.Auth.Adapters.OpenIDConnect.Settings.Changeset do
{:error, %Jason.DecodeError{} = _error} ->
[{:discovery_document_uri, "is invalid, unable to parse response"}]
# XXX: Do these occur with Mint?
{:error, {404, _body}} ->
[{:discovery_document_uri, "does not exist"}]

View File

@@ -352,7 +352,7 @@ defmodule Domain.Gateways do
gateways =
connected_gateways
|> Map.keys()
# XXX: This will create a pretty large query to send to Postgres,
# TODO: This will create a pretty large query to send to Postgres,
# we probably want to load connected resources once when gateway connects,
# and persist them in the memory not to query DB every time with a
# `WHERE ... IN (...)`.

View File

@@ -81,7 +81,7 @@ defmodule Domain.Network.Address.Query do
#
# At the same time offset will fit to bigint even for largest CIDR ranges that Firezone supports.
#
# XXX: We can make this code prettier once https://github.com/elixir-ecto/ecto/commit/8f7bb2665bce30dfab18cfed01585c96495575a6 is released.
# TODO: We can make this code prettier once https://github.com/elixir-ecto/ecto/commit/8f7bb2665bce30dfab18cfed01585c96495575a6 is released.
defp series_from_offset_inclusive_to_end_of_cidr(network_cidr, offset) do
from(
i in fragment(

View File

@@ -75,7 +75,7 @@ defmodule Domain.MixProject do
{:opentelemetry_finch, "~> 0.2.0"},
# Mailer deps
# XXX: This is a workaround for the following issue:
# TODO: This is a workaround for the following issue:
# https://github.com/elixir-lang/elixir/issues/12777
# Remove Swoosh from Domain once this is fixed.
{:phoenix_swoosh, "~> 1.0"},

View File

@@ -69,7 +69,7 @@ config :bureaucrat, :json_library, Jason
config :wallaby,
driver: Wallaby.Chrome,
screenshot_on_failure: true,
# XXX: Contribute to Wallaby to make this configurable on the per-process level,
# TODO: Contribute to Wallaby to make this configurable on the per-process level,
# along with buffer to write logs only on process failure
js_logger: false,
hackney_options: [timeout: 10_000, recv_timeout: 10_000]

View File

@@ -119,7 +119,7 @@ services:
firezone-relay
ports:
# XXX: Only 111 ports are used for local dev / testing because Docker Desktop
# NOTE: Only 111 ports are used for local dev / testing because Docker Desktop
# allocates a userland proxy process for each forwarded port X_X.
#
# Large ranges here will bring your machine to its knees.

View File

@@ -128,7 +128,7 @@ services:
firezone-relay
ports:
# XXX: Only 111 ports are used for local dev / testing because Docker Desktop
# NOTE: Only 111 ports are used for local dev / testing because Docker Desktop
# allocates a userland proxy process for each forwarded port X_X.
#
# Large ranges here will bring your machine to its knees.

View File

@@ -132,7 +132,7 @@ services:
firezone-relay
ports:
# XXX: Only 111 ports are used for local dev / testing because Docker Desktop
# NOTE: Only 111 ports are used for local dev / testing because Docker Desktop
# allocates a userland proxy process for each forwarded port X_X.
#
# Large ranges here will bring your machine to its knees.

View File

@@ -59,7 +59,7 @@ resource "postgresql_grant" "grant_execute_on_all_functions_schema_to_metabase"
}
module "metabase" {
source = "../../modules/metabase-app"
source = "../../modules/google-cloud/apps/metabase"
project_id = module.google-cloud-project.project.project_id
compute_network = module.google-cloud-vpc.id

View File

@@ -7,7 +7,7 @@ locals {
module "gateways" {
count = var.gateway_token != null ? 1 : 0
source = "../../modules/gateway-google-cloud-compute"
source = "../../modules/google-cloud/apps/gateway-region-instance-group"
project_id = module.google-cloud-project.project.project_id
compute_network = module.google-cloud-vpc.id

View File

@@ -35,7 +35,7 @@ provider "google-beta" {}
# Create the project
module "google-cloud-project" {
source = "../../modules/google-cloud-project"
source = "../../modules/google-cloud/project"
id = "firezone-prod"
name = "Production Environment"
@@ -91,7 +91,7 @@ resource "google_project_iam_binding" "project_owners" {
# Grant GitHub Actions ability to write to the container registry
module "google-artifact-registry" {
source = "../../modules/google-artifact-registry"
source = "../../modules/google-cloud/artifact-registry"
project_id = module.google-cloud-project.project.project_id
project_name = module.google-cloud-project.name
@@ -107,7 +107,7 @@ module "google-artifact-registry" {
# Create a VPC
module "google-cloud-vpc" {
source = "../../modules/google-cloud-vpc"
source = "../../modules/google-cloud/vpc"
project_id = module.google-cloud-project.project.project_id
name = module.google-cloud-project.project.project_id
@@ -117,14 +117,14 @@ module "google-cloud-vpc" {
# Enable Google Cloud Storage for the project
module "google-cloud-storage" {
source = "../../modules/google-cloud-storage"
source = "../../modules/google-cloud/storage"
project_id = module.google-cloud-project.project.project_id
}
# Create DNS managed zone
module "google-cloud-dns" {
source = "../../modules/google-cloud-dns"
source = "../../modules/google-cloud/dns"
project_id = module.google-cloud-project.project.project_id
@@ -134,7 +134,7 @@ module "google-cloud-dns" {
# Create the Cloud SQL database
module "google-cloud-sql" {
source = "../../modules/google-cloud-sql"
source = "../../modules/google-cloud/sql"
project_id = module.google-cloud-project.project.project_id
network = module.google-cloud-vpc.id
@@ -175,678 +175,10 @@ module "google-cloud-sql" {
}
}
# Generate secrets
resource "random_password" "erlang_cluster_cookie" {
length = 64
special = false
}
resource "random_password" "tokens_key_base" {
length = 64
special = false
}
resource "random_password" "tokens_salt" {
length = 32
special = false
}
resource "random_password" "secret_key_base" {
length = 64
special = false
}
resource "random_password" "live_view_signing_salt" {
length = 32
special = false
}
resource "random_password" "cookie_signing_salt" {
length = 32
special = false
}
resource "random_password" "cookie_encryption_salt" {
length = 32
special = false
}
# Create VPC subnet for the application instances,
# we want all apps to be in the same VPC in order for Erlang clustering to work
resource "google_compute_subnetwork" "apps" {
resource "google_compute_firewall" "ssh-ipv4" {
project = module.google-cloud-project.project.project_id
name = "app"
stack_type = "IPV4_IPV6"
ip_cidr_range = "10.128.0.0/20"
region = local.region
network = module.google-cloud-vpc.id
ipv6_access_type = "EXTERNAL"
private_ip_google_access = true
}
# Create VPN subnet for tooling instances
resource "google_compute_subnetwork" "tools" {
project = module.google-cloud-project.project.project_id
name = "tooling"
stack_type = "IPV4_IPV6"
ip_cidr_range = "10.129.0.0/20"
region = local.region
network = module.google-cloud-vpc.id
ipv6_access_type = "EXTERNAL"
private_ip_google_access = true
}
# Create SQL user and database
resource "random_password" "firezone_db_password" {
length = 16
min_lower = 1
min_upper = 1
min_numeric = 1
min_special = 1
lifecycle {
ignore_changes = [min_lower, min_upper, min_numeric, min_special]
}
}
resource "google_sql_user" "firezone" {
project = module.google-cloud-project.project.project_id
instance = module.google-cloud-sql.master_instance_name
name = "firezone"
password = random_password.firezone_db_password.result
}
resource "google_sql_database" "firezone" {
project = module.google-cloud-project.project.project_id
name = "firezone"
instance = module.google-cloud-sql.master_instance_name
}
# Create IAM users for the database for all project owners
resource "google_sql_user" "iam_users" {
for_each = toset(local.project_owners)
project = module.google-cloud-project.project.project_id
instance = module.google-cloud-sql.master_instance_name
type = "CLOUD_IAM_USER"
name = each.value
}
# We can't remove passwords complete because for IAM users we still need to execute those GRANT statements
provider "postgresql" {
scheme = "gcppostgres"
host = "${module.google-cloud-project.project.project_id}:${local.region}:${module.google-cloud-sql.master_instance_name}"
port = 5432
username = google_sql_user.firezone.name
password = random_password.firezone_db_password.result
superuser = false
sslmode = "disable"
}
resource "postgresql_grant" "grant_select_on_all_tables_schema_to_iam_users" {
for_each = toset(local.project_owners)
database = google_sql_database.firezone.name
privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"]
objects = [] # ALL
object_type = "table"
schema = "public"
role = each.key
depends_on = [
google_sql_user.iam_users
]
}
resource "postgresql_grant" "grant_execute_on_all_functions_schema_to_iam_users" {
for_each = toset(local.project_owners)
database = google_sql_database.firezone.name
privileges = ["EXECUTE"]
objects = [] # ALL
object_type = "function"
schema = "public"
role = each.key
depends_on = [
google_sql_user.iam_users
]
}
# Create bucket for client logs
resource "google_storage_bucket" "client-logs" {
project = module.google-cloud-project.project.project_id
name = "${module.google-cloud-project.project.project_id}-client-logs"
location = "US"
lifecycle_rule {
condition {
age = 3
}
action {
type = "Delete"
}
}
lifecycle_rule {
condition {
age = 1
}
action {
type = "AbortIncompleteMultipartUpload"
}
}
logging {
log_bucket = true
log_object_prefix = "firezone.dev/clients"
}
public_access_prevention = "enforced"
uniform_bucket_level_access = true
lifecycle {
prevent_destroy = true
ignore_changes = []
}
}
locals {
cluster = {
name = "firezone"
cookie = base64encode(random_password.erlang_cluster_cookie.result)
}
shared_application_environment_variables = [
# Database
{
name = "DATABASE_HOST"
value = module.google-cloud-sql.master_instance_ip_address
},
{
name = "DATABASE_NAME"
value = google_sql_database.firezone.name
},
{
name = "DATABASE_USER"
value = google_sql_user.firezone.name
},
{
name = "DATABASE_PASSWORD"
value = google_sql_user.firezone.password
},
# Secrets
{
name = "SECRET_KEY_BASE"
value = random_password.secret_key_base.result
},
{
name = "TOKENS_KEY_BASE"
value = base64encode(random_password.tokens_key_base.result)
},
{
name = "TOKENS_SALT"
value = base64encode(random_password.tokens_salt.result)
},
{
name = "SECRET_KEY_BASE"
value = base64encode(random_password.secret_key_base.result)
},
{
name = "LIVE_VIEW_SIGNING_SALT"
value = base64encode(random_password.live_view_signing_salt.result)
},
{
name = "COOKIE_SIGNING_SALT"
value = base64encode(random_password.cookie_signing_salt.result)
},
{
name = "COOKIE_ENCRYPTION_SALT"
value = base64encode(random_password.cookie_encryption_salt.result)
},
# Erlang
{
name = "ERLANG_DISTRIBUTION_PORT"
value = "9000"
},
{
name = "CLUSTER_NAME"
value = local.cluster.name
},
{
name = "ERLANG_CLUSTER_ADAPTER"
value = "Elixir.Domain.Cluster.GoogleComputeLabelsStrategy"
},
{
name = "ERLANG_CLUSTER_ADAPTER_CONFIG"
value = jsonencode({
project_id = module.google-cloud-project.project.project_id
cluster_name = local.cluster.name
cluster_name_label = "cluster_name"
cluster_version_label = "cluster_version"
cluster_version = split(".", var.image_tag)[0]
node_name_label = "application"
polling_interval_ms = 7000
})
},
{
name = "RELEASE_COOKIE"
value = local.cluster.cookie
},
# Auth
{
name = "AUTH_PROVIDER_ADAPTERS"
value = "email,openid_connect,google_workspace,token,microsoft_entra,okta"
},
# Registry from which Docker install scripts pull from
{
name = "DOCKER_REGISTRY"
value = "ghcr.io/firezone"
},
# Billing system
{
name = "BILLING_ENABLED"
value = "true"
},
{
name = "STRIPE_SECRET_KEY"
value = var.stripe_secret_key
},
{
name = "STRIPE_WEBHOOK_SIGNING_SECRET"
value = var.stripe_webhook_signing_secret
},
{
name = "STRIPE_DEFAULT_PRICE_ID"
value = var.stripe_default_price_id
},
# Telemetry
{
name = "INSTRUMENTATION_CLIENT_LOGS_ENABLED"
value = true
},
{
name = "INSTRUMENTATION_CLIENT_LOGS_BUCKET"
value = google_storage_bucket.client-logs.name
},
# Emails
{
name = "OUTBOUND_EMAIL_ADAPTER"
value = "Elixir.Swoosh.Adapters.Mailgun"
},
{
name = "OUTBOUND_EMAIL_FROM"
value = "notifications@firezone.dev"
},
{
name = "OUTBOUND_EMAIL_ADAPTER_OPTS"
value = jsonencode({
api_key = var.mailgun_server_api_token,
domain = local.tld
})
},
# Feature Flags
{
name = "FEATURE_SIGN_UP_ENABLED"
value = false
}
]
}
module "web" {
source = "../../modules/elixir-app"
project_id = module.google-cloud-project.project.project_id
compute_instance_type = "n1-standard-1"
compute_instance_region = local.region
compute_instance_availability_zones = ["${local.region}-d"]
dns_managed_zone_name = module.google-cloud-dns.zone_name
vpc_network = module.google-cloud-vpc.self_link
vpc_subnetwork = google_compute_subnetwork.apps.self_link
container_registry = module.google-artifact-registry.url
image_repo = module.google-artifact-registry.repo
image = "web"
image_tag = var.image_tag
scaling_horizontal_replicas = 2
observability_log_level = "debug"
erlang_release_name = "firezone"
erlang_cluster_cookie = random_password.erlang_cluster_cookie.result
application_name = "web"
application_version = replace(var.image_tag, ".", "-")
application_dns_tld = "app.${local.tld}"
application_ports = [
{
name = "http"
protocol = "TCP"
port = 8080
health_check = {
initial_delay_sec = 60
check_interval_sec = 15
timeout_sec = 10
healthy_threshold = 1
unhealthy_threshold = 2
http_health_check = {
request_path = "/healthz"
}
}
}
]
application_environment_variables = concat([
# Web Server
{
name = "EXTERNAL_URL"
value = "https://app.${local.tld}"
},
{
name = "PHOENIX_HTTP_WEB_PORT"
value = "8080"
}
], local.shared_application_environment_variables)
application_labels = {
"cluster_name" = local.cluster.name
"cluster_version" = split(".", var.image_tag)[0]
}
}
module "api" {
source = "../../modules/elixir-app"
project_id = module.google-cloud-project.project.project_id
compute_instance_type = "n1-standard-1"
compute_instance_region = local.region
compute_instance_availability_zones = ["${local.region}-d"]
dns_managed_zone_name = module.google-cloud-dns.zone_name
vpc_network = module.google-cloud-vpc.self_link
vpc_subnetwork = google_compute_subnetwork.apps.self_link
container_registry = module.google-artifact-registry.url
image_repo = module.google-artifact-registry.repo
image = "api"
image_tag = var.image_tag
scaling_horizontal_replicas = 2
observability_log_level = "debug"
erlang_release_name = "firezone"
erlang_cluster_cookie = random_password.erlang_cluster_cookie.result
application_name = "api"
application_version = replace(var.image_tag, ".", "-")
application_dns_tld = "api.${local.tld}"
application_ports = [
{
name = "http"
protocol = "TCP"
port = 8080
health_check = {
initial_delay_sec = 60
check_interval_sec = 15
timeout_sec = 10
healthy_threshold = 1
unhealthy_threshold = 3
http_health_check = {
request_path = "/healthz"
}
}
}
]
application_environment_variables = concat([
# Web Server
{
name = "EXTERNAL_URL"
value = "https://api.${local.tld}"
},
{
name = "PHOENIX_HTTP_API_PORT"
value = "8080"
},
], local.shared_application_environment_variables)
application_labels = {
"cluster_name" = local.cluster.name
"cluster_version" = split(".", var.image_tag)[0]
}
application_token_scopes = [
"https://www.googleapis.com/auth/cloud-platform"
]
}
## Allow API nodes to sign URLs for Google Cloud Storage
resource "google_storage_bucket_iam_member" "sign-urls" {
bucket = google_storage_bucket.client-logs.name
role = "roles/storage.objectAdmin"
member = "serviceAccount:${module.api.service_account.email}"
}
resource "google_project_iam_custom_role" "sign-urls" {
project = module.google-cloud-project.project.project_id
title = "Sign URLs for Google Cloud Storage"
role_id = "iam.sign_urls"
permissions = [
"iam.serviceAccounts.signBlob"
]
}
resource "google_project_iam_member" "sign-urls" {
project = module.google-cloud-project.project.project_id
role = "projects/${module.google-cloud-project.project.project_id}/roles/${google_project_iam_custom_role.sign-urls.role_id}"
member = "serviceAccount:${module.api.service_account.email}"
}
# Erlang Cluster
## Allow traffic between Elixir apps for Erlang clustering
resource "google_compute_firewall" "erlang-distribution" {
project = module.google-cloud-project.project.project_id
name = "erlang-distribution"
network = module.google-cloud-vpc.self_link
allow {
protocol = "tcp"
ports = [4369, 9000]
}
allow {
protocol = "udp"
ports = [4369, 9000]
}
source_ranges = [google_compute_subnetwork.apps.ip_cidr_range]
target_tags = concat(module.web.target_tags, module.api.target_tags)
}
## Allow service account to list running instances
resource "google_project_iam_custom_role" "erlang-discovery" {
project = module.google-cloud-project.project.project_id
title = "Read list of Compute instances"
description = "This role is used for Erlang Cluster discovery and allows to list running instances."
role_id = "compute.list_instances"
permissions = [
"compute.instances.list",
"compute.zones.list"
]
}
resource "google_project_iam_member" "application" {
for_each = {
api = module.api.service_account.email
web = module.web.service_account.email
}
project = module.google-cloud-project.project.project_id
role = "projects/${module.google-cloud-project.project.project_id}/roles/${google_project_iam_custom_role.erlang-discovery.role_id}"
member = "serviceAccount:${each.value}"
}
# Deploy relays
module "relays" {
count = var.relay_token != null ? 1 : 0
source = "../../modules/relay-app"
project_id = module.google-cloud-project.project.project_id
instances = {
# XXX: We may need these in the future, but for now, we don't have many
# (if any) Enterprise customers in these regions
# "asia-east1" = {
# cidr_range = "10.129.0.0/24"
# type = "n1-standard-1"
# replicas = 1
# zones = ["asia-east1-a"]
# }
#
# "asia-south1" = {
# cidr_range = "10.130.0.0/24"
# type = "n1-standard-1"
# replicas = 1
# zones = ["asia-south1-a"]
# }
#
# "australia-southeast1" = {
# cidr_range = "10.131.0.0/24"
# type = "n1-standard-1"
# replicas = 1
# zones = ["australia-southeast1-a"]
# }
#
# "me-central1" = {
# cidr_range = "10.133.0.0/24"
# type = "n2-standard-2"
# replicas = 1
# zones = ["me-central1-a"]
# }
#
# "southamerica-east1" = {
# cidr_range = "10.134.0.0/24"
# type = "n1-standard-1"
# replicas = 1
# zones = ["southamerica-east1-b"]
# }
#
# "us-central1" = {
# cidr_range = "10.135.0.0/24"
# type = "n1-standard-1"
# replicas = 1
# zones = ["us-central1-b"]
# }
"europe-west1" = {
cidr_range = "10.132.0.0/24"
type = "n1-standard-1"
replicas = 1
zones = ["europe-west1-d"]
}
"europe-west2" = {
cidr_range = "10.140.0.0/24"
type = "n1-standard-1"
replicas = 1
zones = ["europe-west2-c"]
}
"us-east1" = {
cidr_range = "10.136.0.0/24"
type = "n1-standard-1"
replicas = 1
zones = ["us-east1-d"]
}
"us-west2" = {
cidr_range = "10.137.0.0/24"
type = "n1-standard-1"
replicas = 1
zones = ["us-west2-b"]
}
}
container_registry = module.google-artifact-registry.url
image_repo = module.google-artifact-registry.repo
image = "relay"
image_tag = var.image_tag
observability_log_level = "info,hyper=off,h2=warn,tower=warn"
application_name = "relay"
application_version = replace(var.image_tag, ".", "-")
health_check = {
name = "health"
protocol = "TCP"
port = 8080
initial_delay_sec = 60
check_interval_sec = 15
timeout_sec = 10
healthy_threshold = 1
unhealthy_threshold = 3
http_health_check = {
request_path = "/healthz"
}
}
api_url = "wss://api.${local.tld}"
token = var.relay_token
}
resource "google_compute_firewall" "portal-ssh-ipv4" {
project = module.google-cloud-project.project.project_id
name = "portal-ssh-ipv4"
name = "iap-ssh-ipv4"
network = module.google-cloud-vpc.self_link
allow {
@@ -866,39 +198,16 @@ resource "google_compute_firewall" "portal-ssh-ipv4" {
# Only allows connections using IAP
source_ranges = local.iap_ipv4_ranges
target_tags = concat(module.web.target_tags, module.api.target_tags)
}
resource "google_compute_firewall" "relays-ssh-ipv4" {
count = length(module.relays) > 0 ? 1 : 0
project = module.google-cloud-project.project.project_id
name = "relays-ssh-ipv4"
network = module.google-cloud-vpc.self_link
allow {
protocol = "tcp"
ports = [22]
}
allow {
protocol = "udp"
ports = [22]
}
allow {
protocol = "sctp"
ports = [22]
}
# Only allows connections using IAP
source_ranges = local.iap_ipv4_ranges
target_tags = module.relays[0].target_tags
target_tags = concat(
module.web.target_tags,
module.api.target_tags,
module.domain.target_tags,
length(module.relays) > 0 ? module.relays[0].target_tags : []
)
}
module "ops" {
source = "../../modules/google-cloud-ops"
source = "../../modules/google-cloud/ops"
project_id = module.google-cloud-project.project.project_id

View File

@@ -0,0 +1,628 @@
# Generate secrets
resource "random_password" "erlang_cluster_cookie" {
length = 64
special = false
}
resource "random_password" "tokens_key_base" {
length = 64
special = false
}
resource "random_password" "tokens_salt" {
length = 32
special = false
}
resource "random_password" "secret_key_base" {
length = 64
special = false
}
resource "random_password" "live_view_signing_salt" {
length = 32
special = false
}
resource "random_password" "cookie_signing_salt" {
length = 32
special = false
}
resource "random_password" "cookie_encryption_salt" {
length = 32
special = false
}
# Create VPC subnet for the application instances,
# we want all apps to be in the same VPC in order for Erlang clustering to work
resource "google_compute_subnetwork" "apps" {
project = module.google-cloud-project.project.project_id
name = "app"
stack_type = "IPV4_IPV6"
ip_cidr_range = "10.128.0.0/20"
region = local.region
network = module.google-cloud-vpc.id
ipv6_access_type = "EXTERNAL"
private_ip_google_access = true
}
# Create VPN subnet for tooling instances
resource "google_compute_subnetwork" "tools" {
project = module.google-cloud-project.project.project_id
name = "tooling"
stack_type = "IPV4_IPV6"
ip_cidr_range = "10.129.0.0/20"
region = local.region
network = module.google-cloud-vpc.id
ipv6_access_type = "EXTERNAL"
private_ip_google_access = true
}
# Create SQL user and database
resource "random_password" "firezone_db_password" {
length = 16
min_lower = 1
min_upper = 1
min_numeric = 1
min_special = 1
lifecycle {
ignore_changes = [min_lower, min_upper, min_numeric, min_special]
}
}
resource "google_sql_user" "firezone" {
project = module.google-cloud-project.project.project_id
instance = module.google-cloud-sql.master_instance_name
name = "firezone"
password = random_password.firezone_db_password.result
}
resource "google_sql_database" "firezone" {
project = module.google-cloud-project.project.project_id
name = "firezone"
instance = module.google-cloud-sql.master_instance_name
}
# Create IAM users for the database for all project owners
resource "google_sql_user" "iam_users" {
for_each = toset(local.project_owners)
project = module.google-cloud-project.project.project_id
instance = module.google-cloud-sql.master_instance_name
type = "CLOUD_IAM_USER"
name = each.value
}
# We can't remove passwords complete because for IAM users we still need to execute those GRANT statements
provider "postgresql" {
scheme = "gcppostgres"
host = "${module.google-cloud-project.project.project_id}:${local.region}:${module.google-cloud-sql.master_instance_name}"
port = 5432
username = google_sql_user.firezone.name
password = random_password.firezone_db_password.result
superuser = false
sslmode = "disable"
}
resource "postgresql_grant" "grant_select_on_all_tables_schema_to_iam_users" {
for_each = toset(local.project_owners)
database = google_sql_database.firezone.name
privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"]
objects = [] # ALL
object_type = "table"
schema = "public"
role = each.key
depends_on = [
google_sql_user.iam_users
]
}
resource "postgresql_grant" "grant_execute_on_all_functions_schema_to_iam_users" {
for_each = toset(local.project_owners)
database = google_sql_database.firezone.name
privileges = ["EXECUTE"]
objects = [] # ALL
object_type = "function"
schema = "public"
role = each.key
depends_on = [
google_sql_user.iam_users
]
}
# Create bucket for client logs
resource "google_storage_bucket" "client-logs" {
project = module.google-cloud-project.project.project_id
name = "${module.google-cloud-project.project.project_id}-client-logs"
location = "US"
lifecycle_rule {
condition {
age = 3
}
action {
type = "Delete"
}
}
lifecycle_rule {
condition {
age = 1
}
action {
type = "AbortIncompleteMultipartUpload"
}
}
logging {
log_bucket = true
log_object_prefix = "firezone.dev/clients"
}
public_access_prevention = "enforced"
uniform_bucket_level_access = true
lifecycle {
prevent_destroy = true
ignore_changes = []
}
}
locals {
cluster = {
name = "firezone"
cookie = base64encode(random_password.erlang_cluster_cookie.result)
}
shared_application_environment_variables = [
# Database
{
name = "DATABASE_HOST"
value = module.google-cloud-sql.master_instance_ip_address
},
{
name = "DATABASE_NAME"
value = google_sql_database.firezone.name
},
{
name = "DATABASE_USER"
value = google_sql_user.firezone.name
},
{
name = "DATABASE_PASSWORD"
value = google_sql_user.firezone.password
},
# Secrets
{
name = "SECRET_KEY_BASE"
value = random_password.secret_key_base.result
},
{
name = "TOKENS_KEY_BASE"
value = base64encode(random_password.tokens_key_base.result)
},
{
name = "TOKENS_SALT"
value = base64encode(random_password.tokens_salt.result)
},
{
name = "SECRET_KEY_BASE"
value = base64encode(random_password.secret_key_base.result)
},
{
name = "LIVE_VIEW_SIGNING_SALT"
value = base64encode(random_password.live_view_signing_salt.result)
},
{
name = "COOKIE_SIGNING_SALT"
value = base64encode(random_password.cookie_signing_salt.result)
},
{
name = "COOKIE_ENCRYPTION_SALT"
value = base64encode(random_password.cookie_encryption_salt.result)
},
# Erlang
{
name = "ERLANG_DISTRIBUTION_PORT"
value = "9000"
},
{
name = "CLUSTER_NAME"
value = local.cluster.name
},
{
name = "ERLANG_CLUSTER_ADAPTER"
value = "Elixir.Domain.Cluster.GoogleComputeLabelsStrategy"
},
{
name = "ERLANG_CLUSTER_ADAPTER_CONFIG"
value = jsonencode({
project_id = module.google-cloud-project.project.project_id
cluster_name = local.cluster.name
cluster_name_label = "cluster_name"
cluster_version_label = "cluster_version"
cluster_version = split(".", var.image_tag)[0]
node_name_label = "application"
polling_interval_ms = 7000
})
},
{
name = "RELEASE_COOKIE"
value = local.cluster.cookie
},
# Auth
{
name = "AUTH_PROVIDER_ADAPTERS"
value = "email,openid_connect,google_workspace,token,microsoft_entra,okta"
},
# Registry from which Docker install scripts pull from
{
name = "DOCKER_REGISTRY"
value = "ghcr.io/firezone"
},
# Billing system
{
name = "BILLING_ENABLED"
value = "true"
},
{
name = "STRIPE_SECRET_KEY"
value = var.stripe_secret_key
},
{
name = "STRIPE_WEBHOOK_SIGNING_SECRET"
value = var.stripe_webhook_signing_secret
},
{
name = "STRIPE_DEFAULT_PRICE_ID"
value = var.stripe_default_price_id
},
# Telemetry
{
name = "INSTRUMENTATION_CLIENT_LOGS_ENABLED"
value = true
},
{
name = "INSTRUMENTATION_CLIENT_LOGS_BUCKET"
value = google_storage_bucket.client-logs.name
},
# Emails
{
name = "OUTBOUND_EMAIL_ADAPTER"
value = "Elixir.Swoosh.Adapters.Mailgun"
},
{
name = "OUTBOUND_EMAIL_FROM"
value = "notifications@firezone.dev"
},
{
name = "OUTBOUND_EMAIL_ADAPTER_OPTS"
value = jsonencode({
api_key = var.mailgun_server_api_token,
domain = local.tld
})
},
# Feature Flags
{
name = "FEATURE_SIGN_UP_ENABLED"
value = false
}
]
}
module "domain" {
source = "../../modules/google-cloud/apps/elixir"
project_id = module.google-cloud-project.project.project_id
compute_instance_type = "n1-standard-2"
compute_instance_region = local.region
compute_instance_availability_zones = ["${local.region}-d"]
dns_managed_zone_name = module.google-cloud-dns.zone_name
vpc_network = module.google-cloud-vpc.self_link
vpc_subnetwork = google_compute_subnetwork.apps.self_link
container_registry = module.google-artifact-registry.url
image_repo = module.google-artifact-registry.repo
image = "domain"
image_tag = var.image_tag
scaling_horizontal_replicas = 2
observability_log_level = "debug"
erlang_release_name = "firezone"
erlang_cluster_cookie = random_password.erlang_cluster_cookie.result
application_name = "domain"
application_version = replace(var.image_tag, ".", "-")
application_ports = [
{
name = "http"
protocol = "TCP"
port = 4000
health_check = {
initial_delay_sec = 60
check_interval_sec = 15
timeout_sec = 10
healthy_threshold = 1
unhealthy_threshold = 2
http_health_check = {
request_path = "/healthz"
}
}
}
]
application_environment_variables = concat([
# Background Jobs
{
name = "BACKGROUND_JOBS_ENABLED"
value = "true"
},
], local.shared_application_environment_variables)
application_labels = {
"cluster_name" = local.cluster.name
"cluster_version" = split(".", var.image_tag)[0]
}
}
module "web" {
source = "../../modules/google-cloud/apps/elixir"
project_id = module.google-cloud-project.project.project_id
compute_instance_type = "n1-standard-1"
compute_instance_region = local.region
compute_instance_availability_zones = ["${local.region}-d"]
dns_managed_zone_name = module.google-cloud-dns.zone_name
vpc_network = module.google-cloud-vpc.self_link
vpc_subnetwork = google_compute_subnetwork.apps.self_link
container_registry = module.google-artifact-registry.url
image_repo = module.google-artifact-registry.repo
image = "web"
image_tag = var.image_tag
scaling_horizontal_replicas = 2
observability_log_level = "debug"
erlang_release_name = "firezone"
erlang_cluster_cookie = random_password.erlang_cluster_cookie.result
application_name = "web"
application_version = replace(var.image_tag, ".", "-")
application_dns_tld = "app.${local.tld}"
application_ports = [
{
name = "http"
protocol = "TCP"
port = 8080
health_check = {
initial_delay_sec = 60
check_interval_sec = 15
timeout_sec = 10
healthy_threshold = 1
unhealthy_threshold = 2
http_health_check = {
request_path = "/healthz"
}
}
}
]
application_environment_variables = concat([
# Web Server
{
name = "EXTERNAL_URL"
value = "https://app.${local.tld}"
},
{
name = "PHOENIX_HTTP_WEB_PORT"
value = "8080"
},
{
name = "BACKGROUND_JOBS_ENABLED"
value = "false"
},
], local.shared_application_environment_variables)
application_labels = {
"cluster_name" = local.cluster.name
"cluster_version" = split(".", var.image_tag)[0]
}
}
module "api" {
source = "../../modules/google-cloud/apps/elixir"
project_id = module.google-cloud-project.project.project_id
compute_instance_type = "n1-standard-1"
compute_instance_region = local.region
compute_instance_availability_zones = ["${local.region}-d"]
dns_managed_zone_name = module.google-cloud-dns.zone_name
vpc_network = module.google-cloud-vpc.self_link
vpc_subnetwork = google_compute_subnetwork.apps.self_link
container_registry = module.google-artifact-registry.url
image_repo = module.google-artifact-registry.repo
image = "api"
image_tag = var.image_tag
scaling_horizontal_replicas = 2
observability_log_level = "debug"
erlang_release_name = "firezone"
erlang_cluster_cookie = random_password.erlang_cluster_cookie.result
application_name = "api"
application_version = replace(var.image_tag, ".", "-")
application_dns_tld = "api.${local.tld}"
application_ports = [
{
name = "http"
protocol = "TCP"
port = 8080
health_check = {
initial_delay_sec = 60
check_interval_sec = 15
timeout_sec = 10
healthy_threshold = 1
unhealthy_threshold = 3
http_health_check = {
request_path = "/healthz"
}
}
}
]
application_environment_variables = concat([
# Web Server
{
name = "EXTERNAL_URL"
value = "https://api.${local.tld}"
},
{
name = "PHOENIX_HTTP_API_PORT"
value = "8080"
},
{
name = "BACKGROUND_JOBS_ENABLED"
value = "false"
},
], local.shared_application_environment_variables)
application_labels = {
"cluster_name" = local.cluster.name
"cluster_version" = split(".", var.image_tag)[0]
}
application_token_scopes = [
"https://www.googleapis.com/auth/cloud-platform"
]
}
## Allow API nodes to sign URLs for Google Cloud Storage
resource "google_storage_bucket_iam_member" "sign-urls" {
bucket = google_storage_bucket.client-logs.name
role = "roles/storage.objectAdmin"
member = "serviceAccount:${module.api.service_account.email}"
}
resource "google_project_iam_custom_role" "sign-urls" {
project = module.google-cloud-project.project.project_id
title = "Sign URLs for Google Cloud Storage"
role_id = "iam.sign_urls"
permissions = [
"iam.serviceAccounts.signBlob"
]
}
resource "google_project_iam_member" "sign-urls" {
project = module.google-cloud-project.project.project_id
role = "projects/${module.google-cloud-project.project.project_id}/roles/${google_project_iam_custom_role.sign-urls.role_id}"
member = "serviceAccount:${module.api.service_account.email}"
}
# Erlang Cluster
## Allow traffic between Elixir apps for Erlang clustering
resource "google_compute_firewall" "erlang-distribution" {
project = module.google-cloud-project.project.project_id
name = "erlang-distribution"
network = module.google-cloud-vpc.self_link
allow {
protocol = "tcp"
ports = [4369, 9000]
}
allow {
protocol = "udp"
ports = [4369, 9000]
}
source_ranges = [google_compute_subnetwork.apps.ip_cidr_range]
target_tags = concat(module.web.target_tags, module.api.target_tags, module.domain.target_tags)
}
## Allow service account to list running instances
resource "google_project_iam_custom_role" "erlang-discovery" {
project = module.google-cloud-project.project.project_id
title = "Read list of Compute instances"
description = "This role is used for Erlang Cluster discovery and allows to list running instances."
role_id = "compute.list_instances"
permissions = [
"compute.instances.list",
"compute.zones.list"
]
}
resource "google_project_iam_member" "application" {
for_each = {
api = module.api.service_account.email
web = module.web.service_account.email
domain = module.domain.service_account.email
}
project = module.google-cloud-project.project.project_id
role = "projects/${module.google-cloud-project.project.project_id}/roles/${google_project_iam_custom_role.erlang-discovery.role_id}"
member = "serviceAccount:${each.value}"
}

View File

@@ -0,0 +1,112 @@
module "relays" {
count = var.relay_token != null ? 1 : 0
source = "../../modules/google-cloud/apps/relay"
project_id = module.google-cloud-project.project.project_id
instances = {
# We may deploy more relays as more enterprise
# customers are onboarded from those regions
# "asia-east1" = {
# cidr_range = "10.129.0.0/24"
# type = "n1-standard-1"
# replicas = 1
# zones = ["asia-east1-a"]
# }
#
# "asia-south1" = {
# cidr_range = "10.130.0.0/24"
# type = "n1-standard-1"
# replicas = 1
# zones = ["asia-south1-a"]
# }
#
# "australia-southeast1" = {
# cidr_range = "10.131.0.0/24"
# type = "n1-standard-1"
# replicas = 1
# zones = ["australia-southeast1-a"]
# }
"europe-west1" = {
cidr_range = "10.132.0.0/24"
type = "f1-micro"
replicas = 1
zones = ["europe-west1-d"]
}
# "me-central1" = {
# cidr_range = "10.133.0.0/24"
# type = "n2-standard-2"
# replicas = 1
# zones = ["me-central1-a"]
# }
#
# "southamerica-east1" = {
# cidr_range = "10.134.0.0/24"
# type = "n1-standard-1"
# replicas = 1
# zones = ["southamerica-east1-b"]
# }
"us-central1" = {
cidr_range = "10.135.0.0/24"
type = "f1-micro"
replicas = 1
zones = ["us-central1-b"]
}
"us-east1" = {
cidr_range = "10.136.0.0/24"
type = "f1-micro"
replicas = 2
zones = ["us-east1-d"]
}
"us-west2" = {
cidr_range = "10.137.0.0/24"
type = "f1-micro"
replicas = 2
zones = ["us-west2-b"]
}
"europe-west2" = {
cidr_range = "10.140.0.0/24"
type = "f1-micro"
replicas = 1
zones = ["europe-west2-c"]
}
}
container_registry = module.google-artifact-registry.url
image_repo = module.google-artifact-registry.repo
image = "relay"
image_tag = var.image_tag
observability_log_level = "info,hyper=off,h2=warn,tower=warn"
application_name = "relay"
application_version = replace(var.image_tag, ".", "-")
health_check = {
name = "health"
protocol = "TCP"
port = 8080
initial_delay_sec = 60
check_interval_sec = 15
timeout_sec = 10
healthy_threshold = 1
unhealthy_threshold = 3
http_health_check = {
request_path = "/healthz"
}
}
api_url = "wss://api.${local.tld}"
token = var.relay_token
}

View File

@@ -4,7 +4,7 @@ terraform {
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.5"
version = "~> 3.6"
}
null = {

View File

@@ -1,4 +1,3 @@
resource "google_monitoring_uptime_check_config" "website-https" {
project = module.google-cloud-project.project.project_id

View File

@@ -59,7 +59,7 @@ resource "postgresql_grant" "grant_execute_on_all_functions_schema_to_metabase"
}
module "metabase" {
source = "../../modules/metabase-app"
source = "../../modules/google-cloud/apps/metabase"
project_id = module.google-cloud-project.project.project_id
compute_network = module.google-cloud-vpc.id

View File

@@ -8,6 +8,7 @@ locals {
"trish@firezone.dev"
]
# list of emails for users that should be able to SSH into a demo instance
demo_access = []
region = "us-east1"
@@ -44,7 +45,7 @@ provider "google-beta" {}
# Create the project
module "google-cloud-project" {
source = "../../modules/google-cloud-project"
source = "../../modules/google-cloud/project"
id = "firezone-staging"
name = "Staging Environment"
@@ -61,7 +62,7 @@ resource "google_project_iam_binding" "project_owners" {
# Grant GitHub Actions ability to write to the container registry
module "google-artifact-registry" {
source = "../../modules/google-artifact-registry"
source = "../../modules/google-cloud/artifact-registry"
project_id = module.google-cloud-project.project.project_id
project_name = module.google-cloud-project.name
@@ -76,7 +77,7 @@ module "google-artifact-registry" {
# Create a VPC
module "google-cloud-vpc" {
source = "../../modules/google-cloud-vpc"
source = "../../modules/google-cloud/vpc"
project_id = module.google-cloud-project.project.project_id
name = module.google-cloud-project.project.project_id
@@ -86,14 +87,14 @@ module "google-cloud-vpc" {
# Enable Google Cloud Storage for the project
module "google-cloud-storage" {
source = "../../modules/google-cloud-storage"
source = "../../modules/google-cloud/storage"
project_id = module.google-cloud-project.project.project_id
}
# Create DNS managed zone
module "google-cloud-dns" {
source = "../../modules/google-cloud-dns"
source = "../../modules/google-cloud/dns"
project_id = module.google-cloud-project.project.project_id
@@ -103,7 +104,7 @@ module "google-cloud-dns" {
# Create the Cloud SQL database
module "google-cloud-sql" {
source = "../../modules/google-cloud-sql"
source = "../../modules/google-cloud/sql"
project_id = module.google-cloud-project.project.project_id
network = module.google-cloud-vpc.id
@@ -144,723 +145,11 @@ module "google-cloud-sql" {
}
}
# Generate secrets
resource "random_password" "erlang_cluster_cookie" {
length = 64
special = false
}
resource "random_password" "tokens_key_base" {
length = 64
special = false
}
resource "random_password" "tokens_salt" {
length = 32
special = false
}
resource "random_password" "secret_key_base" {
length = 64
special = false
}
resource "random_password" "live_view_signing_salt" {
length = 32
special = false
}
resource "random_password" "cookie_signing_salt" {
length = 32
special = false
}
resource "random_password" "cookie_encryption_salt" {
length = 32
special = false
}
# Create VPC subnet for the application instances,
# we want all apps to be in the same VPC in order for Erlang clustering to work
resource "google_compute_subnetwork" "apps" {
project = module.google-cloud-project.project.project_id
name = "app"
stack_type = "IPV4_IPV6"
ip_cidr_range = "10.128.0.0/20"
region = local.region
network = module.google-cloud-vpc.id
ipv6_access_type = "EXTERNAL"
private_ip_google_access = true
}
# Deploy the web app to the GCE
resource "random_password" "web_db_password" {
length = 16
min_lower = 1
min_upper = 1
min_numeric = 1
min_special = 1
lifecycle {
ignore_changes = [min_lower, min_upper, min_numeric, min_special]
}
}
# TODO: raname it to "firezone"
resource "google_sql_user" "web" {
project = module.google-cloud-project.project.project_id
instance = module.google-cloud-sql.master_instance_name
name = "web"
password = random_password.web_db_password.result
}
resource "google_sql_database" "firezone" {
project = module.google-cloud-project.project.project_id
name = "firezone"
instance = module.google-cloud-sql.master_instance_name
}
# Create IAM users for the database for all project owners
resource "google_sql_user" "iam_users" {
for_each = toset(local.project_owners)
project = module.google-cloud-project.project.project_id
instance = module.google-cloud-sql.master_instance_name
type = "CLOUD_IAM_USER"
name = each.value
}
# We can't remove passwords complete because for IAM users we still need to execute those GRANT statements
provider "postgresql" {
scheme = "gcppostgres"
host = "${module.google-cloud-project.project.project_id}:${local.region}:${module.google-cloud-sql.master_instance_name}"
port = 5432
username = google_sql_user.web.name
password = random_password.web_db_password.result
superuser = false
sslmode = "disable"
}
resource "postgresql_grant" "grant_select_on_all_tables_schema_to_iam_users" {
for_each = toset(local.project_owners)
database = google_sql_database.firezone.name
privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"]
objects = [] # ALL
object_type = "table"
schema = "public"
role = each.key
depends_on = [
google_sql_user.iam_users
]
}
resource "postgresql_grant" "grant_execute_on_all_functions_schema_to_iam_users" {
for_each = toset(local.project_owners)
database = google_sql_database.firezone.name
privileges = ["EXECUTE"]
objects = [] # ALL
object_type = "function"
schema = "public"
role = each.key
depends_on = [
google_sql_user.iam_users
]
}
resource "google_storage_bucket" "client-logs" {
project = module.google-cloud-project.project.project_id
name = "${module.google-cloud-project.project.project_id}-client-logs"
location = "US"
lifecycle_rule {
condition {
age = 3
}
action {
type = "Delete"
}
}
lifecycle_rule {
condition {
age = 1
}
action {
type = "AbortIncompleteMultipartUpload"
}
}
logging {
log_bucket = true
log_object_prefix = "firezone.dev/clients"
}
public_access_prevention = "enforced"
uniform_bucket_level_access = true
lifecycle {
prevent_destroy = true
ignore_changes = []
}
}
locals {
cluster = {
name = "firezone"
cookie = base64encode(random_password.erlang_cluster_cookie.result)
}
shared_application_environment_variables = [
# Database
{
name = "DATABASE_HOST"
value = module.google-cloud-sql.master_instance_ip_address
},
{
name = "DATABASE_NAME"
value = google_sql_database.firezone.name
},
{
name = "DATABASE_USER"
value = google_sql_user.web.name
},
{
name = "DATABASE_PASSWORD"
value = google_sql_user.web.password
},
# Secrets
{
name = "SECRET_KEY_BASE"
value = random_password.secret_key_base.result
},
{
name = "TOKENS_KEY_BASE"
value = base64encode(random_password.tokens_key_base.result)
},
{
name = "TOKENS_SALT"
value = base64encode(random_password.tokens_salt.result)
},
{
name = "SECRET_KEY_BASE"
value = base64encode(random_password.secret_key_base.result)
},
{
name = "LIVE_VIEW_SIGNING_SALT"
value = base64encode(random_password.live_view_signing_salt.result)
},
{
name = "COOKIE_SIGNING_SALT"
value = base64encode(random_password.cookie_signing_salt.result)
},
{
name = "COOKIE_ENCRYPTION_SALT"
value = base64encode(random_password.cookie_encryption_salt.result)
},
# Erlang
{
name = "ERLANG_DISTRIBUTION_PORT"
value = "9000"
},
{
name = "CLUSTER_NAME"
value = local.cluster.name
},
{
name = "ERLANG_CLUSTER_ADAPTER"
value = "Elixir.Domain.Cluster.GoogleComputeLabelsStrategy"
},
{
name = "ERLANG_CLUSTER_ADAPTER_CONFIG"
value = jsonencode({
project_id = module.google-cloud-project.project.project_id
cluster_name = local.cluster.name
cluster_name_label = "cluster_name"
cluster_version_label = "cluster_version"
cluster_version = split(".", var.image_tag)[0]
node_name_label = "application"
polling_interval_ms = 7000
})
},
{
name = "RELEASE_COOKIE"
value = local.cluster.cookie
},
# Auth
{
name = "AUTH_PROVIDER_ADAPTERS"
value = "email,openid_connect,google_workspace,token,microsoft_entra,okta"
},
# Registry from which Docker install scripts pull from
{
name = "DOCKER_REGISTRY"
value = "${module.google-artifact-registry.url}/${module.google-artifact-registry.repo}"
},
# Billing system
{
name = "BILLING_ENABLED"
value = "true"
},
{
name = "STRIPE_SECRET_KEY"
value = var.stripe_secret_key
},
{
name = "STRIPE_WEBHOOK_SIGNING_SECRET"
value = var.stripe_webhook_signing_secret
},
{
name = "STRIPE_DEFAULT_PRICE_ID"
value = var.stripe_default_price_id
},
# Telemetry
{
name = "INSTRUMENTATION_CLIENT_LOGS_ENABLED"
value = true
},
{
name = "INSTRUMENTATION_CLIENT_LOGS_BUCKET"
value = google_storage_bucket.client-logs.name
},
# Emails
{
name = "OUTBOUND_EMAIL_ADAPTER"
value = "Elixir.Swoosh.Adapters.Mailgun"
},
{
name = "OUTBOUND_EMAIL_FROM"
value = "notifications@firez.one"
},
{
name = "OUTBOUND_EMAIL_ADAPTER_OPTS"
value = jsonencode({
api_key = var.mailgun_server_api_token,
domain = local.tld
})
},
# Feature Flags
{
name = "FEATURE_FLOW_ACTIVITIES_ENABLED"
value = true
},
{
name = "FEATURE_TRAFFIC_FILTERS_ENABLED"
value = true
},
{
name = "FEATURE_SELF_HOSTED_RELAYS_ENABLED"
value = true
},
{
name = "FEATURE_MULTI_SITE_RESOURCES_ENABLED"
value = true
},
{
name = "FEATURE_SIGN_UP_ENABLED"
value = false
}
]
}
module "domain" {
source = "../../modules/elixir-app"
project_id = module.google-cloud-project.project.project_id
compute_instance_type = "n1-standard-2"
compute_instance_region = local.region
compute_instance_availability_zones = ["${local.region}-d"]
dns_managed_zone_name = module.google-cloud-dns.zone_name
vpc_network = module.google-cloud-vpc.self_link
vpc_subnetwork = google_compute_subnetwork.apps.self_link
container_registry = module.google-artifact-registry.url
image_repo = module.google-artifact-registry.repo
image = "domain"
image_tag = var.image_tag
scaling_horizontal_replicas = 2
observability_log_level = "debug"
erlang_release_name = "firezone"
erlang_cluster_cookie = random_password.erlang_cluster_cookie.result
application_name = "domain"
application_version = replace(var.image_tag, ".", "-")
application_ports = [
{
name = "http"
protocol = "TCP"
port = 4000
health_check = {
initial_delay_sec = 60
check_interval_sec = 15
timeout_sec = 10
healthy_threshold = 1
unhealthy_threshold = 2
http_health_check = {
request_path = "/healthz"
}
}
}
]
application_environment_variables = concat([
# Background Jobs
{
name = "BACKGROUND_JOBS_ENABLED"
value = "true"
},
], local.shared_application_environment_variables)
application_labels = {
"cluster_name" = local.cluster.name
"cluster_version" = split(".", var.image_tag)[0]
}
}
module "web" {
source = "../../modules/elixir-app"
project_id = module.google-cloud-project.project.project_id
compute_instance_type = "n1-standard-1"
compute_instance_region = local.region
compute_instance_availability_zones = ["${local.region}-d"]
dns_managed_zone_name = module.google-cloud-dns.zone_name
vpc_network = module.google-cloud-vpc.self_link
vpc_subnetwork = google_compute_subnetwork.apps.self_link
container_registry = module.google-artifact-registry.url
image_repo = module.google-artifact-registry.repo
image = "web"
image_tag = var.image_tag
scaling_horizontal_replicas = 2
observability_log_level = "debug"
erlang_release_name = "firezone"
erlang_cluster_cookie = random_password.erlang_cluster_cookie.result
application_name = "web"
application_version = replace(var.image_tag, ".", "-")
application_dns_tld = "app.${local.tld}"
application_ports = [
{
name = "http"
protocol = "TCP"
port = 8080
health_check = {
initial_delay_sec = 60
check_interval_sec = 15
timeout_sec = 10
healthy_threshold = 1
unhealthy_threshold = 2
http_health_check = {
request_path = "/healthz"
}
}
}
]
application_environment_variables = concat([
# Web Server
{
name = "EXTERNAL_URL"
value = "https://app.${local.tld}"
},
{
name = "PHOENIX_HTTP_WEB_PORT"
value = "8080"
},
{
name = "API_URL_OVERRIDE"
value = "wss://api.${local.tld}"
},
{
name = "BACKGROUND_JOBS_ENABLED"
value = "false"
},
], local.shared_application_environment_variables)
application_labels = {
"cluster_name" = local.cluster.name
"cluster_version" = split(".", var.image_tag)[0]
}
}
module "api" {
source = "../../modules/elixir-app"
project_id = module.google-cloud-project.project.project_id
compute_instance_type = "n1-standard-1"
compute_instance_region = local.region
compute_instance_availability_zones = ["${local.region}-d"]
dns_managed_zone_name = module.google-cloud-dns.zone_name
vpc_network = module.google-cloud-vpc.self_link
vpc_subnetwork = google_compute_subnetwork.apps.self_link
container_registry = module.google-artifact-registry.url
image_repo = module.google-artifact-registry.repo
image = "api"
image_tag = var.image_tag
scaling_horizontal_replicas = 2
observability_log_level = "debug"
erlang_release_name = "firezone"
erlang_cluster_cookie = random_password.erlang_cluster_cookie.result
application_name = "api"
application_version = replace(var.image_tag, ".", "-")
application_dns_tld = "api.${local.tld}"
application_ports = [
{
name = "http"
protocol = "TCP"
port = 8080
health_check = {
initial_delay_sec = 60
check_interval_sec = 15
timeout_sec = 10
healthy_threshold = 1
unhealthy_threshold = 3
http_health_check = {
request_path = "/healthz"
}
}
}
]
application_environment_variables = concat([
# Web Server
{
name = "EXTERNAL_URL"
value = "https://api.${local.tld}"
},
{
name = "PHOENIX_HTTP_API_PORT"
value = "8080"
},
{
name = "BACKGROUND_JOBS_ENABLED"
value = "false"
},
], local.shared_application_environment_variables)
application_labels = {
"cluster_name" = local.cluster.name
"cluster_version" = split(".", var.image_tag)[0]
}
application_token_scopes = [
"https://www.googleapis.com/auth/cloud-platform"
]
}
## Allow API nodes to sign URLs for Google Cloud Storage
resource "google_storage_bucket_iam_member" "sign-urls" {
bucket = google_storage_bucket.client-logs.name
role = "roles/storage.objectAdmin"
member = "serviceAccount:${module.api.service_account.email}"
}
resource "google_project_iam_custom_role" "sign-urls" {
project = module.google-cloud-project.project.project_id
title = "Sign URLs for Google Cloud Storage"
role_id = "iam.sign_urls"
permissions = [
"iam.serviceAccounts.signBlob"
]
}
resource "google_project_iam_member" "sign-urls" {
project = module.google-cloud-project.project.project_id
role = "projects/${module.google-cloud-project.project.project_id}/roles/${google_project_iam_custom_role.sign-urls.role_id}"
member = "serviceAccount:${module.api.service_account.email}"
}
# Erlang Cluster
## Allow traffic between Elixir apps for Erlang clustering
resource "google_compute_firewall" "erlang-distribution" {
project = module.google-cloud-project.project.project_id
name = "erlang-distribution"
network = module.google-cloud-vpc.self_link
allow {
protocol = "tcp"
ports = [4369, 9000]
}
allow {
protocol = "udp"
ports = [4369, 9000]
}
source_ranges = [google_compute_subnetwork.apps.ip_cidr_range]
target_tags = concat(module.web.target_tags, module.api.target_tags, module.domain.target_tags)
}
## Allow service account to list running instances
resource "google_project_iam_custom_role" "erlang-discovery" {
project = module.google-cloud-project.project.project_id
title = "Read list of Compute instances"
description = "This role is used for Erlang Cluster discovery and allows to list running instances."
role_id = "compute.list_instances"
permissions = [
"compute.instances.list",
"compute.zones.list"
]
}
resource "google_project_iam_member" "application" {
for_each = {
api = module.api.service_account.email
web = module.web.service_account.email
domain = module.domain.service_account.email
}
project = module.google-cloud-project.project.project_id
role = "projects/${module.google-cloud-project.project.project_id}/roles/${google_project_iam_custom_role.erlang-discovery.role_id}"
member = "serviceAccount:${each.value}"
}
# Deploy relays
module "relays" {
count = var.relay_token != null ? 1 : 0
source = "../../modules/relay-app"
project_id = module.google-cloud-project.project.project_id
instances = {
"australia-southeast1" = {
cidr_range = "10.131.0.0/24"
type = "f1-micro"
replicas = 1
zones = ["australia-southeast1-a"]
}
"southamerica-east1" = {
cidr_range = "10.134.0.0/24"
type = "f1-micro"
replicas = 1
zones = ["southamerica-east1-b"]
}
"us-east1" = {
cidr_range = "10.136.0.0/24"
type = "f1-micro"
replicas = 1
zones = ["us-east1-d"]
}
"us-west2" = {
cidr_range = "10.137.0.0/24"
type = "f1-micro"
replicas = 1
zones = ["us-west2-b"]
}
"us-central1" = {
cidr_range = "10.135.0.0/24"
type = "f1-micro"
replicas = 1
zones = ["us-central1-b"]
}
}
container_registry = module.google-artifact-registry.url
image_repo = module.google-artifact-registry.repo
image = "relay"
image_tag = var.image_tag
observability_log_level = "debug,firezone_relay=trace,hyper=off,h2=warn,tower=warn,wire=trace"
application_name = "relay"
application_version = replace(var.image_tag, ".", "-")
health_check = {
name = "health"
protocol = "TCP"
port = 8080
initial_delay_sec = 60
check_interval_sec = 15
timeout_sec = 10
healthy_threshold = 1
unhealthy_threshold = 3
http_health_check = {
request_path = "/healthz"
}
}
api_url = "wss://api.${local.tld}"
token = var.relay_token
}
# Enable SSH on staging
resource "google_compute_firewall" "ssh-ipv4" {
project = module.google-cloud-project.project.project_id
name = "staging-ssh-ipv4"
name = "ssh-ipv4"
network = module.google-cloud-vpc.self_link
allow {
@@ -879,13 +168,18 @@ resource "google_compute_firewall" "ssh-ipv4" {
}
source_ranges = ["0.0.0.0/0"]
target_tags = concat(module.web.target_tags, module.api.target_tags, module.domain.target_tags)
target_tags = concat(
module.web.target_tags,
module.api.target_tags,
module.domain.target_tags,
length(module.relays) > 0 ? module.relays[0].target_tags : []
)
}
resource "google_compute_firewall" "ssh-ipv6" {
project = module.google-cloud-project.project.project_id
name = "staging-ssh-ipv6"
name = "ssh-ipv6"
network = module.google-cloud-vpc.self_link
allow {
@@ -904,72 +198,22 @@ resource "google_compute_firewall" "ssh-ipv6" {
}
source_ranges = ["::/0"]
target_tags = concat(module.web.target_tags, module.api.target_tags, module.domain.target_tags)
}
resource "google_compute_firewall" "relays-ssh-ipv4" {
count = length(module.relays) > 0 ? 1 : 0
project = module.google-cloud-project.project.project_id
name = "staging-relays-ssh-ipv4"
network = module.relays[0].network
allow {
protocol = "tcp"
ports = [22]
}
allow {
protocol = "udp"
ports = [22]
}
allow {
protocol = "sctp"
ports = [22]
}
source_ranges = ["0.0.0.0/0"]
target_tags = module.relays[0].target_tags
}
resource "google_compute_firewall" "relays-ssh-ipv6" {
count = length(module.relays) > 0 ? 1 : 0
project = module.google-cloud-project.project.project_id
name = "staging-relays-ssh-ipv6"
network = module.relays[0].network
allow {
protocol = "tcp"
ports = [22]
}
allow {
protocol = "udp"
ports = [22]
}
allow {
protocol = "sctp"
ports = [22]
}
source_ranges = ["::/0"]
target_tags = module.relays[0].target_tags
target_tags = concat(
module.web.target_tags,
module.api.target_tags,
module.domain.target_tags,
length(module.relays) > 0 ? module.relays[0].target_tags : []
)
}
module "ops" {
source = "../../modules/google-cloud-ops"
source = "../../modules/google-cloud/ops"
project_id = module.google-cloud-project.project.project_id
slack_alerts_auth_token = var.slack_alerts_auth_token
slack_alerts_channel = var.slack_alerts_channel
api_host = module.api.host
web_host = module.web.host
domain_host = module.domain.host
api_host = module.api.host
web_host = module.web.host
}

View File

@@ -0,0 +1,631 @@
# Generate secrets
resource "random_password" "erlang_cluster_cookie" {
length = 64
special = false
}
resource "random_password" "tokens_key_base" {
length = 64
special = false
}
resource "random_password" "tokens_salt" {
length = 32
special = false
}
resource "random_password" "secret_key_base" {
length = 64
special = false
}
resource "random_password" "live_view_signing_salt" {
length = 32
special = false
}
resource "random_password" "cookie_signing_salt" {
length = 32
special = false
}
resource "random_password" "cookie_encryption_salt" {
length = 32
special = false
}
# Create VPC subnet for the application instances,
# we want all apps to be in the same VPC in order for Erlang clustering to work
resource "google_compute_subnetwork" "apps" {
project = module.google-cloud-project.project.project_id
name = "app"
stack_type = "IPV4_IPV6"
ip_cidr_range = "10.128.0.0/20"
region = local.region
network = module.google-cloud-vpc.id
ipv6_access_type = "EXTERNAL"
private_ip_google_access = true
}
# Deploy the web app to the GCE
resource "random_password" "web_db_password" {
length = 16
min_lower = 1
min_upper = 1
min_numeric = 1
min_special = 1
lifecycle {
ignore_changes = [min_lower, min_upper, min_numeric, min_special]
}
}
# TODO: raname it to "firezone"
resource "google_sql_user" "web" {
project = module.google-cloud-project.project.project_id
instance = module.google-cloud-sql.master_instance_name
name = "web"
password = random_password.web_db_password.result
}
resource "google_sql_database" "firezone" {
project = module.google-cloud-project.project.project_id
name = "firezone"
instance = module.google-cloud-sql.master_instance_name
}
# Create IAM users for the database for all project owners
resource "google_sql_user" "iam_users" {
for_each = toset(local.project_owners)
project = module.google-cloud-project.project.project_id
instance = module.google-cloud-sql.master_instance_name
type = "CLOUD_IAM_USER"
name = each.value
}
# We can't remove passwords complete because for IAM users we still need to execute those GRANT statements
provider "postgresql" {
scheme = "gcppostgres"
host = "${module.google-cloud-project.project.project_id}:${local.region}:${module.google-cloud-sql.master_instance_name}"
port = 5432
username = google_sql_user.web.name
password = random_password.web_db_password.result
superuser = false
sslmode = "disable"
}
resource "postgresql_grant" "grant_select_on_all_tables_schema_to_iam_users" {
for_each = toset(local.project_owners)
database = google_sql_database.firezone.name
privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"]
objects = [] # ALL
object_type = "table"
schema = "public"
role = each.key
depends_on = [
google_sql_user.iam_users
]
}
resource "postgresql_grant" "grant_execute_on_all_functions_schema_to_iam_users" {
for_each = toset(local.project_owners)
database = google_sql_database.firezone.name
privileges = ["EXECUTE"]
objects = [] # ALL
object_type = "function"
schema = "public"
role = each.key
depends_on = [
google_sql_user.iam_users
]
}
resource "google_storage_bucket" "client-logs" {
project = module.google-cloud-project.project.project_id
name = "${module.google-cloud-project.project.project_id}-client-logs"
location = "US"
lifecycle_rule {
condition {
age = 3
}
action {
type = "Delete"
}
}
lifecycle_rule {
condition {
age = 1
}
action {
type = "AbortIncompleteMultipartUpload"
}
}
logging {
log_bucket = true
log_object_prefix = "firezone.dev/clients"
}
public_access_prevention = "enforced"
uniform_bucket_level_access = true
lifecycle {
prevent_destroy = true
ignore_changes = []
}
}
locals {
cluster = {
name = "firezone"
cookie = base64encode(random_password.erlang_cluster_cookie.result)
}
shared_application_environment_variables = [
# Database
{
name = "DATABASE_HOST"
value = module.google-cloud-sql.master_instance_ip_address
},
{
name = "DATABASE_NAME"
value = google_sql_database.firezone.name
},
{
name = "DATABASE_USER"
value = google_sql_user.web.name
},
{
name = "DATABASE_PASSWORD"
value = google_sql_user.web.password
},
# Secrets
{
name = "SECRET_KEY_BASE"
value = random_password.secret_key_base.result
},
{
name = "TOKENS_KEY_BASE"
value = base64encode(random_password.tokens_key_base.result)
},
{
name = "TOKENS_SALT"
value = base64encode(random_password.tokens_salt.result)
},
{
name = "SECRET_KEY_BASE"
value = base64encode(random_password.secret_key_base.result)
},
{
name = "LIVE_VIEW_SIGNING_SALT"
value = base64encode(random_password.live_view_signing_salt.result)
},
{
name = "COOKIE_SIGNING_SALT"
value = base64encode(random_password.cookie_signing_salt.result)
},
{
name = "COOKIE_ENCRYPTION_SALT"
value = base64encode(random_password.cookie_encryption_salt.result)
},
# Erlang
{
name = "ERLANG_DISTRIBUTION_PORT"
value = "9000"
},
{
name = "CLUSTER_NAME"
value = local.cluster.name
},
{
name = "ERLANG_CLUSTER_ADAPTER"
value = "Elixir.Domain.Cluster.GoogleComputeLabelsStrategy"
},
{
name = "ERLANG_CLUSTER_ADAPTER_CONFIG"
value = jsonencode({
project_id = module.google-cloud-project.project.project_id
cluster_name = local.cluster.name
cluster_name_label = "cluster_name"
cluster_version_label = "cluster_version"
cluster_version = split(".", var.image_tag)[0]
node_name_label = "application"
polling_interval_ms = 7000
})
},
{
name = "RELEASE_COOKIE"
value = local.cluster.cookie
},
# Auth
{
name = "AUTH_PROVIDER_ADAPTERS"
value = "email,openid_connect,google_workspace,token,microsoft_entra,okta"
},
# Registry from which Docker install scripts pull from
{
name = "DOCKER_REGISTRY"
value = "${module.google-artifact-registry.url}/${module.google-artifact-registry.repo}"
},
# Billing system
{
name = "BILLING_ENABLED"
value = "true"
},
{
name = "STRIPE_SECRET_KEY"
value = var.stripe_secret_key
},
{
name = "STRIPE_WEBHOOK_SIGNING_SECRET"
value = var.stripe_webhook_signing_secret
},
{
name = "STRIPE_DEFAULT_PRICE_ID"
value = var.stripe_default_price_id
},
# Telemetry
{
name = "INSTRUMENTATION_CLIENT_LOGS_ENABLED"
value = true
},
{
name = "INSTRUMENTATION_CLIENT_LOGS_BUCKET"
value = google_storage_bucket.client-logs.name
},
# Emails
{
name = "OUTBOUND_EMAIL_ADAPTER"
value = "Elixir.Swoosh.Adapters.Mailgun"
},
{
name = "OUTBOUND_EMAIL_FROM"
value = "notifications@firez.one"
},
{
name = "OUTBOUND_EMAIL_ADAPTER_OPTS"
value = jsonencode({
api_key = var.mailgun_server_api_token,
domain = local.tld
})
},
# Feature Flags
{
name = "FEATURE_FLOW_ACTIVITIES_ENABLED"
value = true
},
{
name = "FEATURE_TRAFFIC_FILTERS_ENABLED"
value = true
},
{
name = "FEATURE_SELF_HOSTED_RELAYS_ENABLED"
value = true
},
{
name = "FEATURE_MULTI_SITE_RESOURCES_ENABLED"
value = true
},
{
name = "FEATURE_SIGN_UP_ENABLED"
value = false
}
]
}
module "domain" {
source = "../../modules/google-cloud/apps/elixir"
project_id = module.google-cloud-project.project.project_id
compute_instance_type = "n1-standard-2"
compute_instance_region = local.region
compute_instance_availability_zones = ["${local.region}-d"]
dns_managed_zone_name = module.google-cloud-dns.zone_name
vpc_network = module.google-cloud-vpc.self_link
vpc_subnetwork = google_compute_subnetwork.apps.self_link
container_registry = module.google-artifact-registry.url
image_repo = module.google-artifact-registry.repo
image = "domain"
image_tag = var.image_tag
scaling_horizontal_replicas = 2
observability_log_level = "debug"
erlang_release_name = "firezone"
erlang_cluster_cookie = random_password.erlang_cluster_cookie.result
application_name = "domain"
application_version = replace(var.image_tag, ".", "-")
application_ports = [
{
name = "http"
protocol = "TCP"
port = 4000
health_check = {
initial_delay_sec = 60
check_interval_sec = 15
timeout_sec = 10
healthy_threshold = 1
unhealthy_threshold = 2
http_health_check = {
request_path = "/healthz"
}
}
}
]
application_environment_variables = concat([
# Background Jobs
{
name = "BACKGROUND_JOBS_ENABLED"
value = "true"
},
], local.shared_application_environment_variables)
application_labels = {
"cluster_name" = local.cluster.name
"cluster_version" = split(".", var.image_tag)[0]
}
}
module "web" {
source = "../../modules/google-cloud/apps/elixir"
project_id = module.google-cloud-project.project.project_id
compute_instance_type = "n1-standard-1"
compute_instance_region = local.region
compute_instance_availability_zones = ["${local.region}-d"]
dns_managed_zone_name = module.google-cloud-dns.zone_name
vpc_network = module.google-cloud-vpc.self_link
vpc_subnetwork = google_compute_subnetwork.apps.self_link
container_registry = module.google-artifact-registry.url
image_repo = module.google-artifact-registry.repo
image = "web"
image_tag = var.image_tag
scaling_horizontal_replicas = 2
observability_log_level = "debug"
erlang_release_name = "firezone"
erlang_cluster_cookie = random_password.erlang_cluster_cookie.result
application_name = "web"
application_version = replace(var.image_tag, ".", "-")
application_dns_tld = "app.${local.tld}"
application_ports = [
{
name = "http"
protocol = "TCP"
port = 8080
health_check = {
initial_delay_sec = 60
check_interval_sec = 15
timeout_sec = 10
healthy_threshold = 1
unhealthy_threshold = 2
http_health_check = {
request_path = "/healthz"
}
}
}
]
application_environment_variables = concat([
# Web Server
{
name = "EXTERNAL_URL"
value = "https://app.${local.tld}"
},
{
name = "PHOENIX_HTTP_WEB_PORT"
value = "8080"
},
{
name = "API_URL_OVERRIDE"
value = "wss://api.${local.tld}"
},
{
name = "BACKGROUND_JOBS_ENABLED"
value = "false"
},
], local.shared_application_environment_variables)
application_labels = {
"cluster_name" = local.cluster.name
"cluster_version" = split(".", var.image_tag)[0]
}
}
module "api" {
source = "../../modules/google-cloud/apps/elixir"
project_id = module.google-cloud-project.project.project_id
compute_instance_type = "n1-standard-1"
compute_instance_region = local.region
compute_instance_availability_zones = ["${local.region}-d"]
dns_managed_zone_name = module.google-cloud-dns.zone_name
vpc_network = module.google-cloud-vpc.self_link
vpc_subnetwork = google_compute_subnetwork.apps.self_link
container_registry = module.google-artifact-registry.url
image_repo = module.google-artifact-registry.repo
image = "api"
image_tag = var.image_tag
scaling_horizontal_replicas = 2
observability_log_level = "debug"
erlang_release_name = "firezone"
erlang_cluster_cookie = random_password.erlang_cluster_cookie.result
application_name = "api"
application_version = replace(var.image_tag, ".", "-")
application_dns_tld = "api.${local.tld}"
application_ports = [
{
name = "http"
protocol = "TCP"
port = 8080
health_check = {
initial_delay_sec = 60
check_interval_sec = 15
timeout_sec = 10
healthy_threshold = 1
unhealthy_threshold = 3
http_health_check = {
request_path = "/healthz"
}
}
}
]
application_environment_variables = concat([
# Web Server
{
name = "EXTERNAL_URL"
value = "https://api.${local.tld}"
},
{
name = "PHOENIX_HTTP_API_PORT"
value = "8080"
},
{
name = "BACKGROUND_JOBS_ENABLED"
value = "false"
},
], local.shared_application_environment_variables)
application_labels = {
"cluster_name" = local.cluster.name
"cluster_version" = split(".", var.image_tag)[0]
}
application_token_scopes = [
"https://www.googleapis.com/auth/cloud-platform"
]
}
## Allow API nodes to sign URLs for Google Cloud Storage
resource "google_storage_bucket_iam_member" "sign-urls" {
bucket = google_storage_bucket.client-logs.name
role = "roles/storage.objectAdmin"
member = "serviceAccount:${module.api.service_account.email}"
}
resource "google_project_iam_custom_role" "sign-urls" {
project = module.google-cloud-project.project.project_id
title = "Sign URLs for Google Cloud Storage"
role_id = "iam.sign_urls"
permissions = [
"iam.serviceAccounts.signBlob"
]
}
resource "google_project_iam_member" "sign-urls" {
project = module.google-cloud-project.project.project_id
role = "projects/${module.google-cloud-project.project.project_id}/roles/${google_project_iam_custom_role.sign-urls.role_id}"
member = "serviceAccount:${module.api.service_account.email}"
}
# Erlang Cluster
## Allow traffic between Elixir apps for Erlang clustering
resource "google_compute_firewall" "erlang-distribution" {
project = module.google-cloud-project.project.project_id
name = "erlang-distribution"
network = module.google-cloud-vpc.self_link
allow {
protocol = "tcp"
ports = [4369, 9000]
}
allow {
protocol = "udp"
ports = [4369, 9000]
}
source_ranges = [google_compute_subnetwork.apps.ip_cidr_range]
target_tags = concat(module.web.target_tags, module.api.target_tags, module.domain.target_tags)
}
## Allow service account to list running instances
resource "google_project_iam_custom_role" "erlang-discovery" {
project = module.google-cloud-project.project.project_id
title = "Read list of Compute instances"
description = "This role is used for Erlang Cluster discovery and allows to list running instances."
role_id = "compute.list_instances"
permissions = [
"compute.instances.list",
"compute.zones.list"
]
}
resource "google_project_iam_member" "application" {
for_each = {
api = module.api.service_account.email
web = module.web.service_account.email
domain = module.domain.service_account.email
}
project = module.google-cloud-project.project.project_id
role = "projects/${module.google-cloud-project.project.project_id}/roles/${google_project_iam_custom_role.erlang-discovery.role_id}"
member = "serviceAccount:${each.value}"
}

View File

@@ -0,0 +1,79 @@
module "relays" {
count = var.relay_token != null ? 1 : 0
source = "../../modules/google-cloud/apps/relay"
project_id = module.google-cloud-project.project.project_id
instances = {
"australia-southeast1" = {
cidr_range = "10.131.0.0/24"
type = "f1-micro"
replicas = 1
zones = ["australia-southeast1-a"]
}
"southamerica-east1" = {
cidr_range = "10.134.0.0/24"
type = "f1-micro"
replicas = 1
zones = ["southamerica-east1-b"]
}
"us-east1" = {
cidr_range = "10.136.0.0/24"
type = "f1-micro"
replicas = 1
zones = ["us-east1-d"]
}
"us-west2" = {
cidr_range = "10.137.0.0/24"
type = "f1-micro"
replicas = 1
zones = ["us-west2-b"]
}
"us-central1" = {
cidr_range = "10.135.0.0/24"
type = "f1-micro"
replicas = 1
zones = ["us-central1-b"]
}
}
container_registry = module.google-artifact-registry.url
image_repo = module.google-artifact-registry.repo
image = "relay"
image_tag = var.image_tag
observability_log_level = "debug,firezone_relay=trace,hyper=off,h2=warn,tower=warn,wire=trace"
application_name = "relay"
application_version = replace(var.image_tag, ".", "-")
health_check = {
name = "health"
protocol = "TCP"
port = 8080
initial_delay_sec = 60
check_interval_sec = 15
timeout_sec = 10
healthy_threshold = 1
unhealthy_threshold = 3
http_health_check = {
request_path = "/healthz"
}
}
api_url = "wss://api.${local.tld}"
token = var.relay_token
}

View File

@@ -1,10 +1,10 @@
terraform {
required_version = "1.6.6"
required_version = "1.7.5"
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.5"
version = "~> 3.6"
}
null = {

View File

@@ -33,47 +33,6 @@ locals {
)
}
resource "google_monitoring_uptime_check_config" "domain-https" {
project = var.project_id
display_name = "domain-https"
timeout = "60s"
http_check {
port = "443"
use_ssl = true
validate_ssl = true
request_method = "GET"
path = "/healthz"
accepted_response_status_codes {
status_class = "STATUS_CLASS_2XX"
}
}
monitored_resource {
type = "uptime_url"
labels = {
project_id = var.project_id
host = var.domain_host
}
}
content_matchers {
content = "\"ok\""
matcher = "MATCHES_JSON_PATH"
json_path_matcher {
json_path = "$.status"
json_matcher = "EXACT_MATCH"
}
}
checker_type = "STATIC_IP_CHECKERS"
}
resource "google_monitoring_uptime_check_config" "api-https" {
project = var.project_id

View File

@@ -25,7 +25,3 @@ variable "api_host" {
variable "web_host" {
type = string
}
variable "domain_host" {
type = string
}