Hashrocket.com / blog

Large large gatling

How I Built My Own Heroku for Phoenix Apps: Part 2

posted on and written by in

Image 100x100 micah cooper

Building upon the the first version of Gatling. We're adding functionality to handle event hooks for each action in the deployment process

Click here to read part 1 first.

Gatling is a deployment tool in for Phoenix apps. In this post, we'll walk through how I updated gatling to add a lot more flexibility to the deployment process.

First, let's walk through all the steps gatling takes to execute the mix gatling.upgrade task and the elixir functions that execute them. Remember, this is triggered by a a git post-update hook on the server.

$ mix deps.get                   -> mix_deps_get
$ mix compile                    -> mix_compile
$ mix digest                     -> mix_digest
$ mix release                    -> mix_release
$ mkdir /path/to/deploy          -> make_upgrade_dir
$ cp -r release /path/to/deploy  -> copy_release_to_upgrade
$ sudo service <project> upgrade -> upgrade_service
# configure nginx                -> configure_nginx

All these functions are already defined and execute on the server. But I wanted the ability to execute some elixir code before and/or after each of these steps and I wanted to configure this in each app I deployed as each app my require different steps.

I wanted to put a file called ./upgrade.ex in my project. And when I deploy with a git push, that file would be picked up by Gatling to execute any hooks I defined.

Here is an example of one of these hooks:

#./deploy.ex

defmodule MyProject.UpgradeHooks do
  def before_mix_deps_get(_env) do
    #do some work
    #log some things
    #track some things
  end
end

With this above example, every time we do a git push, Gatling would find the ./upgrade.ex and call before_mix_deps_get right before the default mix_deps_get function.

How it's made

Now we know the desired functionality, lets walk throught the implementation.

Here is the (abbreviated) original verion of the Mix.Tasks.Gatling.Upgrade module:

defmodule Mix.Tasks.Gatling.Upgrade do
  def upgrade(project) do
    Gatling.env(project)
    |> mix_deps_get
    |> mix_compile
    |> mix_digest
    |> mix_release
    |> make_upgrade_dir
    |> copy_release_to_upgrade
    |> upgrade_service
    |> configure_nginx
  end
end

We want to have a wrapper function around each of these that will hook in a before and after function. Let's just use call. So our upgrade function will now look lik this:

defmodule Mix.Tasks.Gatling.Upgrade do
  def upgrade(project) do
    Gatling.env(project)
    |> call(:mix_deps_get)
    |> call(:mix_compile)
    |> call(:mix_digest)
    |> call(:mix_release)
    |> call(:make_upgrade_dir)
    |> call(:copy_release_to_upgrade)
    |> call(:upgrade_service)
    |> call(:configure_nginx)
  end
end

call/1

Let's see how call works:


 1 def callback(env, action, type) do
 2   module          = env.upgrade_callback_module
 3   callback_action = [type, action]
 4                     |> Enum.map(&to_string/1)
 5                     |> Enum.join("_")
 6                     |> String.to_atom()
 7
 8   if function_exported?(module, callback_action, 1) do
 9     apply(module, callback_action, [env])
10   end
11
12   nil
13 end
14
15 def call(env, action) do
16   callback(env, action, :before)-
17   apply(__MODULE__, action, [env])
18   callback(env, action, :after)
19   env
20 end

Starting with callback/3 on line:1 we take in the following parameters:

  • env => A struct with all the information we need for a deploy
  • action => An atom of the function we want to call e.g. :mix_deps_get
  • type => An atom of the callback type. Either :before or :after
  1. line:2 Assign our callback module which was previously loaded from the file ./upgrade.ex
  2. line:3 - 6 Assign our callback function. :before_mix_deps_get
  3. line:8-10 If the function exists in the module we defined in .upgrade.ex, then execute it now.
  4. line:12 Return nil. We don't want this callback function to be able to change the current env in any way. So we return nil to express that.

In our call/2 function we take in the following parameters

  • env => A struct with all the information we need for a deploy
  • action => An atom of the function we want to call e.g. :mix_deps_get

You'll notice these arguments are the same as callback/3 just without a type.

  1. line:16 Execute the before action callback e.g before_mix_compile
  2. line:17 Execute the actual action e.g. mix_compile
  3. line:18 Execute the after action callback e.g. after_mix_compile
  4. line:20 Return the env struct so it can be used by the next function in the pipeline.

And that's it! It's actually quite simple. There is still one part missing though. How did that env.upgrade_callback_module get in there. How is Gatling loading our ./upgrade.ex file and using it in its own project?

Loading code from another project

Before we start loading code, lets look at hour our env is created.

First we have our %Gatling.Env{} struct. For brevity's sake, we're only seeing the relavent attributes here:

defmodule Gatling.Env do
  defstruct ~w[
    #...
    upgrade_callback_module
    #...
  ]s
end

Our Gatling module is what actually populates this struct:

 1 defmodule Gatling do
 2   def env(project) do
 3     %Gatling.Env{
 4       #...
 5       :upgrade_callback_module => callback_module(project, task: "upgrade"),
 6       #...
 7     }
 8   end
 9
10   def callback_module(project, [task: task_name]) do
11     callback_path = Path.join(path/to/project, "#{task_name}.ex")
12     if File.exists? callback_path do
13       Code.load_file(callback_path) |> List.first() |> elem(0)
14     else
15       nil
16     end
17   end
18 end

When we call Gatling.env we populate upgrade_callback_module with callback_module/2:

  1. line:11 build the path that points to the deployed project and assign it
  2. line:12-16 If the file exists, use the Code module to load the elixir file into the Gatling runtime

When we call Gatling.env.upgrade_callback_module we'll have assess to the module we defined in ~/upgrade.ex which (again) looks like this:

defmodule MyProject.UpgradeHooks do
  def before_mix_deps_get(_env) do
    #do some work
    #log some things
    #track some things
  end
end

And that's it! That's all the moving parts and in my opinion, it's quite a minimal effort to gain the flexablity I desired. This concludes part 2 of this blog serires. Next, we'll look into migrating Gatling's underlying dependency exrm with it's successor - Distillery

Posted in Elixir and tagged with Deployment, Elixir