With every app we decide to take out to the world, there's one important decision to make when the time comes.
Where is the best place to deploy it?
The short answer is: "It depends, probably there's no one best place actually".
The long answer however, will depend on what your needs are, your budget, the setup of your current applications, company restrictions etc.
Particularly, I think a really convenient way to deploy your apps in no time is using Platform as a Service (PaaS) options like Heroku.
Heroku is a platform that allows you to abstract yourself from all the complexity that comes from deploying and maintaining an app, and to focus your efforts on the app you want to build. Here's a video from Heroku's Greg Nokes explaining the concept behind their platform.
Heroku has this idea of buildpacks, which transform your code into a slug that can be run on their platform and abstract all the complexity.
They support a small range of popular programming languages using these buildpacks, however Elixir is not one of them.
But not all hope is lost, we have the ability to use custom buildpacks to support it ourselves. And there is one already available that Akash Manohar and his contributors built for us.
If this is all new for you, don't worry, we're going to start putting up the pieces together in a bit.
Let's build a very small app using Elixir that queries an open API and displays the data via HTTP in a JSON format.
Before you get started, you'll need a Heroku account. You can create a new one for free at heroku.com, and there's a free tier we can use for our development projects.
If you're not too familiar with Elixir, you can check my hello-world-elixir article to give you an overview.
This article will be divided into 5 parts
- Creating your Elixir app skeleton.
- Writing the basic functionality.
- Testing it works locally.
- Creating your Heroku app.
- Deploying and testing your app.
1. Creating your Elixir app skeleton
There are multiple ways to create an Elixir app. You could use one of the available frameworks or you could potentially create the files yourself one by one for example. In this exercise, we'll use the convenient mix new helper that will create most of the initial structure for us.
Go to your project folder on your shell, then type mix new simple_app --sup
❯ mix new simple_app --sup
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/simple_app.ex
* creating lib/simple_app/application.ex
* creating test
* creating test/test_helper.exs
* creating test/simple_app_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd simple_app
mix test
Run "mix help" for more commands.
This will create a basic skeleton for your app, including a module called SimpleApp
and a basic supervisor.
Make sure to check your version of Elixir. Syntax and structure might differ slightly on different versions. I'm using 1.11.3 for the following exercise.
Let's go ahead and compile our app first to test the hello method.
❯ cd simple_app
❯ mix compile
Compiling 2 files (.ex)
Generated simple_app app
We can use the interactive console to test manually or just use mix test
to use the test suite
❯ iex -S mix
Erlang/OTP 23 [erts-11.1.7]
Interactive Elixir (1.11.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> SimpleApp.hello()
:world
By typing SimpleApp.hello()
we are invoking the hello
method created by the helper and we should get a symbol :world
in return if everything is set up correctly.
2. Writing the basic functionality
For this app, we'll use the public API of Coindesk to query the price of Bitcoin today. If you'd rather prefer to use a different API, you can find a list of public ones on this Github repo. The data that we're getting is really not that important for the purpose of this exercise.
Before continuing however, you may want to check the API you choose is operational and to check the format of the returned data. I'd use curl
in my shell, but wget
, Postman or any other UI client should do the trick.
❯ curl https://api.coindesk.com/v1/bpi/currentprice.json
To integrate the API via Elixir let's use the HTTP wrapper Tesla. There are many good options out there, such as the good old Httpoison. However, Tesla has some added benefits. I won't go into details as it's not the purpose of this article, but it's worth checking out.
To install Tesla, we need to first add the dependency to our mix.exs
file
defp deps do
[
{:tesla, "~> 1.4.0"},
{:hackney, "~> 1.16.0"},
{:jason, ">= 1.0.0"}
...
]
end
Notice that I also included hackney
and jason
. We'll use Hackey as the adapter to actually perform our http calls. Tesla uses httpc
as the default adapter, but Hackney is far better in many aspects.
We'll need jason
too as it's a required JSON parser for our middleware. It's also more performant than other JSON parsers so it's good to have anyway.
Go back to your shell and install the newly added dependencies
❯ mix deps.get
You may not have hex
installed in your system yet, so if you get prompted an option to do it, just say yes.
The last part of the Tesla setup is adding the adapter to our config file. If you haven't created the file yet, just put it under config/config.exs
.
We can then add the Hackney adapter so your config file should look like this
use Mix.Config
config :tesla, adapter: Tesla.Adapter.Hackney
And that's it, we're done configuring. Throw in a mix compile
for good measure, to make sure we're still good to go and we haven't made any typos.
The API calling class
To query the Coinbase API via Tesla, we're going to create a model.
I like to have business logic inside a models folder because it helps me keep things organised on larger projects. Go ahead and create lib/models/coinbase.ex
Inside it we'll need 3 things, our module definition, our Tesla middleware configuration and the method(s) we want to abstract.
defmodule Coinbase do
@moduledoc """
A module abstraction used to interact with the coinbase http API
"""
use Tesla
plug Tesla.Middleware.BaseUrl, "https://api.coindesk.com"
plug Tesla.Middleware.Headers, [{"Content-Type", "application/json"}]
plug Tesla.Middleware.JSON
@doc """
Returns the Bitcoin Price Index from coinbase
"""
def bpi_current_price do
get("/v1/bpi/currentprice.json")
end
end
We can test this right away in our iex
console
❯ iex -S mix
Erlang/OTP 23
Interactive Elixir (1.11.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Coinbase.bpi_current_price
{:ok,
%Tesla.Env{
__client__: %Tesla.Client{adapter: nil, fun: nil, post: [], pre: []},
__module__: Coinbase,
body: ...,
headers: [
...
],
method: :get,
opts: [],
query: [],
status: 200,
url: "https://api.coindesk.com/v1/bpi/currentprice.json"
}}
Displaying the data
With our base model done, the last thing we need is something to display the data in an easy to read format.
We are going to be using the light http server Cowboy and the adapter Plug to return our data.
Exploring the different server options and frameworks along with their pros and cons is out of the scope of this article, so we'll stick with the simplest solution.
Let's go back to our mix.exs
file to add Plug and Cowboy to our dependencies
defp deps do
[
{:tesla, "~> 1.4.0"},
{:hackney, "~> 1.16.0"},
{:jason, ">= 1.0.0"},
{:plug_cowboy, "~> 2.0"} # NEW!
...
]
end
Get those deps, compile the project and we're ready to move forward
❯ mix deps.get
❯ mix compile
Now, without caring for organisation too much, let's add a little router file that will expose our endpoint to the world and call the API we've created previously.
Go ahead and create lib/router.ex
and define a basic Plug endpoint
defmodule SimpleAppRouter do
use Plug.Router
plug :match
plug :dispatch
get "/bpi" do
send_resp(conn, 200, "BPI price coming soon...")
end
match _ do
send_resp(conn, 404, "Wrong place mate")
end
end
If you're not familiar with it, feel free to check the inner workings of Plug in their documentation. For now, the code above is fairly self-explanatory I hope. All we need to know is that we're using the Plug.Router
capabilities and exposing an endpoint /bpi
which we're going to use to retrieve our data and to show it.
We can now proceed to tell our supervision tree that our Router is ready to roll.
Go to the lib/simple_app/application.ex
where you'll find the application file created by the mix new
helper. Here we will ask our application to start the router under the supervision tree.
Your resulting file will look like this
defmodule SimpleApp.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc """
Application that starts the http router for `SimpleApp`.
"""
use Application
@impl true
def start(_type, _args) do
children = [
{Plug.Cowboy, scheme: :http, plug: SimpleAppRouter, options: [port: 8080]}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: SimpleApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
You can go ahead and test it all works fine by running your server with
❯ mix run --no-halt
To display the Coinbase data, all we need to do is go back to our SimpleAppRouter
and add the Coinbase call. Your router function will then look like this
get "/bpi" do
send_resp(conn, 200, Coinbase.bpi_current_price)
end
If you remember the format of the bpi_current_price
function response, you may have noticed that it includes not only the body of the response but also headers, process status etc which we don't necessarily want to display. So to fix this, go back to lib/models/coinbase.ex
and modify the function to only return the response body instead
def bpi_current_price do
{:ok, response} = get("/v1/bpi/currentprice.json")
response.body
end
Bear in mind we're not doing any sort of validations or error considerations for the sake of simplicity.
3. Testing it works locally
Now with the app finally ready, we can test it works in our local environment.
Go ahead and run your server again
❯ mix run --no-halt
Then in your browser or curl client, you should be able to obtain the results on http://localhost:8080/bpi
❯ curl http://localhost:8080/bpi
HTTP/1.1 200 OK
...
server: Cowboy
{ ..."chartName":"Bitcoin","bpi":{"USD":{"code":"USD"... }
4. Creating your Heroku app
Before we move entirely into the Heroku app creation, there's one last thing we need to change in the code.
Notice how we used port 8080
as the default, but once in Heroku our app port will be dynamic and determined by them. For this reason, we need to read the port from the environment variables.
To do this, go ahead and update the port 8080 inside the application file with one taken from the system environment variables.
children = [
{Plug.Cowboy,
scheme: :http,
plug: SimpleAppRouter,
options: [port: (System.get_env("PORT") || "8080") |> String.to_integer()]
}
]
This has the added benefit of defaulting to 8080 if the PORT
environment variable does not exist, which is convenient when using it locally.
To create our Heroku app we're going to be using the heroku-cli. However, if you feel more comfortable using an UI, you can go to your Heroku dashboard and the steps are pretty straightforward.
The first thing you need is a Heroku account, which I'm going to assume you already have. If you don't, just sign up on their website using your preferred email and password.
PRO TIP: If you happen to have an existing Heroku account that you do not want
to use for this exercise, you can use the heroku-accounts plugin to manage
multiple accounts easily.
PRO TIP: If you happen to have an existing Heroku account that you do not want
to use for this exercise, you can use the [heroku-accounts](https://github.com/heroku/heroku-accounts) plugin to manage
multiple accounts easily.
To create a new Heroku app using the cli, just go to the root directory of your app in the shell and type
❯ heroku create simple-app-elixir
Creating ⬢ simple-app-elixir... done
https://simple-app-elixir.herokuapp.com/ | https://git.heroku.com/simple-app-elixir.git
Where simple-app-elixir
is a unique name you want to give to your app.
Because Heroku doesn't have any default buildpack for Elixir at the time of writing, we need to set our app's buildpack to HashNuke's version of it.
❯ heroku buildpacks:set \
https://github.com/HashNuke/heroku-buildpack-elixir.git -a simple-app-elixir
For this buildpack to work in production, we need to specify a default configuration on our Heroku app. In the root of the project go ahead and create a file named elixir_buildpack.config
and fill it with your versions of the OTP and Elixir.
erlang_version=23.0
elixir_version=1.11
always_rebuild=false
runtime_path=/app
The last thing we need to do is to tell Heroku what process to run when deploying the app. For this we can use a Procfile
. In our Procfile we will specify that we want mix
to run our app which in turn will run the Cowboy server.
Create another file at the root of your project called Procfile
(without any file extension), and add the following inside it
web: mix run --no-halt
Notice this is the same command that we use to run our server locally. The buildpack will actually use this same command as default if no Profile is found, but it's good practice to define it ourselves anyway.
5. Deploying and testing your app
Now we're ready to deploy. Heroku has a git
based deployment system, which is really convenient because all we need to do to deploy our app is to git push
our code.
In your shell, at the root of your project, go ahead and commit your code to Heroku as if it was another regular git repository.
You may need to add your Heroku remote using your own git URL
❯ git remote add heroku [https://git.heroku.com/simple-app-elixir.git](https://git.heroku.com/simple-app-elixir.git)
Yours was printed when creating the app, or it can also be found in the settings of your Heroku app.
# If you haven't initialised the repository yet
❯ git init
Initialized empty Git repository
# Commit your code
❯ git add .
❯ git commit -m "SimpleApp initial commit. Coinbase API"
❯ git push heroku master
This should've deployed your app if your setup is correct, SSH keys are set and there aren't any additional configuration issues.
Once your code is deployed, Heroku will assign a subdomain to your app, something like
https://simple-app-elixir.herokuapp.com/
Depending on the time you're reading this article, mine may or may not be active. Either way, once yours is deployed you should be able to check it out.
You can go ahead and test our /bpi
endpoint in the browser or via curl.
❯ curl https://simple-app-elixir.herokuapp.com/bpi
This endpoint should return a json object with the Coinbase Bitcoin exchange rates and your deployment would be complete. Congrats! 👏
Conclusion
As you can see, it's fairly simple to deploy Elixir apps in Heroku. The platform allows for scaling as you go and it's able to handle Elixir's performance requirements very well.
Another advantage of Heroku is that you can have apps in multiple languages running all under the same umbrella platform, which lets you have complex microservices architectures servicing different needs without any additional platform knowledge required.
I've put a full working version of the code on this Github repository, in case you want to compare with it or simply clone it and test it.
For more topics, check my profile or visit rarias.dev.
Happy coding!
Top comments (0)