Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Dockerfile variant for clustered deployment on Fly.io #2286

Merged
merged 8 commits into from
Oct 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 4 additions & 0 deletions lib/livebook.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down
1 change: 1 addition & 0 deletions lib/livebook/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
15 changes: 15 additions & 0 deletions lib/livebook/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down
32 changes: 29 additions & 3 deletions lib/livebook/hubs/dockerfile.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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(),
Expand All @@ -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

Expand Down Expand Up @@ -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\
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we move the static variables out of the script?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason? If we could move them all that would make sense, but since LIVEBOOK_NODE is runtime, I would keep together for consistency.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea was to keep only runtime ones in the runtime script. Also, any thoughts in an approach like this?

ENV ERL_AFLAGS "-proto_dist inet6_tcp"
ENV LIVEBOOK_DISTRIBUTION name
CMD [ 'sh', '-c' , 'LIVEBOOK_NODE="${FLY_APP_NAME}-${FLY_IMAGE_REF##*-}@${FLY_PRIVATE_IP}" LIVEBOOK_CLUSTER="dns:${FLY_APP_NAME}.internal" /app/bin/livebook start']

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea was to keep only runtime ones in the runtime script.

Yeah, the practical difference is whether the env vars are set globally, which I don't think they necessarily should be? Also, normally all of these would go to env.sh.eex, that's why I prefer to keep them together.

I considered CMD, which we could even break it into multiple lines, but what I like about creating a startup file is that usually you'd have that file alongside and COPY it, so it's the closest to that (and if the user wants to do that, it's easy). Also for CMD you actually must use double quotes, because because technically it is parsed as a JSON array 🙈

# 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" ]

vs

# Custom startup script to cluster multiple Livebook nodes on Fly.io
CMD [ "sh", "-c" , "\
  ERL_AFLAGS=\"-proto_dist inet6_tcp\" \
  LIVEBOOK_DISTRIBUTION=\"name\" \
  LIVEBOOK_NODE=\"${FLY_APP_NAME}-${FLY_IMAGE_REF##*-}@${FLY_PRIVATE_IP}\" \
  LIVEBOOK_CLUSTER=\"dns:${FLY_APP_NAME}.internal\" \
  /app/bin/livebook start" ]

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")
Expand Down
30 changes: 29 additions & 1 deletion lib/livebook_web/live/app_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,41 @@ defmodule LivebookWeb.AppHelpers do
]}
/>
<.radio_field label="Base image" field={@form[:docker_tag]} options={docker_tag_options()} />
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
<div class="grid grid-cols-1 md:grid-cols-2">
<.select_field
label="Clustering"
help={
~S'''
When running multiple
instances of Livebook,
they need to be connected
into a single cluster.
You must either deploy
it as a single instance
or choose a platform to
enable clustering on.
'''
}
field={@form[:clustering]}
options={[
{"Single instance", ""},
{"Fly.io", "fly_io"}
]}
/>
</div>
<%= if Hubs.Provider.type(@hub) == "team" do %>
<div class="flex flex-col">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<.select_field
label="Zero Trust Authentication provider"
field={@form[:zta_provider]}
help="Enable this option if you want to deploy your notebooks behind an authentication proxy"
help={
~S'''
Enable this option if you want
to deploy your notebooks behind
an authentication proxy
'''
}
prompt="None"
options={zta_options()}
/>
Expand Down
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ defmodule Livebook.MixProject do
{:aws_signature, "~> 0.3.0"},
{:mint_web_socket, "~> 1.0.0"},
{:protobuf, "~> 0.8.0"},
{:dns_cluster, "~> 0.1.1"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:floki, ">= 0.27.0", only: :test},
{:bypass, "~> 2.1", only: :test},
Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"dns_cluster": {:hex, :dns_cluster, "0.1.1", "73b4b2c3ec692f8a64276c43f8c929733a9ab9ac48c34e4c0b3d9d1b5cd69155", [:mix], [], "hexpm", "03a3f6ff16dcbb53e219b99c7af6aab29eb6b88acf80164b4bd76ac18dc890b3"},
"earmark_parser": {:hex, :earmark_parser, "1.4.37", "2ad73550e27c8946648b06905a57e4d454e4d7229c2dafa72a0348c99d8be5f7", [:mix], [], "hexpm", "6b19783f2802f039806f375610faa22da130b8edc21209d0bff47918bb48360e"},
"ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},
"ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"},
Expand Down
10 changes: 10 additions & 0 deletions test/livebook/hubs/dockerfile_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,16 @@ defmodule Livebook.Hubs.DockerfileTest do
COPY files/data.csv files/image.jpeg /apps/files/
"""
end

test "deploying with fly.io cluster setup" do
config = dockerfile_config(%{clustering: :fly_io})
hub = personal_hub()
file = Livebook.FileSystem.File.local(p("/notebook.livemd"))

dockerfile = Dockerfile.build_dockerfile(config, hub, [], [], file, [], %{})

assert dockerfile =~ ~s/export LIVEBOOK_CLUSTER="dns:${FLY_APP_NAME}.internal"/
end
end

describe "warnings/6" do
Expand Down
Loading