diff --git a/.github/workflows/_build_artifacts.yml b/.github/workflows/_build_artifacts.yml index 5843c16f3..c2622a2c4 100644 --- a/.github/workflows/_build_artifacts.yml +++ b/.github/workflows/_build_artifacts.yml @@ -82,6 +82,10 @@ jobs: fail-fast: false matrix: include: + - image_name: domain + target: runtime + build-args: | + APPLICATION_NAME=domain - image_name: api target: runtime build-args: | diff --git a/.github/workflows/_deploy_production.yml b/.github/workflows/_deploy_production.yml index 7363d2575..683a5f758 100644 --- a/.github/workflows/_deploy_production.yml +++ b/.github/workflows/_deploy_production.yml @@ -48,7 +48,7 @@ jobs: - name: Pull and push images run: | set -xe - IMAGES=(client relay gateway api web) + IMAGES=(domain api web gateway relay client) MAJOR_VERSION="${VERSION%%.*}" MAJOR_MINOR_VERSION="${VERSION%.*}" diff --git a/.github/workflows/_integration_tests.yml b/.github/workflows/_integration_tests.yml index 11b581849..b521f6c6c 100644 --- a/.github/workflows/_integration_tests.yml +++ b/.github/workflows/_integration_tests.yml @@ -3,6 +3,14 @@ run-name: Triggered from ${{ github.event_name }} by ${{ github.actor }} on: workflow_call: inputs: + domain_image: + required: false + type: string + default: 'us-east1-docker.pkg.dev/firezone-staging/firezone/domain' + domain_tag: + required: false + type: string + default: ${{ github.sha }} api_image: required: false type: string @@ -61,6 +69,8 @@ jobs: id-token: write pull-requests: write env: + DOMAIN_IMAGE: ${{ inputs.domain_image }} + DOMAIN_TAG: ${{ inputs.domain_tag }} API_IMAGE: ${{ inputs.api_image }} API_TAG: ${{ inputs.api_tag }} WEB_IMAGE: ${{ inputs.web_image }} @@ -99,7 +109,7 @@ jobs: run: | # Start one-by-one to avoid variability in service startup order docker compose up -d dns.httpbin httpbin - docker compose up -d api web --no-build + docker compose up -d api web domain --no-build docker compose up -d relay --no-build docker compose up -d gateway --no-build docker compose up -d client --no-build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39f89b76c..658eab7af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -185,7 +185,7 @@ jobs: # Start services in the same order each time for the tests docker compose up -d iperf3 - docker compose up -d api web --no-build + docker compose up -d api web domain --no-build docker compose up -d relay --no-build docker compose up -d gateway --no-build docker compose up -d client --no-build diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3c6b84402..d1c42c193 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -41,7 +41,7 @@ jobs: - name: Pull and push run: | set -xe - IMAGES=(relay api gateway web client) + IMAGES=(domain api web gateway relay client) MAJOR_VERSION="${VERSION%%.*}" MAJOR_MINOR_VERSION="${VERSION%.*}" diff --git a/docker-compose.yml b/docker-compose.yml index 2ff15e0ba..97fe81098 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -68,7 +68,7 @@ services: # Erlang ERLANG_DISTRIBUTION_PORT: 9000 ERLANG_CLUSTER_ADAPTER: "Elixir.Cluster.Strategy.Epmd" - ERLANG_CLUSTER_ADAPTER_CONFIG: '{"hosts":["api@api.cluster.local","web@web.cluster.local"]}' + ERLANG_CLUSTER_ADAPTER_CONFIG: '{"hosts":["api@api.cluster.local","web@web.cluster.local","domain@web.cluster.local"]}' RELEASE_COOKIE: "NksuBhJFBhjHD1uUa9mDOHV" RELEASE_HOSTNAME: "web.cluster.local" RELEASE_NAME: "web" @@ -87,8 +87,68 @@ services: LIVE_VIEW_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" COOKIE_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" COOKIE_ENCRYPTION_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" - # Telemetry - TELEMETRY_ENABLED: "false" + # Debugging + LOG_LEVEL: "debug" + # Emails + OUTBOUND_EMAIL_FROM: "public-noreply@firez.one" + OUTBOUND_EMAIL_ADAPTER: "Elixir.Swoosh.Adapters.Postmark" + ## Warning: The token is for the blackhole Postmark server created in a separate isolated account, + ## that WILL NOT send any actual emails, but you can see and debug them in the Postmark dashboard. + OUTBOUND_EMAIL_ADAPTER_OPTS: '{"api_key":"7da7d1cd-111c-44a7-b5ac-4027b9d230e5"}' + # Seeds + STATIC_SEEDS: "true" + healthcheck: + test: ["CMD-SHELL", "curl -f localhost:8080/healthz"] + start_period: 10s + interval: 30s + retries: 5 + timeout: 5s + depends_on: + vault: + condition: "service_healthy" + postgres: + condition: "service_healthy" + networks: + - app + + api: + build: + context: elixir + cache_from: + - type=registry,ref=us-east1-docker.pkg.dev/firezone-staging/cache/api:main + args: + APPLICATION_NAME: api + image: ${API_IMAGE:-us-east1-docker.pkg.dev/firezone-staging/firezone/api}:${API_TAG:-main} + hostname: api.cluster.local + ports: + - 8081:8081/tcp + environment: + # Web Server + EXTERNAL_URL: http://localhost:8081/ + PHOENIX_HTTP_API_PORT: "8081" + PHOENIX_SECURE_COOKIES: "false" + # Erlang + ERLANG_DISTRIBUTION_PORT: 9000 + ERLANG_CLUSTER_ADAPTER: "Elixir.Cluster.Strategy.Epmd" + ERLANG_CLUSTER_ADAPTER_CONFIG: '{"hosts":["api@api.cluster.local","web@web.cluster.local","domain@web.cluster.local"]}' + RELEASE_COOKIE: "NksuBhJFBhjHD1uUa9mDOHV" + RELEASE_HOSTNAME: "api.cluster.local" + RELEASE_NAME: "api" + # Database + DATABASE_HOST: postgres + DATABASE_PORT: 5432 + DATABASE_NAME: firezone_dev + DATABASE_USER: postgres + DATABASE_PASSWORD: postgres + # Auth + AUTH_PROVIDER_ADAPTERS: "email,openid_connect,userpass,token,google_workspace,microsoft_entra,okta" + # Secrets + TOKENS_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" + TOKENS_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" + SECRET_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" + LIVE_VIEW_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" + COOKIE_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" + COOKIE_ENCRYPTION_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" # Debugging LOG_LEVEL: "debug" # Emails @@ -104,6 +164,122 @@ services: condition: "service_healthy" postgres: condition: "service_healthy" + healthcheck: + test: ["CMD-SHELL", "curl -f localhost:8081/healthz"] + start_period: 10s + interval: 30s + retries: 5 + timeout: 5s + networks: + - app + + domain: + build: + context: elixir + cache_from: + - type=registry,ref=us-east1-docker.pkg.dev/firezone-staging/cache/domain:main + args: + APPLICATION_NAME: domain + image: ${API_IMAGE:-us-east1-docker.pkg.dev/firezone-staging/firezone/domain}:${API_TAG:-main} + hostname: domain.cluster.local + environment: + # Erlang + ERLANG_DISTRIBUTION_PORT: 9000 + ERLANG_CLUSTER_ADAPTER: "Elixir.Cluster.Strategy.Epmd" + ERLANG_CLUSTER_ADAPTER_CONFIG: '{"hosts":["api@api.cluster.local","web@web.cluster.local","domain@domain.cluster.local"]}' + RELEASE_COOKIE: "NksuBhJFBhjHD1uUa9mDOHV" + RELEASE_HOSTNAME: "domain.cluster.local" + RELEASE_NAME: "domain" + # Database + DATABASE_HOST: postgres + DATABASE_PORT: 5432 + DATABASE_NAME: firezone_dev + DATABASE_USER: postgres + DATABASE_PASSWORD: postgres + # Auth + AUTH_PROVIDER_ADAPTERS: "email,openid_connect,userpass,token,google_workspace,microsoft_entra,okta" + # Secrets + TOKENS_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" + TOKENS_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" + SECRET_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" + LIVE_VIEW_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" + COOKIE_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" + COOKIE_ENCRYPTION_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" + # Debugging + LOG_LEVEL: "debug" + # Emails + OUTBOUND_EMAIL_FROM: "public-noreply@firez.one" + OUTBOUND_EMAIL_ADAPTER: "Elixir.Swoosh.Adapters.Postmark" + ## Warning: The token is for the blackhole Postmark server created in a separate isolated account, + ## that WILL NOT send any actual emails, but you can see and debug them in the Postmark dashboard. + OUTBOUND_EMAIL_ADAPTER_OPTS: '{"api_key":"7da7d1cd-111c-44a7-b5ac-4027b9d230e5"}' + # Seeds + STATIC_SEEDS: "true" + healthcheck: + test: ["CMD-SHELL", "curl -f localhost:4000/healthz"] + start_period: 10s + interval: 30s + retries: 5 + timeout: 5s + depends_on: + vault: + condition: "service_healthy" + postgres: + condition: "service_healthy" + networks: + - app + + # This is a service container which allows to run mix tasks for local development + # without having to install Elixir and Erlang on the host machine. + elixir: + build: + context: elixir + target: compiler + cache_from: + - type=registry,ref=us-east1-docker.pkg.dev/firezone-staging/cache/elixir:main + args: + APPLICATION_NAME: api + image: ${ELIXIR_IMAGE:-us-east1-docker.pkg.dev/firezone-staging/firezone/elixir}:${ELIXIR_TAG:-main} + hostname: elixir + environment: + # Web Server + EXTERNAL_URL: http://localhost:8081/ + # Erlang + ERLANG_DISTRIBUTION_PORT: 9000 + RELEASE_COOKIE: "NksuBhJFBhjHD1uUa9mDOHV" + RELEASE_HOSTNAME: "mix.cluster.local" + RELEASE_NAME: "mix" + # Database + DATABASE_HOST: postgres + DATABASE_PORT: 5432 + DATABASE_NAME: firezone_dev + DATABASE_USER: postgres + DATABASE_PASSWORD: postgres + # Auth + AUTH_PROVIDER_ADAPTERS: "email,openid_connect,userpass,token,google_workspace,microsoft_entra,okta" + # Secrets + TOKENS_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" + TOKENS_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" + SECRET_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" + LIVE_VIEW_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" + COOKIE_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" + COOKIE_ENCRYPTION_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" + # Higher log level not to make seeds output too verbose + LOG_LEVEL: "info" + # Emails + OUTBOUND_EMAIL_FROM: "public-noreply@firez.one" + OUTBOUND_EMAIL_ADAPTER: "Elixir.Swoosh.Adapters.Postmark" + ## Warning: The token is for the blackhole Postmark server created in a separate isolated account, + ## that WILL NOT send any actual emails, but you can see and debug them in the Postmark dashboard. + OUTBOUND_EMAIL_ADAPTER_OPTS: '{"api_key":"7da7d1cd-111c-44a7-b5ac-4027b9d230e5"}' + # Mix env should be set to prod to use secrets declared above, + # otherwise seeds will generate invalid tokens + MIX_ENV: "prod" + # Seeds + STATIC_SEEDS: "true" + depends_on: + postgres: + condition: "service_healthy" networks: - app @@ -240,125 +416,6 @@ services: app: ipv4_address: 172.28.0.101 - api: - build: - context: elixir - cache_from: - - type=registry,ref=us-east1-docker.pkg.dev/firezone-staging/cache/api:main - args: - APPLICATION_NAME: api - image: ${API_IMAGE:-us-east1-docker.pkg.dev/firezone-staging/cache/api}:${API_TAG:-main} - hostname: api.cluster.local - ports: - - 8081:8081/tcp - environment: - # Web Server - EXTERNAL_URL: http://localhost:8081/ - PHOENIX_HTTP_API_PORT: "8081" - PHOENIX_SECURE_COOKIES: "false" - # Erlang - ERLANG_DISTRIBUTION_PORT: 9000 - ERLANG_CLUSTER_ADAPTER: "Elixir.Cluster.Strategy.Epmd" - ERLANG_CLUSTER_ADAPTER_CONFIG: '{"hosts":["api@api.cluster.local","web@web.cluster.local"]}' - RELEASE_COOKIE: "NksuBhJFBhjHD1uUa9mDOHV" - RELEASE_HOSTNAME: "api.cluster.local" - RELEASE_NAME: "api" - # Database - DATABASE_HOST: postgres - DATABASE_PORT: 5432 - DATABASE_NAME: firezone_dev - DATABASE_USER: postgres - DATABASE_PASSWORD: postgres - # Auth - AUTH_PROVIDER_ADAPTERS: "email,openid_connect,userpass,token,google_workspace,microsoft_entra,okta" - # Secrets - TOKENS_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" - TOKENS_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" - SECRET_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" - LIVE_VIEW_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" - COOKIE_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" - COOKIE_ENCRYPTION_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" - # Telemetry - TELEMETRY_ENABLED: "false" - # Debugging - LOG_LEVEL: "debug" - # Emails - OUTBOUND_EMAIL_FROM: "public-noreply@firez.one" - OUTBOUND_EMAIL_ADAPTER: "Elixir.Swoosh.Adapters.Postmark" - ## Warning: The token is for the blackhole Postmark server created in a separate isolated account, - ## that WILL NOT send any actual emails, but you can see and debug them in the Postmark dashboard. - OUTBOUND_EMAIL_ADAPTER_OPTS: '{"api_key":"7da7d1cd-111c-44a7-b5ac-4027b9d230e5"}' - # Seeds - STATIC_SEEDS: "true" - depends_on: - vault: - condition: "service_healthy" - postgres: - condition: "service_healthy" - healthcheck: - test: ["CMD-SHELL", "curl -f localhost:8081/healthz"] - start_period: 10s - interval: 30s - retries: 5 - timeout: 5s - networks: - - app - - # This is a service container which allows to run mix tasks for local development - # without having to install Elixir and Erlang on the host machine. - elixir: - build: - context: elixir - target: compiler - cache_from: - - type=registry,ref=us-east1-docker.pkg.dev/firezone-staging/cache/elixir:main - args: - APPLICATION_NAME: api - image: ${ELIXIR_IMAGE:-us-east1-docker.pkg.dev/firezone-staging/firezone/elixir}:${ELIXIR_TAG:-main} - hostname: elixir - environment: - # Web Server - EXTERNAL_URL: http://localhost:8081/ - # Erlang - ERLANG_DISTRIBUTION_PORT: 9000 - RELEASE_COOKIE: "NksuBhJFBhjHD1uUa9mDOHV" - RELEASE_HOSTNAME: "mix.cluster.local" - RELEASE_NAME: "mix" - # Database - DATABASE_HOST: postgres - DATABASE_PORT: 5432 - DATABASE_NAME: firezone_dev - DATABASE_USER: postgres - DATABASE_PASSWORD: postgres - # Auth - AUTH_PROVIDER_ADAPTERS: "email,openid_connect,userpass,token,google_workspace,microsoft_entra,okta" - # Secrets - TOKENS_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" - TOKENS_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" - SECRET_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" - LIVE_VIEW_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" - COOKIE_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" - COOKIE_ENCRYPTION_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" - # Telemetry - TELEMETRY_ENABLED: "false" - # Higher log level not to make seeds output too verbose - LOG_LEVEL: "info" - # Emails - OUTBOUND_EMAIL_FROM: "public-noreply@firez.one" - OUTBOUND_EMAIL_ADAPTER: "Elixir.Swoosh.Adapters.Postmark" - ## Warning: The token is for the blackhole Postmark server created in a separate isolated account, - ## that WILL NOT send any actual emails, but you can see and debug them in the Postmark dashboard. - OUTBOUND_EMAIL_ADAPTER_OPTS: '{"api_key":"7da7d1cd-111c-44a7-b5ac-4027b9d230e5"}' - # Mix env should be set to prod to use secrets declared above, - # otherwise seeds will generate invalid tokens - MIX_ENV: "prod" - # Seeds - STATIC_SEEDS: "true" - depends_on: - postgres: - condition: "service_healthy" - networks: - - app # IPv6 is currently causing flakiness with GH actions and on our testbed. # Disabling until there's more time to debug. diff --git a/elixir/apps/domain/lib/domain/application.ex b/elixir/apps/domain/lib/domain/application.ex index 31850ce8e..302bce11d 100644 --- a/elixir/apps/domain/lib/domain/application.ex +++ b/elixir/apps/domain/lib/domain/application.ex @@ -37,10 +37,10 @@ defmodule Domain.Application do Domain.Relays, Domain.Gateways, Domain.Clients, - Domain.Billing + Domain.Billing, # Observability - # Domain.Telemetry + Domain.Telemetry ] end end diff --git a/elixir/apps/domain/lib/domain/config/definitions.ex b/elixir/apps/domain/lib/domain/config/definitions.ex index 93d1bdd20..d4856e055 100644 --- a/elixir/apps/domain/lib/domain/config/definitions.ex +++ b/elixir/apps/domain/lib/domain/config/definitions.ex @@ -39,6 +39,13 @@ defmodule Domain.Config.Definitions do def doc_sections do [ + {"Background Jobs", + """ + You need to make sure that at least one of the nodes in the cluster has background jobs enabled. + """, + [ + :background_jobs_enabled + ]}, {"WebServer", [ :external_url, @@ -114,28 +121,30 @@ defmodule Domain.Config.Definitions do [ :instrumentation_client_logs_enabled, :instrumentation_client_logs_bucket - ]}, - {"Telemetry", - [ - :telemetry_enabled, - :telemetry_id ]} ] end + ############################################## + ## Background Jobs + ############################################## + + @doc """ + Enabled or disable background job workers (eg. syncing IdP directory) for the app instance. + """ + defconfig(:background_jobs_enabled, :boolean, default: false) + ############################################## ## Web Server ############################################## @doc """ - The external URL the web UI will be accessible at. + The external URL the UI/API will be accessible at. - Must be a valid and public FQDN for ACME SSL issuance to function. - - You can add a path suffix if you want to serve firezone from a non-root path, - eg: `https://firezone.mycorp.com/vpn/`. + If this field is not set or set to `nil`, the server for `api` and `web` apps will not start. """ defconfig(:external_url, :string, + default: nil, changeset: fn changeset, key -> changeset |> Domain.Validator.validate_uri(key, require_trailing_slash: true) @@ -151,7 +160,7 @@ defmodule Domain.Config.Definitions do defconfig(:phoenix_listen_address, Types.IP, default: "0.0.0.0") @doc """ - Internal port to listen on for the Phoenix web server. + Internal port to listen on for the Phoenix server for the `web` application. """ defconfig(:phoenix_http_web_port, :integer, default: 13_000, @@ -164,7 +173,7 @@ defmodule Domain.Config.Definitions do ) @doc """ - Internal port to listen on for the Phoenix api server. + Internal port to listen on for the Phoenix server for the `api` application. """ defconfig(:phoenix_http_api_port, :integer, default: 13_000, @@ -444,25 +453,6 @@ defmodule Domain.Config.Definitions do """ defconfig(:instrumentation_client_logs_bucket, :string, default: "logs") - ############################################## - ## Telemetry - ############################################## - - @doc """ - Enable or disable the Firezone telemetry collection. - - For more details see https://docs.firezone.dev/reference/telemetry/. - """ - defconfig(:telemetry_enabled, :boolean, default: true) - - defconfig(:telemetry_id, :string, - default: fn -> - :crypto.hash(:sha256, compile_config!(:external_url)) - |> Base.url_encode64(padding: false) - end, - legacy_keys: [{:env, "TID", nil}] - ) - ############################################## ## Gateways ############################################## diff --git a/elixir/apps/domain/lib/domain/telemetry.ex b/elixir/apps/domain/lib/domain/telemetry.ex index e5ed75f0c..db57784f2 100644 --- a/elixir/apps/domain/lib/domain/telemetry.ex +++ b/elixir/apps/domain/lib/domain/telemetry.ex @@ -1,189 +1,50 @@ -# TODO: when app starts for migrations set env to disable connectivity checks and telemetry -# defmodule Domain.Telemetry do -# @moduledoc """ -# Functions for various telemetry events. -# """ -# use Supervisor -# alias Domain.Telemetry.{Timer, PostHog} -# require Logger +defmodule Domain.Telemetry do + use Supervisor + import Telemetry.Metrics + alias Domain.Telemetry -# def start_link(opts) do -# Supervisor.start_link(__MODULE__, opts, name: __MODULE__) -# end + def start_link(arg) do + Supervisor.start_link(__MODULE__, arg, name: __MODULE__) + end -# def init(_opts) do -# config = Domain.Config.fetch_env!(:domain, Domain.Telemetry) + @impl true + def init(_arg) do + children = [ + # We start a /healthz endpoint that is used for liveness probes + {Bandit, plug: Telemetry.HealthzPlug, scheme: :http, port: 4000}, -# if Keyword.fetch!(config, :enabled) == true do -# children = [Timer] -# Supervisor.init(children, strategy: :one_for_one) -# else -# :ignore -# end -# end + # Telemetry poller will execute the given period measurements + # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics + {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} + # Add reporters as children of your supervision tree. + # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} + ] -# def create_api_token do -# PostHog.capture("add_api_token", common_fields()) -# :ok -# end + Supervisor.init(children, strategy: :one_for_one) + end -# def delete_api_token(api_token) do -# PostHog.capture( -# "delete_api_token", -# common_fields() ++ -# [ -# api_token_created_at: api_token.inserted_at -# ] -# ) + def metrics do + [ + # Database Metrics + summary("domain.repo.query.total_time", unit: {:native, :millisecond}), + summary("domain.repo.query.decode_time", unit: {:native, :millisecond}), + summary("domain.repo.query.query_time", unit: {:native, :millisecond}), + summary("domain.repo.query.queue_time", unit: {:native, :millisecond}), + summary("domain.repo.query.idle_time", unit: {:native, :millisecond}), -# :ok -# end + # VM Metrics + summary("vm.memory.total", unit: {:byte, :kilobyte}), + summary("vm.total_run_queue_lengths.total"), + summary("vm.total_run_queue_lengths.cpu"), + summary("vm.total_run_queue_lengths.io") + ] + end -# def add_client do -# PostHog.capture("add_client", common_fields()) -# :ok -# end - -# def add_actor do -# PostHog.capture("add_actor", common_fields()) -# :ok -# end - -# def add_rule do -# PostHog.capture("add_rule", common_fields()) -# :ok -# end - -# def delete_client do -# PostHog.capture("delete_client", common_fields()) -# :ok -# end - -# def delete_actor do -# PostHog.capture("delete_actor", common_fields()) -# :ok -# end - -# def delete_rule do -# PostHog.capture("delete_rule", common_fields()) -# :ok -# end - -# def login do -# PostHog.capture("login", common_fields()) -# :ok -# end - -# def enable_actor do -# PostHog.capture("enable_actor", common_fields()) -# :ok -# end - -# def disable_actor do -# PostHog.capture("disable_actor", common_fields()) -# :ok -# end - -# def domain_started do -# PostHog.capture("domain_started", common_fields()) -# :ok -# end - -# def ping do -# PostHog.capture("ping", ping_data()) -# :ok -# end - -# # How far back to count handshakes as an active client -# # @active_client_window 86_400 -# def ping_data do -# %{ -# local_auth_enabled: {_, local_auth_enabled}, -# logo: {_, logo} -# } = -# Domain.Config.fetch_resolved_configs_with_sources!([ -# :local_auth_enabled, -# :logo -# ]) - -# common_fields() ++ -# [ -# # clients_active_within_24h: Clients.count_active_within(@active_client_window), -# # admin_count: Users.count_by_role(:account_admin_user), -# # actor_count: Users.count(), -# in_docker: in_docker?(), -# # client_count: Clients.count(), -# # max_clients_for_actors: Clients.count_maximum_for_a_actor(), -# # actors_with_mfa: MFA.count_actors_with_mfa_enabled(), -# # actors_with_mfa_totp: MFA.count_actors_with_totp_method(), -# local_authentication: local_auth_enabled, -# # outbound_email: Web.Mailer.active?(), -# external_database: -# external_database?(Map.new(Domain.Config.fetch_env!(:domain, Domain.Repo))), -# logo_type: Domain.Config.Logo.type(logo) -# ] -# end - -# defp in_docker? do -# File.exists?("/.dockerenv") -# end - -# defp common_fields do -# [ -# distinct_id: id(), -# fqdn: fqdn(), -# version: version(), -# kernel_version: "#{os_type()} #{os_version()}" -# ] -# end - -# def id do -# Domain.Config.fetch_env!(:domain, __MODULE__) -# |> Keyword.fetch!(:id) -# end - -# defp fqdn do -# :web -# |> Domain.Config.fetch_env!(Web.Endpoint) -# |> Keyword.get(:url) -# |> Keyword.get(:host) -# end - -# defp version do -# Application.spec(:domain, :vsn) |> to_string() -# end - -# defp external_database?(repo_conf) when is_map_key(repo_conf, :hostname) do -# is_external_db?(repo_conf.hostname) -# end - -# defp external_database?(repo_conf) when is_map_key(repo_conf, :url) do -# %{host: host} = URI.parse(repo_conf.url) - -# is_external_db?(host) -# end - -# defp is_external_db?(host) do -# host != "localhost" && host != "127.0.0.1" -# end - -# defp os_type do -# case :os.type() do -# {:unix, type} -> -# "#{type}" - -# _ -> -# "other" -# end -# end - -# defp os_version do -# case :os.version() do -# {major, minor, patch} -> -# "#{major}.#{minor}.#{patch}" - -# _ -> -# "0.0.0" -# end -# end -# end + defp periodic_measurements do + [ + # A module, function and arguments to be invoked periodically. + # This function must call :telemetry.execute/3 and a metric must be added above. + # {Web, :count_users, []} + ] + end +end diff --git a/elixir/apps/domain/lib/domain/telemetry/healthz_plug.ex b/elixir/apps/domain/lib/domain/telemetry/healthz_plug.ex new file mode 100644 index 000000000..cabaaff4a --- /dev/null +++ b/elixir/apps/domain/lib/domain/telemetry/healthz_plug.ex @@ -0,0 +1,19 @@ +defmodule Domain.Telemetry.HealthzPlug do + @moduledoc """ + A plug that returns a 200 OK response for health checks. + """ + import Plug.Conn + + @behaviour Plug + + @impl true + def init(opts), do: opts + + @impl true + def call(conn, _opts) do + conn + |> put_resp_content_type("application/json") + |> send_resp(200, Jason.encode!(%{status: :ok})) + |> halt() + end +end diff --git a/elixir/apps/domain/mix.exs b/elixir/apps/domain/mix.exs index 11fa6ae93..06c7fa4f9 100644 --- a/elixir/apps/domain/mix.exs +++ b/elixir/apps/domain/mix.exs @@ -60,7 +60,11 @@ defmodule Domain.MixProject do {:libcluster, "~> 3.3"}, # Observability and Runtime debugging + {:bandit, "~> 1.0"}, + {:plug, "~> 1.15"}, {:telemetry, "~> 1.0"}, + {:telemetry_poller, "~> 1.0"}, + {:telemetry_metrics, "~> 0.6.2"}, {:logger_json, "~> 5.1"}, {:recon, "~> 2.5"}, {:observer_cli, "~> 1.7"}, diff --git a/elixir/apps/domain/test/domain/config_test.exs b/elixir/apps/domain/test/domain/config_test.exs index cddf490fc..5278c7048 100644 --- a/elixir/apps/domain/test/domain/config_test.exs +++ b/elixir/apps/domain/test/domain/config_test.exs @@ -97,7 +97,7 @@ defmodule Domain.ConfigTest do test "raises an error when value is missing", %{account: account} do message = """ - Missing required configuration value for 'external_url'. + Missing required configuration value for 'secret_key_base'. ## How to fix? @@ -105,24 +105,19 @@ defmodule Domain.ConfigTest do You can set this configuration via environment variable by adding it to `.env` file: - EXTERNAL_URL=YOUR_VALUE + SECRET_KEY_BASE=YOUR_VALUE ## Documentation - The external URL the web UI will be accessible at. - - Must be a valid and public FQDN for ACME SSL issuance to function. - - You can add a path suffix if you want to serve firezone from a non-root path, - eg: `https://firezone.mycorp.com/vpn/`. + Primary secret key base for the Phoenix application. You can find more information on configuration here: https://www.firezone.dev/docs/reference/env-vars/#environment-variable-listing """ assert_raise RuntimeError, message, fn -> - fetch_resolved_configs!(account.id, [:external_url]) + fetch_resolved_configs!(account.id, [:secret_key_base]) end end end @@ -151,7 +146,7 @@ defmodule Domain.ConfigTest do test "raises an error when value is missing", %{account: account} do message = """ - Missing required configuration value for 'external_url'. + Missing required configuration value for 'secret_key_base'. ## How to fix? @@ -159,24 +154,19 @@ defmodule Domain.ConfigTest do You can set this configuration via environment variable by adding it to `.env` file: - EXTERNAL_URL=YOUR_VALUE + SECRET_KEY_BASE=YOUR_VALUE ## Documentation - The external URL the web UI will be accessible at. - - Must be a valid and public FQDN for ACME SSL issuance to function. - - You can add a path suffix if you want to serve firezone from a non-root path, - eg: `https://firezone.mycorp.com/vpn/`. + Primary secret key base for the Phoenix application. You can find more information on configuration here: https://www.firezone.dev/docs/reference/env-vars/#environment-variable-listing """ assert_raise RuntimeError, message, fn -> - fetch_resolved_configs_with_sources!(account.id, [:external_url]) + fetch_resolved_configs_with_sources!(account.id, [:secret_key_base]) end end @@ -192,12 +182,9 @@ defmodule Domain.ConfigTest do ## Documentation - The external URL the web UI will be accessible at. + The external URL the UI/API will be accessible at. - Must be a valid and public FQDN for ACME SSL issuance to function. - - You can add a path suffix if you want to serve firezone from a non-root path, - eg: `https://firezone.mycorp.com/vpn/`. + If this field is not set or set to `nil`, the server for `api` and `web` apps will not start. You can find more information on configuration here: https://www.firezone.dev/docs/reference/env-vars/#environment-variable-listing diff --git a/elixir/apps/web/lib/web/telemetry.ex b/elixir/apps/web/lib/web/telemetry.ex index a22b5e33c..c5cac76f4 100644 --- a/elixir/apps/web/lib/web/telemetry.ex +++ b/elixir/apps/web/lib/web/telemetry.ex @@ -49,13 +49,7 @@ defmodule Web.Telemetry do summary("phoenix.channel_handled_in.duration", tags: [:event], unit: {:native, :millisecond} - ), - - # VM Metrics - summary("vm.memory.total", unit: {:byte, :kilobyte}), - summary("vm.total_run_queue_lengths.total"), - summary("vm.total_run_queue_lengths.cpu"), - summary("vm.total_run_queue_lengths.io") + ) ] end diff --git a/elixir/config/runtime.exs b/elixir/config/runtime.exs index 5cd932c78..baf56fd7b 100644 --- a/elixir/config/runtime.exs +++ b/elixir/config/runtime.exs @@ -18,15 +18,6 @@ if config_env() == :prod do ssl_opts: compile_config!(:database_ssl_opts), parameters: compile_config!(:database_parameters) - external_url = compile_config!(:external_url) - - %{ - scheme: external_url_scheme, - host: external_url_host, - port: external_url_port, - path: external_url_path - } = URI.parse(external_url) - config :domain, Domain.Tokens, key_base: compile_config!(:tokens_key_base), salt: compile_config!(:tokens_salt) @@ -35,10 +26,6 @@ if config_env() == :prod do gateway_ipv4_masquerade: compile_config!(:gateway_ipv4_masquerade), gateway_ipv6_masquerade: compile_config!(:gateway_ipv6_masquerade) - config :domain, Domain.Telemetry, - enabled: compile_config!(:telemetry_enabled), - id: compile_config!(:telemetry_id) - config :domain, Domain.Auth.Adapters.GoogleWorkspace.APIClient, finch_transport_opts: compile_config!(:http_client_ssl_opts) @@ -78,70 +65,91 @@ if config_env() == :prod do config :domain, outbound_email_adapter_configured?: !!compile_config!(:outbound_email_adapter) - ############################### - ##### Web ##################### - ############################### + # Enable background jobs only on dedicated nodes + config :domain, Domain.Tokens.Jobs, enabled: compile_config!(:background_jobs_enabled) - config :web, Web.Endpoint, - http: [ - ip: compile_config!(:phoenix_listen_address).address, - port: compile_config!(:phoenix_http_web_port), - protocol_options: compile_config!(:phoenix_http_protocol_options) - ], - url: [ + config :domain, Domain.Auth.Adapters.GoogleWorkspace.Jobs, + enabled: compile_config!(:background_jobs_enabled) + + config :domain, Domain.Auth.Adapters.MicrosoftEntra.Jobs, + enabled: compile_config!(:background_jobs_enabled) + + config :domain, Domain.Auth.Adapters.Okta.Jobs, + enabled: compile_config!(:background_jobs_enabled) + + if external_url = compile_config!(:external_url) do + %{ scheme: external_url_scheme, host: external_url_host, port: external_url_port, path: external_url_path - ], - secret_key_base: compile_config!(:secret_key_base), - check_origin: [ - "#{external_url_scheme}://#{external_url_host}:#{external_url_port}", - "#{external_url_scheme}://*.#{external_url_host}:#{external_url_port}", - "#{external_url_scheme}://#{external_url_host}", - "#{external_url_scheme}://*.#{external_url_host}" - ], - live_view: [ - signing_salt: compile_config!(:live_view_signing_salt) - ] + } = URI.parse(external_url) - config :web, - external_trusted_proxies: compile_config!(:phoenix_external_trusted_proxies), - private_clients: compile_config!(:phoenix_private_clients) + ############################### + ##### Web ##################### + ############################### - config :web, - cookie_secure: compile_config!(:phoenix_secure_cookies), - cookie_signing_salt: compile_config!(:cookie_signing_salt), - cookie_encryption_salt: compile_config!(:cookie_encryption_salt) + config :web, Web.Endpoint, + http: [ + ip: compile_config!(:phoenix_listen_address).address, + port: compile_config!(:phoenix_http_web_port), + protocol_options: compile_config!(:phoenix_http_protocol_options) + ], + url: [ + scheme: external_url_scheme, + host: external_url_host, + port: external_url_port, + path: external_url_path + ], + secret_key_base: compile_config!(:secret_key_base), + check_origin: [ + "#{external_url_scheme}://#{external_url_host}:#{external_url_port}", + "#{external_url_scheme}://*.#{external_url_host}:#{external_url_port}", + "#{external_url_scheme}://#{external_url_host}", + "#{external_url_scheme}://*.#{external_url_host}" + ], + live_view: [ + signing_salt: compile_config!(:live_view_signing_salt) + ] - config :web, api_url_override: compile_config!(:api_url_override) + config :web, + external_trusted_proxies: compile_config!(:phoenix_external_trusted_proxies), + private_clients: compile_config!(:phoenix_private_clients) - ############################### - ##### API ##################### - ############################### + config :web, + cookie_secure: compile_config!(:phoenix_secure_cookies), + cookie_signing_salt: compile_config!(:cookie_signing_salt), + cookie_encryption_salt: compile_config!(:cookie_encryption_salt) - config :api, API.Endpoint, - http: [ - ip: compile_config!(:phoenix_listen_address).address, - port: compile_config!(:phoenix_http_api_port), - protocol_options: compile_config!(:phoenix_http_protocol_options) - ], - url: [ - scheme: external_url_scheme, - host: external_url_host, - port: external_url_port, - path: external_url_path - ], - secret_key_base: compile_config!(:secret_key_base) + config :web, api_url_override: compile_config!(:api_url_override) - config :api, - cookie_secure: compile_config!(:phoenix_secure_cookies), - cookie_signing_salt: compile_config!(:cookie_signing_salt), - cookie_encryption_salt: compile_config!(:cookie_encryption_salt) + ############################### + ##### API ##################### + ############################### - config :api, - external_trusted_proxies: compile_config!(:phoenix_external_trusted_proxies), - private_clients: compile_config!(:phoenix_private_clients) + config :api, API.Endpoint, + http: [ + ip: compile_config!(:phoenix_listen_address).address, + port: compile_config!(:phoenix_http_api_port), + protocol_options: compile_config!(:phoenix_http_protocol_options) + ], + url: [ + scheme: external_url_scheme, + host: external_url_host, + port: external_url_port, + path: external_url_path + ], + secret_key_base: compile_config!(:secret_key_base) + + config :api, + cookie_secure: compile_config!(:phoenix_secure_cookies), + cookie_signing_salt: compile_config!(:cookie_signing_salt), + cookie_encryption_salt: compile_config!(:cookie_encryption_salt) + + config :api, + external_trusted_proxies: compile_config!(:phoenix_external_trusted_proxies), + private_clients: compile_config!(:phoenix_private_clients) + end ############################### ##### Third-party configs ##### diff --git a/elixir/mix.exs b/elixir/mix.exs index 243ddbfe2..7b6f38a50 100644 --- a/elixir/mix.exs +++ b/elixir/mix.exs @@ -63,6 +63,15 @@ defmodule Firezone.MixProject do defp releases do [ + domain: [ + include_executables_for: [:unix], + validate_compile_env: true, + applications: [ + domain: :permanent, + opentelemetry_exporter: :permanent, + opentelemetry: :temporary + ] + ], web: [ include_executables_for: [:unix], validate_compile_env: true, diff --git a/elixir/mix.lock b/elixir/mix.lock index 1a4ec7fdd..9ff7b737f 100644 --- a/elixir/mix.lock +++ b/elixir/mix.lock @@ -1,6 +1,7 @@ %{ "acceptor_pool": {:hex, :acceptor_pool, "1.0.0", "43c20d2acae35f0c2bcd64f9d2bde267e459f0f3fd23dab26485bf518c281b21", [:rebar3], [], "hexpm", "0cbcd83fdc8b9ad2eee2067ef8b91a14858a5883cb7cd800e6fcd5803e158788"}, "argon2_elixir": {:hex, :argon2_elixir, "4.0.0", "7f6cd2e4a93a37f61d58a367d82f830ad9527082ff3c820b8197a8a736648941", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f9da27cf060c9ea61b1bd47837a28d7e48a8f6fa13a745e252556c14f9132c7f"}, + "bandit": {:hex, :bandit, "1.3.0", "6a4e8d7c9ea721edd02c389e2cc867890cd96f83116e71ddf1ccbdd80661550c", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "bda37d6c614d74778a5dc43b8bcdc3245cd30619eab0342f58042f968f2165da"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "bureaucrat": {:hex, :bureaucrat, "0.2.9", "d98e4d2b9bdbf22e4a45c2113ce8b38b5b63278506c6ff918e3b943a4355d85b", [:mix], [{:inflex, ">= 1.10.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "111c8dd84382a62e1026ae011d592ceee918553e5203fe8448d9ba6ccbdfff7d"}, "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, @@ -99,6 +100,7 @@ "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, "telemetry_registry": {:hex, :telemetry_registry, "0.3.1", "14a3319a7d9027bdbff7ebcacf1a438f5f5c903057b93aee484cca26f05bdcba", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6d0ca77b691cf854ed074b459a93b87f4c7f5512f8f7743c635ca83da81f939e"}, "tesla": {:hex, :tesla, "1.8.0", "d511a4f5c5e42538d97eef7c40ec4f3e44effdc5068206f42ed859e09e51d1fd", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "10501f360cd926a309501287470372af1a6e1cbed0f43949203a4c13300bc79f"}, + "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, "tls_certificate_check": {:hex, :tls_certificate_check, "1.21.0", "042ab2c0c860652bc5cf69c94e3a31f96676d14682e22ec7813bd173ceff1788", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "6cee6cffc35a390840d48d463541d50746a7b0e421acaadb833cfc7961e490e7"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "wallaby": {:hex, :wallaby, "0.30.6", "7dc4c1213f3b52c4152581d126632bc7e06892336d3a0f582853efeeabd45a71", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "50950c1d968549b54c20e16175c68c7fc0824138e2bb93feb11ef6add8eb23d4"}, diff --git a/terraform/environments/production/main.tf b/terraform/environments/production/main.tf index c3f932a35..cb35f8bb6 100644 --- a/terraform/environments/production/main.tf +++ b/terraform/environments/production/main.tf @@ -481,10 +481,6 @@ locals { value = var.stripe_default_price_id }, # Telemetry - { - name = "TELEMETRY_ENABLED" - value = "false" - }, { name = "INSTRUMENTATION_CLIENT_LOGS_ENABLED" value = true diff --git a/terraform/environments/production/versions.tf b/terraform/environments/production/versions.tf index 160f52bea..add556987 100644 --- a/terraform/environments/production/versions.tf +++ b/terraform/environments/production/versions.tf @@ -14,12 +14,12 @@ terraform { google = { source = "hashicorp/google" - version = "~> 5.2" + version = "~> 5.20" } google-beta = { source = "hashicorp/google-beta" - version = "~> 5.2" + version = "~> 5.20" } tls = { diff --git a/terraform/environments/staging/.terraform.lock.hcl b/terraform/environments/staging/.terraform.lock.hcl index 3f57f07bd..9cfaa5fad 100644 --- a/terraform/environments/staging/.terraform.lock.hcl +++ b/terraform/environments/staging/.terraform.lock.hcl @@ -24,65 +24,65 @@ provider "registry.terraform.io/cyrilgdn/postgresql" { } provider "registry.terraform.io/hashicorp/aws" { - version = "5.35.0" - constraints = ">= 3.29.0, >= 5.20.0" + version = "5.41.0" + constraints = ">= 3.29.0, >= 5.30.0" hashes = [ - "h1:MKNFmhsOIirK7Qzr6TWkVaBcVGN81lCU0BPiaPOeQ8s=", - "zh:3a2a6f40db82d30ea8c5e3e251ca5e16b08e520570336e7e342be823df67e945", - "zh:420a23b69b412438a15b8b2e2c9aac2cf2e4976f990f117e4bf8f630692d3949", - "zh:4d8b887f6a71b38cff77ad14af9279528433e279eed702d96b81ea48e16e779c", - "zh:4edd41f8e1c7d29931608a7b01a7ae3d89d6f95ef5502cf8200f228a27917c40", - "zh:6337544e2ded5cf37b55a70aa6ce81c07fd444a2644ff3c5aad1d34680051bdc", - "zh:668faa3faaf2e0758bf319ea40d2304340f4a2dc2cd24460ddfa6ab66f71b802", - "zh:79ddc6d7c90e59fdf4a51e6ea822ba9495b1873d6a9d70daf2eeaf6fc4eb6ff3", - "zh:885822027faf1aa57787f980ead7c26e7d0e55b4040d926b65709b764f804513", - "zh:8c50a8f397b871388ff2e048f5eb280af107faa2e8926694f1ffd9f32a7a7cdf", + "h1:DiX7N35G2NUQRyRGy90+gyePnhP4w77f8LrJUronotE=", + "zh:0553331a6287c146353b6daf6f71987d8c000f407b5e29d6e004ea88faec2e67", + "zh:1a11118984bb2950e8ee7ef17b0f91fc9eb4a42c8e7a9cafd7eb4aca771d06e4", + "zh:236fedd266d152a8233a7fe27ffdd99ca27d9e66a9618a988a4c3da1ac24a33f", + "zh:34bc482ea04cf30d4d216afa55eecf66854e1acf93892cb28a6b5af91d43c9b7", + "zh:39d7eb15832fe339bf46e3bab9852280762a1817bf1afc459eecd430e20e3ad5", + "zh:39fb07429c51556b05170ec2b6bd55e2487adfe1606761eaf1f2a43c4bb20e47", + "zh:71d7cd3013e2f3fa0f65194af29ee6f5fa905e0df2b72b723761dc953f4512ea", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:a2f5d2553df5573a060641f18ee7585587047c25ba73fd80617f59b5893d22b4", - "zh:c43833ae2a152213ee92eb5be7653f9493779eddbe0ce403ea49b5f1d87fd766", - "zh:dab01527a3a55b4f0f958af6f46313d775e27f9ad9d10bedbbfea4a35a06dc5f", - "zh:ed49c65620ec42718d681a7fc00c166c295ff2795db6cede2c690b83f9fb3e65", - "zh:f0a358c0ae1087c466d0fbcc3b4da886f33f881a145c3836ec43149878b86a1a", + "zh:9b271ae12394e7e2ce6da568b42226a146e90fd705e02a670fcb93618c4aa19f", + "zh:a884dd978859d001709681f9513ba0fbb0753d1d459a7f3434ecc5f1b8699c49", + "zh:b8c3c7dc10ae4f6143168042dcf8dee63527b103cc37abc238ea06150af38b6e", + "zh:ba94ffe0893ad60c0b70c402e163b4df2cf417e93474a9cc1a37535bba18f22d", + "zh:d5ba851d971ff8d796afd9a100acf55eaac0c197c6ab779787797ce66f419f0e", + "zh:e8c090d0c4f730c4a610dc4f0c22b177a0376d6f78679fc3f1d557b469e656f4", + "zh:ed7623acde26834672969dcb5befdb62900d9f216d32e7478a095d2b040a0ea7", ] } provider "registry.terraform.io/hashicorp/google" { - version = "5.18.0" + version = "5.20.0" constraints = "~> 5.2" hashes = [ - "h1:WicMa7ZYUyJlYmePh4jCaPfxAeM8U/8/8x632gBMMPY=", - "zh:1f9c144b0bd83c125d27b20c0d3970ba817bbd8eaea8778291c74b54f79e9de5", - "zh:4c34f6e875919623bcd6fdc67ee51c54aa469d5184b6a3899cfc97c6dfb83a81", - "zh:661443c63c3f976448eff875262e98a5564fef4bc11047b2d841b71034c7fb26", - "zh:6a1ace1d2a42d4c7fdc13f67cd582d11ffb27b62309d353fb24609032f992acd", - "zh:886bcc6cb6757aa38c6dc31988be1de3aa1880067a8e984aa2592c436f62781f", - "zh:92e4a74ca1bc5128054ba207f996b665f474c0b338e675dafd0d08615faf2693", - "zh:945a41443df9f3dbe16414a9612cbdf8f41f654307207c0d94822b94d2b5d26e", - "zh:9cd6395a435539c088f8d8713599cb05d944f6d348ef08b5804aee71dc956022", - "zh:c526621d5353adc12d3d277eb18738619542f400dc8d1d9938ebdaca1dc32390", - "zh:c896deccd58176e9530023fe58b673269a0e235affae0f344a1b49faf938806e", - "zh:f4dbfab40f58044669215387348565aeb94e6d6ef4916c0594ca5db98ad91e37", + "h1:zVFayCyqprY+NCbUCWe9RgpH3BGuAtOyLzqhA8f3VE8=", + "zh:10197fce4ddf2be32717fb3a5992b45ee1a134f8c66207ba7a2d89339bcad17e", + "zh:428c22bc9ae637adaa3c99e7ec2f5df3828c1625cebfc0ef680e520abdaff820", + "zh:4474139669a13997abd3d8282dc00905a08c695895b1b3d09e5a87753be54a24", + "zh:4bcd7a09577303d71146899f70413a7376531c940015eb6bfa047df55778d2ef", + "zh:5c5083c8deae3093a63d9a7ff18844985e35f2108fca49a812be85f3ec8e7409", + "zh:72ab7bf2a7a1ae98bf1ed790cc3babcd6399db58aa2c9bcef005f9b709b8ad98", + "zh:95cf50e8aa4fe5495b983aca1478cd7909fafc92c0e1942b764343076f367e3f", + "zh:9efd8613897beee98c11befc1f8ebfea14b5a0e88b0fdd2f737a1a2acd5e2a2f", + "zh:b91ddced2f7916338e1f7d0fa003a15bb5700d8d4f389d906f2944334d67bbc2", + "zh:db0270eb90eda9ce98668afc517365d5876c81bb12f0375d6837d204231f6df5", "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:f58e490c192698ff8655081d467e57ee13558aa47f950f1d249318bf5cc93e5e", ] } provider "registry.terraform.io/hashicorp/google-beta" { - version = "5.18.0" + version = "5.20.0" constraints = "~> 5.2" hashes = [ - "h1:jTtimo6WpBCeznYLOcvFM+pP0CT6rQ5ZV4FxeeP3+ZU=", - "zh:2340414b1aaa37b8625b7ebb49d7724923a1970e93d183345779a4344c4932aa", - "zh:36a65d4ca5baebe59c232971052b13e01a67e8d514e4360c86747b631e2068a4", - "zh:40c2e336ea593f69af062a3fffeca34a081f311bf38862ed0ec87257e54b3942", - "zh:418d78834ea0051e0e9b15a609b676c5079cd2cf279c80f9847b71634c280c40", - "zh:79c004b57c5034bc2a5a08c07848b59eecf75ef901524da5f5868ec865cc774d", - "zh:9608a333f6dfe7278fdbbbd6aeb2864fe7e539fa4a5ebca42304cc3b25374d72", - "zh:9bcae66eeaf5d532baa17d5e9c84c0997e8d4d558106c74c3e3d13852a94cfc8", - "zh:9c0a9b6faa2b5fc2fa9190f8222da9d12ea0d6ddcf37f55f9d86c0efc02f594d", - "zh:a21603acb95c8403bcd2b9b9f2d38d1aadbb8e329f0072d1e6c3abf10fa1944b", - "zh:a7ebe06b056571693948004f0015dba209a8304713cbb65507d3f6020e5c1ac4", - "zh:e009cdd91f19e847912ca522efd592d0937dbca297addce9672caa747bcd0a3f", + "h1:NV6nLDJYo9Y0d06ggDM05WJapYg0rFF52RmjxGkD+88=", + "zh:2792639ca660f373ce0c0d152f28d1d2e59b590c19d960eddea3c7b70be2e811", + "zh:5a29c775934d5fdf3960687222b0c1505741104ade9a94e42a11d6bef73c1656", + "zh:71fcdf323e7e5bf91d12450ad7f948eef3df935e1875764d5f3c6316b57faa1b", + "zh:89ca5d8cb4d17d7855b7ee7c347a3bb57d5c935c4cc6d18ac78098c9c1c6008a", + "zh:c2a1e2d093ade9a5a4d5170fa4d439c2542f7d01a114af2a5a92b071268d193e", + "zh:c4999b0bb00ce68bea7b203fdd6685c8133f7593d8ce7a9214cedac4844c22b0", + "zh:c912e8f6baa182780678b1caf44863e7d829942c0e9f4a3f04bd623e1bf5f430", + "zh:d553df714bc0d94b3bcdba37a3fca2eb099d7e494502a64a20b3d0f5213b979b", + "zh:d92266f87f3679b179a238b2a3df5309da6b9d86bdcb9311cce5f4b9537938fb", + "zh:e0daa80c3691c6b977c64d22a0c4f6e180d5d3dc888306f7057adb179baab761", "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:fee824aeb0f16837d3aaeadbabdf9e40d162344415acba8c521e43cf76d0efa8", ] } diff --git a/terraform/environments/staging/main.tf b/terraform/environments/staging/main.tf index fad90ba12..266c6eb60 100644 --- a/terraform/environments/staging/main.tf +++ b/terraform/environments/staging/main.tf @@ -433,10 +433,6 @@ locals { value = var.stripe_default_price_id }, # Telemetry - { - name = "TELEMETRY_ENABLED" - value = "false" - }, { name = "INSTRUMENTATION_CLIENT_LOGS_ENABLED" value = true @@ -485,6 +481,70 @@ locals { ] } +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 @@ -551,6 +611,10 @@ module "web" { name = "API_URL_OVERRIDE" value = "wss://api.${local.tld}" }, + { + name = "BACKGROUND_JOBS_ENABLED" + value = "false" + }, ], local.shared_application_environment_variables) application_labels = { @@ -621,6 +685,10 @@ module "api" { name = "PHOENIX_HTTP_API_PORT" value = "8080" }, + { + name = "BACKGROUND_JOBS_ENABLED" + value = "false" + }, ], local.shared_application_environment_variables) application_labels = { @@ -677,7 +745,7 @@ resource "google_compute_firewall" "erlang-distribution" { } source_ranges = [google_compute_subnetwork.apps.ip_cidr_range] - target_tags = concat(module.web.target_tags, module.api.target_tags) + target_tags = concat(module.web.target_tags, module.api.target_tags, module.domain.target_tags) } ## Allow service account to list running instances @@ -696,8 +764,9 @@ resource "google_project_iam_custom_role" "erlang-discovery" { resource "google_project_iam_member" "application" { for_each = { - api = module.api.service_account.email - web = module.web.service_account.email + 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 @@ -810,7 +879,7 @@ resource "google_compute_firewall" "ssh-ipv4" { } source_ranges = ["0.0.0.0/0"] - target_tags = concat(module.web.target_tags, module.api.target_tags) + target_tags = concat(module.web.target_tags, module.api.target_tags, module.domain.target_tags) } resource "google_compute_firewall" "ssh-ipv6" { @@ -835,7 +904,7 @@ resource "google_compute_firewall" "ssh-ipv6" { } source_ranges = ["::/0"] - target_tags = concat(module.web.target_tags, module.api.target_tags) + target_tags = concat(module.web.target_tags, module.api.target_tags, module.domain.target_tags) } resource "google_compute_firewall" "relays-ssh-ipv4" { @@ -900,6 +969,7 @@ module "ops" { 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 + api_host = module.api.host + web_host = module.web.host + domain_host = module.domain.host } diff --git a/terraform/environments/staging/versions.tf b/terraform/environments/staging/versions.tf index 160f52bea..add556987 100644 --- a/terraform/environments/staging/versions.tf +++ b/terraform/environments/staging/versions.tf @@ -14,12 +14,12 @@ terraform { google = { source = "hashicorp/google" - version = "~> 5.2" + version = "~> 5.20" } google-beta = { source = "hashicorp/google-beta" - version = "~> 5.2" + version = "~> 5.20" } tls = { diff --git a/terraform/modules/elixir-app/dns.tf b/terraform/modules/elixir-app/dns.tf new file mode 100644 index 000000000..1995a2df9 --- /dev/null +++ b/terraform/modules/elixir-app/dns.tf @@ -0,0 +1,38 @@ +# Create DNS records for the application +resource "google_dns_record_set" "application-ipv4" { + count = var.application_dns_tld != null ? 1 : 0 + + project = var.project_id + + name = "${var.application_dns_tld}." + type = "A" + ttl = 300 + + managed_zone = var.dns_managed_zone_name + + rrdatas = google_compute_global_address.ipv4[*].address + + depends_on = [ + google_project_service.compute, + google_project_service.servicenetworking, + ] +} + +resource "google_dns_record_set" "application-ipv6" { + count = var.application_dns_tld != null ? 1 : 0 + + project = var.project_id + + name = "${var.application_dns_tld}." + type = "AAAA" + ttl = 300 + + managed_zone = var.dns_managed_zone_name + + rrdatas = google_compute_global_address.ipv6[*].address + + depends_on = [ + google_project_service.compute, + google_project_service.servicenetworking, + ] +} diff --git a/terraform/modules/elixir-app/iam.tf b/terraform/modules/elixir-app/iam.tf new file mode 100644 index 000000000..8832fc38c --- /dev/null +++ b/terraform/modules/elixir-app/iam.tf @@ -0,0 +1,62 @@ +# Create IAM role for the application instances +resource "google_service_account" "application" { + project = var.project_id + + account_id = "app-${local.application_name}" + display_name = "${local.application_name} app" + description = "Service account for ${local.application_name} application instances." +} + +## Allow application service account to pull images from the container registry +resource "google_project_iam_member" "artifacts" { + project = var.project_id + + role = "roles/artifactregistry.reader" + + member = "serviceAccount:${google_service_account.application.email}" +} + +## Allow fluentbit to injest logs +resource "google_project_iam_member" "logs" { + project = var.project_id + + role = "roles/logging.logWriter" + + member = "serviceAccount:${google_service_account.application.email}" +} + +## Allow reporting application errors +resource "google_project_iam_member" "errors" { + project = var.project_id + + role = "roles/errorreporting.writer" + + member = "serviceAccount:${google_service_account.application.email}" +} + +## Allow reporting metrics +resource "google_project_iam_member" "metrics" { + project = var.project_id + + role = "roles/monitoring.metricWriter" + + member = "serviceAccount:${google_service_account.application.email}" +} + +## Allow reporting metrics +resource "google_project_iam_member" "service_management" { + project = var.project_id + + role = "roles/servicemanagement.reporter" + + member = "serviceAccount:${google_service_account.application.email}" +} + +## Allow appending traces +resource "google_project_iam_member" "cloudtrace" { + project = var.project_id + + role = "roles/cloudtrace.agent" + + member = "serviceAccount:${google_service_account.application.email}" +} diff --git a/terraform/modules/elixir-app/main.tf b/terraform/modules/elixir-app/main.tf index 54672c496..063b85712 100644 --- a/terraform/modules/elixir-app/main.tf +++ b/terraform/modules/elixir-app/main.tf @@ -16,12 +16,14 @@ locals { }, { name = "PHOENIX_EXTERNAL_TRUSTED_PROXIES" - value = jsonencode([ - "35.191.0.0/16", - "130.211.0.0/22", - google_compute_global_address.ipv4.address, - google_compute_global_address.ipv6.address - ]) + value = jsonencode(concat( + [ + "35.191.0.0/16", + "130.211.0.0/22" + ], + google_compute_global_address.ipv4[*].address, + google_compute_global_address.ipv6[*].address + )) }, { name = "LOG_LEVEL" @@ -49,16 +51,6 @@ locals { ], var.application_environment_variables) application_ports_by_name = { for port in var.application_ports : port.name => port } - - google_load_balancer_ip_ranges = [ - "130.211.0.0/22", - "35.191.0.0/16", - ] - - google_health_check_ip_ranges = [ - "130.211.0.0/22", - "35.191.0.0/16" - ] } # Fetch most recent COS image @@ -67,69 +59,6 @@ data "google_compute_image" "coreos" { project = "cos-cloud" } -# Create IAM role for the application instances -resource "google_service_account" "application" { - project = var.project_id - - account_id = "app-${local.application_name}" - display_name = "${local.application_name} app" - description = "Service account for ${local.application_name} application instances." -} - -## Allow application service account to pull images from the container registry -resource "google_project_iam_member" "artifacts" { - project = var.project_id - - role = "roles/artifactregistry.reader" - - member = "serviceAccount:${google_service_account.application.email}" -} - -## Allow fluentbit to injest logs -resource "google_project_iam_member" "logs" { - project = var.project_id - - role = "roles/logging.logWriter" - - member = "serviceAccount:${google_service_account.application.email}" -} - -## Allow reporting application errors -resource "google_project_iam_member" "errors" { - project = var.project_id - - role = "roles/errorreporting.writer" - - member = "serviceAccount:${google_service_account.application.email}" -} - -## Allow reporting metrics -resource "google_project_iam_member" "metrics" { - project = var.project_id - - role = "roles/monitoring.metricWriter" - - member = "serviceAccount:${google_service_account.application.email}" -} - -## Allow reporting metrics -resource "google_project_iam_member" "service_management" { - project = var.project_id - - role = "roles/servicemanagement.reporter" - - member = "serviceAccount:${google_service_account.application.email}" -} - -## Allow appending traces -resource "google_project_iam_member" "cloudtrace" { - project = var.project_id - - role = "roles/cloudtrace.agent" - - member = "serviceAccount:${google_service_account.application.email}" -} - # Deploy app resource "google_compute_instance_template" "application" { project = var.project_id @@ -358,408 +287,3 @@ resource "google_compute_region_instance_group_manager" "application" { google_compute_instance_template.application ] } - -# Define a security policy which allows to filter traffic by IP address, -# an edge security policy can also detect and block common types of web attacks -resource "google_compute_security_policy" "default" { - project = var.project_id - - name = local.application_name - - type = "CLOUD_ARMOR" - - rule { - action = "allow" - priority = "2147483647" - - match { - versioned_expr = "SRC_IPS_V1" - - config { - src_ip_ranges = ["*"] - } - } - - description = "default allow rule" - } - - # TODO: Configure more WAF rules - - depends_on = [ - google_project_service.compute, - google_project_service.pubsub, - google_project_service.bigquery, - google_project_service.container, - google_project_service.stackdriver, - google_project_service.logging, - google_project_service.monitoring, - google_project_service.cloudprofiler, - google_project_service.cloudtrace, - google_project_service.servicenetworking, - ] -} - -# Expose the application ports via HTTP(S) load balancer with a managed SSL certificate and a static IP address -resource "google_compute_backend_service" "default" { - for_each = local.application_ports_by_name - - project = var.project_id - - name = "${local.application_name}-backend-${each.value.name}" - - load_balancing_scheme = "EXTERNAL" - - port_name = each.value.name - protocol = "HTTP" - - timeout_sec = 86400 - connection_draining_timeout_sec = 120 - - enable_cdn = false - compression_mode = "DISABLED" - - custom_request_headers = [ - "X-Geo-Location-Region:{client_region}", - "X-Geo-Location-City:{client_city}", - "X-Geo-Location-Coordinates:{client_city_lat_long}", - ] - - custom_response_headers = [ - "X-Cache-Hit: {cdn_cache_status}" - ] - - session_affinity = "CLIENT_IP" - - health_checks = try([google_compute_health_check.port[each.key].self_link], null) - - security_policy = google_compute_security_policy.default.self_link - - backend { - balancing_mode = "UTILIZATION" - capacity_scaler = 1 - group = google_compute_region_instance_group_manager.application.instance_group - - # Do not send traffic to nodes that have CPU load higher than 80% - # max_utilization = 0.8 - } - - log_config { - enable = false - sample_rate = "1.0" - } - - depends_on = [ - google_compute_region_instance_group_manager.application, - google_compute_health_check.port, - ] -} - -## Create a SSL policy -resource "google_compute_ssl_policy" "application" { - project = var.project_id - - name = local.application_name - - min_tls_version = "TLS_1_2" - profile = "MODERN" - - depends_on = [ - google_project_service.compute, - google_project_service.pubsub, - google_project_service.bigquery, - google_project_service.container, - google_project_service.stackdriver, - google_project_service.logging, - google_project_service.monitoring, - google_project_service.cloudprofiler, - google_project_service.cloudtrace, - google_project_service.servicenetworking, - ] -} - -## Create a managed SSL certificate -resource "google_compute_managed_ssl_certificate" "default" { - project = var.project_id - - name = "${local.application_name}-mig-lb-cert" - - type = "MANAGED" - - managed { - domains = [ - var.application_dns_tld, - ] - } - - depends_on = [ - google_project_service.compute, - google_project_service.servicenetworking, - ] -} - -## Create URL map for the application -resource "google_compute_url_map" "default" { - project = var.project_id - - name = local.application_name - default_service = google_compute_backend_service.default["http"].self_link - - depends_on = [ - google_project_service.compute, - google_project_service.servicenetworking, - ] -} - -# Set up HTTP(s) proxies and redirect HTTP to HTTPS -resource "google_compute_url_map" "https_redirect" { - project = var.project_id - - name = "${local.application_name}-https-redirect" - - default_url_redirect { - https_redirect = true - redirect_response_code = "MOVED_PERMANENTLY_DEFAULT" - strip_query = false - } - - depends_on = [ - google_project_service.compute, - google_project_service.servicenetworking, - ] -} - -resource "google_compute_target_http_proxy" "default" { - project = var.project_id - - name = "${local.application_name}-http" - - url_map = google_compute_url_map.https_redirect.self_link -} - -resource "google_compute_target_https_proxy" "default" { - project = var.project_id - - name = "${local.application_name}-https" - - url_map = google_compute_url_map.default.self_link - - ssl_certificates = [google_compute_managed_ssl_certificate.default.self_link] - ssl_policy = google_compute_ssl_policy.application.self_link - quic_override = "NONE" -} - -# Allocate global addresses for the load balancer and set up forwarding rules -## IPv4 -resource "google_compute_global_address" "ipv4" { - project = var.project_id - - name = "${local.application_name}-ipv4" - - ip_version = "IPV4" - - depends_on = [ - google_project_service.compute, - google_project_service.servicenetworking, - ] -} - -resource "google_compute_global_forwarding_rule" "http" { - project = var.project_id - - name = local.application_name - labels = local.application_labels - - target = google_compute_target_http_proxy.default.self_link - ip_address = google_compute_global_address.ipv4.address - port_range = "80" - - load_balancing_scheme = "EXTERNAL" -} - -resource "google_compute_global_forwarding_rule" "https" { - project = var.project_id - - name = "${local.application_name}-https" - labels = local.application_labels - - target = google_compute_target_https_proxy.default.self_link - ip_address = google_compute_global_address.ipv4.address - port_range = "443" - - load_balancing_scheme = "EXTERNAL" -} - -## IPv6 -resource "google_compute_global_address" "ipv6" { - project = var.project_id - - name = "${local.application_name}-ipv6" - - ip_version = "IPV6" - - depends_on = [ - google_project_service.compute, - google_project_service.servicenetworking, - ] -} - -resource "google_compute_global_forwarding_rule" "http_ipv6" { - project = var.project_id - - name = "${local.application_name}-ipv6-http" - labels = local.application_labels - - target = google_compute_target_http_proxy.default.self_link - ip_address = google_compute_global_address.ipv6.address - port_range = "80" - - load_balancing_scheme = "EXTERNAL" -} - -resource "google_compute_global_forwarding_rule" "https_ipv6" { - project = var.project_id - - name = "${local.application_name}-ipv6-https" - labels = local.application_labels - - target = google_compute_target_https_proxy.default.self_link - ip_address = google_compute_global_address.ipv6.address - port_range = "443" - - load_balancing_scheme = "EXTERNAL" -} - -## Open HTTP(S) ports for the load balancer -resource "google_compute_firewall" "http" { - project = var.project_id - - name = "${local.application_name}-firewall-lb-to-instances-ipv4" - network = var.vpc_network - - source_ranges = local.google_load_balancer_ip_ranges - target_tags = ["app-${local.application_name}"] - - dynamic "allow" { - for_each = var.application_ports - - content { - protocol = allow.value.protocol - ports = [allow.value.port] - } - } - - # We also enable UDP to allow QUIC if it's enabled - dynamic "allow" { - for_each = var.application_ports - - content { - protocol = "udp" - ports = [allow.value.port] - } - } -} - -## Open HTTP(S) ports for the health checks -resource "google_compute_firewall" "http-health-checks" { - project = var.project_id - - name = "${local.application_name}-healthcheck" - network = var.vpc_network - - source_ranges = local.google_health_check_ip_ranges - target_tags = ["app-${local.application_name}"] - - dynamic "allow" { - for_each = var.application_ports - - content { - protocol = allow.value.protocol - ports = [allow.value.port] - } - } - - depends_on = [ - google_project_service.compute, - google_project_service.servicenetworking, - ] -} - -# Allow outbound traffic -resource "google_compute_firewall" "egress-ipv4" { - project = var.project_id - - name = "${local.application_name}-egress-ipv4" - network = var.vpc_network - direction = "EGRESS" - - target_tags = ["app-${local.application_name}"] - destination_ranges = ["0.0.0.0/0"] - - allow { - protocol = "all" - } - - depends_on = [ - google_project_service.compute, - google_project_service.servicenetworking, - ] -} - -resource "google_compute_firewall" "egress-ipv6" { - project = var.project_id - - name = "${local.application_name}-egress-ipv6" - network = var.vpc_network - direction = "EGRESS" - - target_tags = ["app-${local.application_name}"] - destination_ranges = ["::/0"] - - allow { - protocol = "all" - } - - depends_on = [ - google_project_service.compute, - google_project_service.servicenetworking, - ] -} - -# Create DNS records for the application -resource "google_dns_record_set" "application-ipv4" { - project = var.project_id - - name = "${var.application_dns_tld}." - type = "A" - ttl = 300 - - managed_zone = var.dns_managed_zone_name - - rrdatas = [ - google_compute_global_address.ipv4.address - ] - - depends_on = [ - google_project_service.compute, - google_project_service.servicenetworking, - ] -} - -resource "google_dns_record_set" "application-ipv6" { - project = var.project_id - - name = "${var.application_dns_tld}." - type = "AAAA" - ttl = 300 - - managed_zone = var.dns_managed_zone_name - - rrdatas = [ - google_compute_global_address.ipv6.address - ] - - depends_on = [ - google_project_service.compute, - google_project_service.servicenetworking, - ] -} diff --git a/terraform/modules/elixir-app/network.tf b/terraform/modules/elixir-app/network.tf new file mode 100644 index 000000000..7e726edc0 --- /dev/null +++ b/terraform/modules/elixir-app/network.tf @@ -0,0 +1,411 @@ +locals { + google_load_balancer_ip_ranges = [ + "130.211.0.0/22", + "35.191.0.0/16", + ] + + google_health_check_ip_ranges = [ + "130.211.0.0/22", + "35.191.0.0/16" + ] + + public_application = var.application_dns_tld != null +} + +# Define a security policy which allows to filter traffic by IP address, +# an edge security policy can also detect and block common types of web attacks +resource "google_compute_security_policy" "default" { + count = local.public_application ? 1 : 0 + + project = var.project_id + + name = local.application_name + + type = "CLOUD_ARMOR" + + rule { + action = "allow" + priority = "2147483647" + + match { + versioned_expr = "SRC_IPS_V1" + + config { + src_ip_ranges = ["*"] + } + } + + description = "default allow rule" + } + + # TODO: Configure more WAF rules + + depends_on = [ + google_project_service.compute, + google_project_service.pubsub, + google_project_service.bigquery, + google_project_service.container, + google_project_service.stackdriver, + google_project_service.logging, + google_project_service.monitoring, + google_project_service.cloudprofiler, + google_project_service.cloudtrace, + google_project_service.servicenetworking, + ] +} + +# Expose the application ports via HTTP(S) load balancer with a managed SSL certificate and a static IP address +resource "google_compute_backend_service" "default" { + for_each = local.public_application ? local.application_ports_by_name : {} + + project = var.project_id + + name = "${local.application_name}-backend-${each.value.name}" + + load_balancing_scheme = "EXTERNAL" + + port_name = each.value.name + protocol = "HTTP" + + timeout_sec = 86400 + connection_draining_timeout_sec = 120 + + enable_cdn = false + compression_mode = "DISABLED" + + custom_request_headers = [ + "X-Geo-Location-Region:{client_region}", + "X-Geo-Location-City:{client_city}", + "X-Geo-Location-Coordinates:{client_city_lat_long}", + ] + + custom_response_headers = [ + "X-Cache-Hit: {cdn_cache_status}" + ] + + session_affinity = "CLIENT_IP" + + health_checks = try([google_compute_health_check.port[each.key].self_link], null) + + security_policy = google_compute_security_policy.default[0].self_link + + backend { + balancing_mode = "UTILIZATION" + capacity_scaler = 1 + group = google_compute_region_instance_group_manager.application.instance_group + + # Do not send traffic to nodes that have CPU load higher than 80% + # max_utilization = 0.8 + } + + log_config { + enable = false + sample_rate = "1.0" + } + + depends_on = [ + google_compute_region_instance_group_manager.application, + google_compute_health_check.port, + ] +} + +## Create a SSL policy +resource "google_compute_ssl_policy" "application" { + count = local.public_application ? 1 : 0 + + project = var.project_id + + name = local.application_name + + min_tls_version = "TLS_1_2" + profile = "MODERN" + + depends_on = [ + google_project_service.compute, + google_project_service.pubsub, + google_project_service.bigquery, + google_project_service.container, + google_project_service.stackdriver, + google_project_service.logging, + google_project_service.monitoring, + google_project_service.cloudprofiler, + google_project_service.cloudtrace, + google_project_service.servicenetworking, + ] +} + +## Create a managed SSL certificate +resource "google_compute_managed_ssl_certificate" "default" { + count = local.public_application ? 1 : 0 + + project = var.project_id + + name = "${local.application_name}-mig-lb-cert" + + type = "MANAGED" + + managed { + domains = [ + var.application_dns_tld, + ] + } + + depends_on = [ + google_project_service.compute, + google_project_service.servicenetworking, + ] +} + +## Create URL map for the application +resource "google_compute_url_map" "default" { + count = try(google_compute_backend_service.default["http"], null) != null ? 1 : 0 + + project = var.project_id + + name = local.application_name + default_service = google_compute_backend_service.default["http"].self_link + + depends_on = [ + google_project_service.compute, + google_project_service.servicenetworking, + ] +} + +# Set up HTTP(s) proxies and redirect HTTP to HTTPS +resource "google_compute_url_map" "https_redirect" { + count = try(google_compute_backend_service.default["http"], null) != null ? 1 : 0 + + project = var.project_id + + name = "${local.application_name}-https-redirect" + + default_url_redirect { + https_redirect = true + redirect_response_code = "MOVED_PERMANENTLY_DEFAULT" + strip_query = false + } + + depends_on = [ + google_project_service.compute, + google_project_service.servicenetworking, + ] +} + +resource "google_compute_target_http_proxy" "default" { + count = length(google_compute_url_map.https_redirect) > 0 ? 1 : 0 + + project = var.project_id + + name = "${local.application_name}-http" + + url_map = google_compute_url_map.https_redirect[0].self_link +} + +resource "google_compute_target_https_proxy" "default" { + count = local.public_application ? 1 : 0 + + project = var.project_id + + name = "${local.application_name}-https" + + url_map = google_compute_url_map.default[0].self_link + + ssl_certificates = google_compute_managed_ssl_certificate.default[*].self_link + ssl_policy = google_compute_ssl_policy.application[0].self_link + quic_override = "NONE" +} + +# Allocate global addresses for the load balancer and set up forwarding rules +## IPv4 +resource "google_compute_global_address" "ipv4" { + count = local.public_application ? 1 : 0 + + project = var.project_id + + name = "${local.application_name}-ipv4" + + ip_version = "IPV4" + + depends_on = [ + google_project_service.compute, + google_project_service.servicenetworking, + ] +} + +resource "google_compute_global_forwarding_rule" "http" { + count = local.public_application ? 1 : 0 + + project = var.project_id + + name = local.application_name + labels = local.application_labels + + target = google_compute_target_http_proxy.default[0].self_link + ip_address = google_compute_global_address.ipv4[0].address + port_range = "80" + + load_balancing_scheme = "EXTERNAL" +} + +resource "google_compute_global_forwarding_rule" "https" { + count = local.public_application ? 1 : 0 + + project = var.project_id + + name = "${local.application_name}-https" + labels = local.application_labels + + target = google_compute_target_https_proxy.default[0].self_link + ip_address = google_compute_global_address.ipv4[0].address + port_range = "443" + + load_balancing_scheme = "EXTERNAL" +} + +## IPv6 +resource "google_compute_global_address" "ipv6" { + count = local.public_application ? 1 : 0 + + project = var.project_id + + name = "${local.application_name}-ipv6" + + ip_version = "IPV6" + + depends_on = [ + google_project_service.compute, + google_project_service.servicenetworking, + ] +} + +resource "google_compute_global_forwarding_rule" "http_ipv6" { + count = local.public_application ? 1 : 0 + + project = var.project_id + + name = "${local.application_name}-ipv6-http" + labels = local.application_labels + + target = google_compute_target_http_proxy.default[0].self_link + ip_address = google_compute_global_address.ipv6[0].address + port_range = "80" + + load_balancing_scheme = "EXTERNAL" +} + +resource "google_compute_global_forwarding_rule" "https_ipv6" { + count = local.public_application ? 1 : 0 + + project = var.project_id + + name = "${local.application_name}-ipv6-https" + labels = local.application_labels + + target = google_compute_target_https_proxy.default[0].self_link + ip_address = google_compute_global_address.ipv6[0].address + port_range = "443" + + load_balancing_scheme = "EXTERNAL" +} + +## Open HTTP ports for the load balancer +resource "google_compute_firewall" "http" { + count = local.public_application ? 1 : 0 + + project = var.project_id + + name = "${local.application_name}-firewall-lb-to-instances-ipv4" + network = var.vpc_network + + source_ranges = local.google_load_balancer_ip_ranges + target_tags = ["app-${local.application_name}"] + + dynamic "allow" { + for_each = var.application_ports + + content { + protocol = allow.value.protocol + ports = [allow.value.port] + } + } + + # We also enable UDP to allow QUIC if it's enabled + dynamic "allow" { + for_each = var.application_ports + + content { + protocol = "udp" + ports = [allow.value.port] + } + } +} + +## Open HTTP ports for the health checks +resource "google_compute_firewall" "http-health-checks" { + project = var.project_id + + name = "${local.application_name}-healthcheck" + network = var.vpc_network + + source_ranges = local.google_health_check_ip_ranges + target_tags = ["app-${local.application_name}"] + + dynamic "allow" { + for_each = var.application_ports + + content { + protocol = allow.value.protocol + ports = [allow.value.port] + } + } + + depends_on = [ + google_project_service.compute, + google_project_service.servicenetworking, + ] +} + +# Allow outbound traffic +resource "google_compute_firewall" "egress-ipv4" { + count = local.public_application ? 1 : 0 + + project = var.project_id + + name = "${local.application_name}-egress-ipv4" + network = var.vpc_network + direction = "EGRESS" + + target_tags = ["app-${local.application_name}"] + destination_ranges = ["0.0.0.0/0"] + + allow { + protocol = "all" + } + + depends_on = [ + google_project_service.compute, + google_project_service.servicenetworking, + ] +} + +resource "google_compute_firewall" "egress-ipv6" { + count = local.public_application ? 1 : 0 + + project = var.project_id + + name = "${local.application_name}-egress-ipv6" + network = var.vpc_network + direction = "EGRESS" + + target_tags = ["app-${local.application_name}"] + destination_ranges = ["::/0"] + + allow { + protocol = "all" + } + + depends_on = [ + google_project_service.compute, + google_project_service.servicenetworking, + ] +} diff --git a/terraform/modules/elixir-app/variables.tf b/terraform/modules/elixir-app/variables.tf index 141a29028..2b7d94a46 100644 --- a/terraform/modules/elixir-app/variables.tf +++ b/terraform/modules/elixir-app/variables.tf @@ -222,7 +222,8 @@ variable "application_token_scopes" { variable "application_dns_tld" { type = string - nullable = false + nullable = true + default = null description = "DNS host which will be used to create DNS records for the application and provision SSL-certificates." } diff --git a/terraform/modules/google-cloud-ops/main.tf b/terraform/modules/google-cloud-ops/main.tf index ea9e1aa6f..15609902b 100644 --- a/terraform/modules/google-cloud-ops/main.tf +++ b/terraform/modules/google-cloud-ops/main.tf @@ -33,6 +33,47 @@ 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 diff --git a/terraform/modules/google-cloud-ops/variables.tf b/terraform/modules/google-cloud-ops/variables.tf index 4a105ed6c..775de58c0 100644 --- a/terraform/modules/google-cloud-ops/variables.tf +++ b/terraform/modules/google-cloud-ops/variables.tf @@ -25,3 +25,7 @@ variable "api_host" { variable "web_host" { type = string } + +variable "domain_host" { + type = string +}