Sheharyar Naseer

Deploying a Simple Elixir app to GCP with Kubernetes

Elixir v1.9 was just released and with that it brought, uh well, “releases” right into the standard library. Before this, we had to rely on Distillery and EXRM, two excellent tools created by the legendary Paul Schoenfelder, to build releases from Elixir applications before you could deploy them.

I thought I use this opportunity to dive into Elixir releases, and deploy them to Google Cloud Platform with Kubernetes.

For the purposes of learning, we’ll only build and deploy a simple Elixir application. That means no Phoenix Framework, and no Erlang clustering and data-sharing between nodes. So let’s get started:

Creating the App

Start by installing the latest version of Elixir with support for releases (I personally use asdf for this). We’ll create a simple app that just responds with a simple “Hello” message to any GET request.

$ mix new hello --sup

Since we only need plug for this, add that to your dependencies in mix.exs:

defp deps do
  [{:plug_cowboy, "~> 2.0.0"}]

And call mix deps.get. Let’s create the module to respond to web requests. Create a new file called server.ex and put this in:

defmodule Hello.Server do
  @behaviour Plug

  @impl true
  def init(opts), do: opts

  @impl true
  def call(conn, _opts) do
    |> Plug.Conn.put_resp_content_type("text/plain")
    |> Plug.Conn.send_resp(200, "Hi there!")

To be accurate, this will respond to requests for all HTTP methods on all paths, and not just GET, but that’s okay. Next, we need to tell the App supervisor to start a server with this Plug when the application starts. Open application.ex and add this to children:

children = [
  {Plug.Cowboy, scheme: :http, plug: Hello.Server, options: [port: 3000]},

Start the elixir app:

$ mix run --no-halt

And you should see the message Hi there! when opening localhost:3000 in your browser. You can find the full source code here.

Build a Release

Building releases in Elixir now is a breeze. We don’t need to rely on any external dependencies for one, and second, the whole process is super simple. While Elixir does allow you to customize how releases are built and configure them with a lot of options, you can get away by simply running one command:

$ MIX_ENV=prod mix release

This will spit out a compiled binary of your application in _build/prod/rel. To test that it works correctly, you can start the app:

$ _build/prod/rel/hello/bin/hello start

Visiting localhost:3000 in your browser will work like before. You can find a full list of options and more information on HexDocs.

Create a Docker image

I don’t need to mention that you must have Docker installed for this, do I? We’re going to create a Dockerfile in the app root directory with instructions on how to build the release and run the app. Put this in there:

# Build the release
# -----------------

FROM elixir:1.9.0-alpine as build

WORKDIR /source
RUN mix local.hex --force && mix local.rebar --force

# Install and compile dependencies
COPY mix.exs mix.lock ./
COPY config config
RUN mix do deps.get, deps.compile

# Compile and build the app
COPY . .
RUN mix do compile, release

# Run the app
# -----------

FROM elixir:1.9.0-alpine

COPY --from=build /source/_build/${MIX_ENV}/rel/hello .

CMD ["bin/hello", "start"]

We also don’t want docker copying over unnecessary files, so also create a .dockerignore file with the following contents:

# something

You can now build and run your docker image. Let’s give it a name hello-server and tag it v1:

$ docker build -t hello-server:v1 .
$ docker run -it --rm -p 3000:3000 hello-server:v1

Opening localhost:3000 in your browser should work as expected.

Deploying to Google Cloud

Before we can finally deploy it to GCP, we still need to prepare some things:

Now log in to your account and set the project you created:

$ export GCP_PROJECT_ID="<your-gcp-project-id-here>"
$ gcloud auth login
$ gcloud config set project $GCP_PROJECT_ID

We can now build the docker image on GCP and save it to our private registry. This will upload a tarball of your source code to Google’s build servers and remotely build and tag your image:

$ gcloud builds submit${GCP_PROJECT_ID}/hello-server:v1 .

Now to the good part, running it. We start by creating a cluster with two nodes in the region of your choice (for me it’s South East Asia), creating a deployment with the image we built earlier and then exposing it on port 80 via a load balancer.

$ gcloud container clusters create hello-cluster --num-nodes=2 --region=asia-south1
$ kubectl run hello${GCP_PROJECT_ID}/hello-server:v1 --port 3000
$ kubectl expose deployment hello --type=LoadBalancer --port 80 --target-port 3000

We can view what’s the status of our cluster and the nodes with these commands:

$ kubectl get service
$ kubectl get pods

The get service command will initially show “Pending” as the External API but after a couple of minutes it’ll display the IP address where our deployed elixir app is exposed. We can then visit it in any browser to see the familiar message:

$ curl <service-ip-address>
Hi there!

Deploying a newer version

Say, after you make some changes to your app, you want to deploy a newer version? That’s also straight-forward. Just submit a new build to GCP and update the deployment service:

$ gcloud builds submit${GCP_PROJECT_ID}/hello-server:v2 .
$ kubectl set image deployment/hello${GCP_PROJECT_ID}/hello-server:v2

That’s it! And if you want to rollback to a previous version, just call the command again with the version tag you want to rollback to:

$ kubectl set image deployment/hello${GCP_PROJECT_ID}/hello-server:v1

That’s pretty much it. I realize this guide doesn’t cover some of the common Elixir/Phoenix cases, but that’s okay, since this is just meant as a starting point and a learning experience. In one of my future posts, I’ll do a much more detailed one. In the mean time, you can find the source code on Github.