diff --git a/README.md b/README.md index 5785c2d84..9215cdc01 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,11 @@ The following environment variables can be used to configure Livebook on boot: Those certificates are used during for server authentication when Livebook accesses files from external sources. + * LIVEBOOK_CLUSTER - configures clustering strategy when running multiple + instances of Livebook. Currently the only supported value is `dns:QUERY`, + in which case nodes ask DNS for A/AAAA records using the given query and + try to connect to peer nodes on the discovered IPs. + * LIVEBOOK_COOKIE - sets the cookie for running Livebook in a cluster. Defaults to a random string that is generated on boot. diff --git a/lib/livebook.ex b/lib/livebook.ex index 0c6f9eff8..671d62925 100644 --- a/lib/livebook.ex +++ b/lib/livebook.ex @@ -212,6 +212,10 @@ defmodule Livebook do :identity_provider, Livebook.Config.identity_provider!("LIVEBOOK_IDENTITY_PROVIDER") || {LivebookWeb.SessionIdentity, :unused} + + if dns_cluster_query = Livebook.Config.dns_cluster_query!("LIVEBOOK_CLUSTER") do + config :livebook, :dns_cluster_query, dns_cluster_query + end end @doc """ diff --git a/lib/livebook/application.ex b/lib/livebook/application.ex index 279129455..7737d441d 100644 --- a/lib/livebook/application.ex +++ b/lib/livebook/application.ex @@ -49,6 +49,7 @@ defmodule Livebook.Application do iframe_server_specs() ++ identity_provider() ++ [ + {DNSCluster, query: Application.get_env(:livebook, :dns_cluster_query) || :ignore}, # Start the Endpoint (http/https) # We skip the access url as we do our own logging below {LivebookWeb.Endpoint, log_access_url: false} diff --git a/lib/livebook/config.ex b/lib/livebook/config.ex index e1630920c..f3dc1dc14 100644 --- a/lib/livebook/config.ex +++ b/lib/livebook/config.ex @@ -628,6 +628,21 @@ defmodule Livebook.Config do end end + @doc """ + Parses and validates DNS cluster query from env. + """ + def dns_cluster_query!(env) do + if cluster_config = System.get_env(env) do + case cluster_config do + "dns:" <> query -> + query + + other -> + abort!(~s{expected #{env} to be "dns:query", got: #{inspect(other)}}) + end + end + end + @app_version Mix.Project.config()[:version] @doc """ diff --git a/lib/livebook/hubs/dockerfile.ex b/lib/livebook/hubs/dockerfile.ex index 49b2cc993..d99fdd5dc 100644 --- a/lib/livebook/hubs/dockerfile.ex +++ b/lib/livebook/hubs/dockerfile.ex @@ -8,6 +8,7 @@ defmodule Livebook.Hubs.Dockerfile do @type config :: %{ deploy_all: boolean(), docker_tag: String.t(), + clustering: nil | :fly_io, zta_provider: atom() | nil, zta_key: String.t() | nil } @@ -19,7 +20,13 @@ defmodule Livebook.Hubs.Dockerfile do def config_changeset(attrs \\ %{}) do default_image = Livebook.Config.docker_images() |> hd() - data = %{deploy_all: false, docker_tag: default_image.tag, zta_provider: nil, zta_key: nil} + data = %{ + deploy_all: false, + docker_tag: default_image.tag, + clustering: nil, + zta_provider: nil, + zta_key: nil + } zta_types = for provider <- Livebook.Config.identity_providers(), @@ -29,11 +36,12 @@ defmodule Livebook.Hubs.Dockerfile do types = %{ deploy_all: :boolean, docker_tag: :string, + clustering: Ecto.ParameterizedType.init(Ecto.Enum, values: [:fly_io]), zta_provider: Ecto.ParameterizedType.init(Ecto.Enum, values: zta_types), zta_key: :string } - cast({data, types}, attrs, [:deploy_all, :docker_tag, :zta_provider, :zta_key]) + cast({data, types}, attrs, [:deploy_all, :docker_tag, :clustering, :zta_provider, :zta_key]) |> validate_required([:deploy_all, :docker_tag]) end @@ -108,13 +116,31 @@ defmodule Livebook.Hubs.Dockerfile do RUN /app/bin/warmup_apps.sh """ + startup = + if config.clustering == :fly_io do + ~S""" + # Custom startup script to cluster multiple Livebook nodes on Fly.io + RUN printf '\ + #!/bin/bash\n\ + export ERL_AFLAGS="-proto_dist inet6_tcp"\n\ + export LIVEBOOK_DISTRIBUTION="name"\n\ + export LIVEBOOK_NODE="${FLY_APP_NAME}-${FLY_IMAGE_REF##*-}@${FLY_PRIVATE_IP}"\n\ + export LIVEBOOK_CLUSTER="dns:${FLY_APP_NAME}.internal"\n\ + /app/bin/livebook start\n\ + ' > /app/bin/start.sh && chmod +x /app/bin/start.sh + + CMD [ "/app/bin/start.sh" ] + """ + end + [ image, image_envs, hub_config, apps_config, notebook, - apps_warmup + apps_warmup, + startup ] |> Enum.reject(&is_nil/1) |> Enum.join("\n") diff --git a/lib/livebook_web/live/app_helpers.ex b/lib/livebook_web/live/app_helpers.ex index e9c94dc17..4efe4ad2b 100644 --- a/lib/livebook_web/live/app_helpers.ex +++ b/lib/livebook_web/live/app_helpers.ex @@ -102,13 +102,41 @@ defmodule LivebookWeb.AppHelpers do ]} /> <.radio_field label="Base image" field={@form[:docker_tag]} options={docker_tag_options()} /> +