Making Your Elixir Services Configurable CLI Applications In The Blink Of An Eye
Introducing a New Micro-library Called Takeoff
Quite recently I’ve went on to learn Elixir, which up to this point was a real delight. The fact it is built on the Erlang VM means that you get everything it has to offer, such as its built-in support for concurrency and fault-tolerance. At the same time, Elixir provides a language similar to Ruby in syntax, all along with a powerful macro system.
One thing I’ve been stumbling on, however, is that there seems to be no easy way to create services that have their configuration assigned dynamically, e.g. through the command-line. Everything you want to configure — from a port your HTTP service should run on to connection parameters for the database — must, as far as I am aware of, be included in a file within the config/ directory of the project root. This is not always doable. What if, for instance, I want to quickly run several services on different ports for testing purposes? In such a case, it would be really useful if I could just do my_service --port 5000 , which using vanilla Elixir is not possible.
The only two approaches of solving this problem I could find was either hand-writing an Escript-based service bootstrapper or using conform. Conform seemed too big for the humble problem I was trying to solve. Moreover it still requires you to write a file, without support for parsing command-line arguments. On the alternative side, hand-writing a service bootstrapper would mean more code to maintain, and as someone who has a lot of projects to maintain I’m not a big fan of such situations. That’s why I went on to create a tiny library called Takeoff, about which I will talk more in rest of this article.
Takeoff is elegantly simple. It does nothing more than matching command-line arguments with application configuration parameters. By doing so, you can effectively call my_service --http-port 3001 with limited modification to the source code. The following snippet shows how to use it in your own code:
defmodule MyApp.CLI do
use Takeoff.Launcher,
http: [:maru, MyApp.API, :http],
db: [:myapp, MyApp.Repo]
endIn this snippet, we link every --http- -prefixed command-line argument to the HTTP configuration of Maru, while passing every --db- -prefixed argument to an Ecto repository. I’ve tested it just now and it works for these simple cases. It still is very much a work in progress, but until now I quite like it, and it solves exactly the problem I described in the beginning concerning quickly starting multiple instances of the same service.
To get it fully working you need to do three remaining things:
- Add Takeoff as a dependency
- Remove applications that need to be configured from
mix.exs - Make sure you can generate an Escript version of your service
Adding Takeoff as a dependency is quite obvious. Step no. 2 might be less obvious. You see, when you let mix start your application, it is already started with the configuration you defined in config/ . That’s not what we want, and I couldn’t find any other way to work around it than to defer the booting process of these applications to something Takeoff should take care off. As for the last step, adding something like escript: [main_module: MyApp.CLI] in your project do-block will do it.
Interested? Then head over to the package page on hex.pm or view the source on GitHub. If you’ve found any bugs don’t forget to let me know.
