Configuring multiple endpoints in Phoenix Framework

Configuring multiple endpoints in Phoenix Framework

- 8 mins

In this post I want to show how to add another endpoint in Phoenix Framework, that is going to listen on a different port. I found out that this topic is not well described in the internet. In our case, this endpoint will be serving a very simple API returning status of the app. The following instructions were tested with Phoenix version 1.3.2, but the same approach will work with version 1.2, the only difference is small change in the module names and slight changes to directory structure, check 1.2 to 1.3 migration instructions for more details

A separate endpoint gives extra security

The first question that comes to mind is “why?”. Why do we need to have a service on a separate port or even on a separate network interface? The biggest advantage and the most important one is security. By port/interface separation we can easily protect our service from unwanted traffic. For example: Our load balancer is connected to port 80 where we expose public API however, in our app and we want to expose private diagnostic API on port 8080 as well - by doing it public traffic can only hit our public API(80). We get pretty good protection of diagnostic API without authentication and authorization - of course we may still want to do that - to restrict access, even to traffic from our private network. Thanks to the separation some of it can be achieved on the firewall/routing level. Apart from that there are many examples, why it makes sense: admin interface, node stats page, health check and more.

Configuration first

Lets see how to do it on very simple phoenix app, first we need to create a new project - we can skip ecto as there will be no use for it in our example:

mix phx.new mendpoint --no-ecto

After starting the application in the dev mode, there will be a listener on port 4000 - in other words we are able to access our website via localhost:4000. Our endpoint configuration sits in: confing/dev.ex and looks like this:

config :mendpoint, MendpointWeb.Endpoint,
  http: [port: 4000],
  debug_errors: true,
  code_reloader: true,
  check_origin: false

We can see that the port is configured in the http section - these are params passed to Plug.Adapters.Cowboy apart from port we can configure there things like: ssl, ip address on which we listen, compression, maximum number of connections and more.

As we are here, we will start with inserting a configuration for our new endpoint and then do everything to make it work.

config :mendpoint, MendpointDiag.Endpoint,
  http: [port: 9999],
  debug_errors: true,
  code_reloader: true,
  check_origin: false

As presented above new endpoint will be listening on port 9999 and will be located in MendpointDiag module.

New endpoint module

As expected, by adding just a configuration nothing really happens. We need to add module that we are referencing in the config - MendpointDiag.Endpoint. We can just replicate MendpointWeb for this purpose:

defmodule MendpointDiag.Endpoint do
  use Phoenix.Endpoint, otp_app: :mendpoint

  # Code reloading can be explicitly enabled under the
  # :code_reloader configuration of your endpoint.
  if code_reloading? do
    socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
    plug Phoenix.LiveReloader
    plug Phoenix.CodeReloader
  end

  plug Plug.Logger

  plug Plug.Parsers,
    parsers: [:urlencoded, :multipart, :json],
    pass: ["*/*"],
    json_decoder: Poison

  plug Plug.MethodOverride
  plug Plug.Head

  # The session will be stored in the cookie and signed,
  # this means its contents can be read but not tampered with.
  # Set :encryption_salt if you would also like to encrypt it.
  plug Plug.Session,
    store: :cookie,
    key: "_mendpoint_diag_key",
    signing_salt: "aaG/Ts"

  plug MendpointDiag.Router

  @doc """
  Callback invoked for dynamically configuring the endpoint.

  It receives the endpoint configuration and checks if
  configuration should be loaded from the system environment.
  """
  def init(_key, config) do
    if config[:load_from_system_env] do
      port = System.get_env("DIAG_PORT") || raise "expected the DIAG_PORT environment variable to be set"
      {:ok, Keyword.put(config, :http, [:inet6, port: port])}
    else
      {:ok, config}
    end
  end
end

I looks almost the same as MendpointWeb. There is one difference apart from the name - we plug MendpointDiag.Router instead of MendpointWeb.Router to make it work we need to create the router module as well.

defmodule MendpointDiag.Router do
  use MendpointDiag, :router

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", MendpointDiag do
    pipe_through :api # Use the default browser stack

    get "/", PageController, :index
  end

end

Note that we used the API pipeline here as we want to return simple json response, there is no need for complicated, templated views and layouts. In the routes we can easily spot PageController, so let’s add it as well to have the last part of puzzle:

defmodule MendpointDiag.PageController do
  use MendpointWeb, :controller

  def index(conn, _params) do
    json conn, %{status: "ok"}
  end
end

Very basic controller that just returns {"status": "ok"} response no matter what, cause elixir services never go down ;).

Lest start the server in interactive mode(iex -S mix phx.server) and try the following:

:inet.i
Port  Module   Recv Sent Owner     Local Address Foreign Address State     Type
30545 inet_tcp 0    0    <0.332.0> *:terabase    *:*             ACCEPTING STREAM

terrabase is port 4000, so no trace of our newly added endpoint…

Who does start the endpoints?

We learned that endpoints are not started automatically, based on either the configuration entries or directory structure convention. We are in the BEAM world, so we should start looking for startup sequence in the application module and there it is! Endpoints are started as supervisors attached directly to the app supervisor. The only thing wee need to do is to add supervisor(MendpointDiag.Endpoint, []) to the children list. Additionally, we should add it to config change handler.

defmodule Mendpoint.Application do
  use Application

  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  def start(_type, _args) do
    import Supervisor.Spec

    # Define workers and child supervisors to be supervised
    children = [
      # Start the endpoint when the application starts
      supervisor(MendpointWeb.Endpoint, []),
      supervisor(MendpointDiag.Endpoint, [])
      # Start your own worker by calling: Mendpoint.Worker.start_link(arg1, arg2, arg3)
      # worker(Mendpoint.Worker, [arg1, arg2, arg3]),
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: Mendpoint.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # Tell Phoenix to update the endpoint configuration
  # whenever the application is updated.
  def config_change(changed, _new, removed) do
    MendpointWeb.Endpoint.config_change(changed, removed)
    MendpointDiag.Endpoint.config_change(changed, removed)
    :ok
  end
end

Having the changes above let’s restart the server:

iex -S mix phx.server

Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

[info] Running MendpointWeb.Endpoint with Cowboy using http://0.0.0.0:4000
[info] Running MendpointDiag.Endpoint with Cowboy using http://0.0.0.0:9999
Interactive Elixir (1.6.4) - press Ctrl+C to exit (type h() ENTER for help)

The Log messages are very promising, we can spot our endpoint and corresponding module in it. We go and type localhost:9999 in our browser and voalá, we can see

{"status": "ok"}

Summary

I think this example showed us how easy it is to extend phoenix framework, especially knowing how standard OTP application is designed. It would be nice to have mix generator for it, but on the other hand productivity gain will be small, as one usually set up it once at the beginning of a project.

comments powered by Disqus
rss facebook twitter github youtube mail spotify instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora