mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
chore(infra): Split terraform files into folders and add domain to production app (#4172)
This commit is contained in:
6
.github/workflows/_terraform.yml
vendored
6
.github/workflows/_terraform.yml
vendored
@@ -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: |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"}]
|
||||
|
||||
|
||||
@@ -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 (...)`.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
628
terraform/environments/production/portal.tf
Normal file
628
terraform/environments/production/portal.tf
Normal 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}"
|
||||
}
|
||||
112
terraform/environments/production/relays.tf
Normal file
112
terraform/environments/production/relays.tf
Normal 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
|
||||
}
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = "~> 3.5"
|
||||
version = "~> 3.6"
|
||||
}
|
||||
|
||||
null = {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
resource "google_monitoring_uptime_check_config" "website-https" {
|
||||
project = module.google-cloud-project.project.project_id
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
631
terraform/environments/staging/portal.tf
Normal file
631
terraform/environments/staging/portal.tf
Normal 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}"
|
||||
}
|
||||
79
terraform/environments/staging/relays.tf
Normal file
79
terraform/environments/staging/relays.tf
Normal 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
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,7 +25,3 @@ variable "api_host" {
|
||||
variable "web_host" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "domain_host" {
|
||||
type = string
|
||||
}
|
||||
Reference in New Issue
Block a user