Deploying a Phoenix application using ansible-elixir-stack

Akash Manohar (@HashNuke) has created ansible-elixir-stack, a really interesting tool for simple deployment of Elixir & Phoenix apps using Ansible.. except it doesn't require you to know anything about Ansible, making it a great option if you just want to get up and running with your new app.

While I do have a fair amount of experience using Ansible, this seems to be a quick way to get started, and something that you can adapt to your own needs as you learn more. So let's see what it's all about, by creating a simple Phoenix project from scratch, and deploying it!

NOTE: If you run into problems, see the Troubleshooting section at the end..

Installation

Get up and running with Phoenix 0.15:

$ mix archive.install https://github.com/phoenixframework/phoenix/releases/download/v0.15.0/phoenix_new-0.15.0.ez
Found existing archive(s): phoenix_new-0.13.0.ez.
Are you sure you want to replace them? [Yn] 
* creating .mix/archives/phoenix_new-0.15.0.ez

Now for ansible-elixir-stack.. for which we need Ansible. I've already got it installed, so I'll just make sure it's upgraded to the latest version:

$ sudo /usr/local/bin/pip install ansible --upgrade
Downloading/unpacking ansible from https://pypi.python.org/packages/source/a/ansible/ansible-1.9.2.tar.gz#md5=5e0a72c8b7a3848907ac28b5f38cfc27
  Downloading ansible-1.9.2.tar.gz (927kB): 927kB downloaded
...
Successfully installed ansible jinja2 setuptools
Cleaning up...

Then install the role:

$ sudo /usr/local/bin/ansible-galaxy install HashNuke.elixir-stack
- downloading role 'elixir-stack', owned by HashNuke
- downloading role from https://github.com/HashNuke/ansible-elixir-stack/archive/master.tar.gz
- extracting HashNuke.elixir-stack to /etc/ansible/roles/HashNuke.elixir-stack
- HashNuke.elixir-stack was installed successfully

Project Setup

First let's create a simple project:

$ mix phoenix.new hello_phoenix
* creating hello_phoenix/config/config.exs
...

Fetch and install dependencies? [Yn] y
* running npm install && node node_modules/brunch/bin/brunch build
* running mix deps.get

We are all set! Run your Phoenix application:

    $ cd hello_phoenix
    $ mix ecto.create
    $ mix phoenix.server

You can also run your app inside IEx (Interactive Elixir) as:

    $ iex -S mix phoenix.server

Deployments using ansible-elixir-stack depend on having your code in a Git repo that can be accessed from the server, so I created a repository on GitHub for this. Just need to initialize the local repo and then publish our code:

$ cd hello_phoenix
$ git init
Initialized empty Git repository in /home/johwar/src/hello_phoenix/.git/
$ git add .
$ git commit -m "Initial commit"
[master (root-commit) 4b5b0ce] Initial commit
 37 files changed, 2080 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 README.md
 create mode 100644 brunch-config.js
 ...
$ git remote add origin git@github.com:jwarlander/hello_phoenix.git
$ git push -u origin master
Counting objects: 61, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (52/52), done.
Writing objects: 100% (61/61), 49.73 KiB | 0 bytes/s, done.
Total 61 (delta 1), reused 0 (delta 0)
To git@github.com:jwarlander/hello_phoenix.git
 * [new branch]      master -> master
Branch master set up to track remote branch master from origin.

Now it's time to run the setup script as described in the README. Being at least marginally cautious, I've of course examined the contents first..

$ curl -L http://git.io/ansible-elixir-stack.sh | bash
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100  2171  100  2171    0     0   3260      0 --:--:-- --:--:-- --:--:--  3260

*-*-*
Oolaa ~! your project has been setup for deployment
*-*-*

TODO Edit .tool-versions file with appropriate versions of Erlang, Elixir & Node.js required for project
TODO Add server IP address to inventory file
TODO Edit app name, app port & repo url in playbooks/vars/main.yml

Hmm, so we've got a few tasks to finish.. Tool versions look fine as-is:

$ cat .tool-versions
erlang 18.0
elixir 1.0.5
nodejs 0.12.5

The settings in playbooks/vars/main.yml are also good:

$ cat playbooks/vars/main.yml
---
app_name: hello_phoenix
repo_url: "git@github.com:jwarlander/hello_phoenix.git"
app_port: 3001

However, the Ansible inventory file needs to be updated with a real server IP.

I've gone ahead and created a small Digital Ocean droplet running Ubuntu 14.04. I'm also using an SSH key for access, which is configured for the DO server when the droplet is created.

NOTE: If you use this referral link to create an account with Digital Ocean, you will get a $10 credit.

So, let's update inventory, using the actual server IP:

$ cat > inventory
[app-servers]
104.236.59.65

Oh.. and we must not forget the Exrm dependency (as I did the first time through this blog post). Let's add it:

$ vim mix.exs  # edit the file using whatever you're comfortable with, of course
$ git diff
diff --git a/mix.exs b/mix.exs
index 9af193f..7f8fb0c 100644
--- a/mix.exs
+++ b/mix.exs
@@ -34,6 +34,7 @@ defmodule HelloPhoenix.Mixfile do
      {:postgrex, ">= 0.0.0"},
      {:phoenix_html, "~> 1.4"},
      {:phoenix_live_reload, "~> 0.5", only: :dev},
-     {:cowboy, "~> 1.0"}]
+     {:cowboy, "~> 1.0"},
+     {:exrm, "~> 0.18.1"}]
   end
 end

Make sure we update dependencies in mix.lock, which goes into version control, to avoid problems later on:

$ mix deps.get
A new Hex version is available (v0.8.3), please update with `mix local.hex`
Running dependency resolution
Dependency resolution completed successfully
  getopt: v0.8.2
  erlware_commons: v0.13.0
  ...
* Getting erlware_commons (Hex package)
Checking package (https://s3.amazonaws.com/s3.hex.pm/tarballs/erlware_commons-0.13.0.tar)
Using locally cached package
Unpacked package tarball (/home/johwar/.hex/packages/erlware_commons-0.13.0.tar)
* Getting bbmustache (Hex package)
Checking package (https://s3.amazonaws.com/s3.hex.pm/tarballs/bbmustache-1.0.1.tar)
Using locally cached package
Unpacked package tarball (/home/johwar/.hex/packages/bbmustache-1.0.1.tar)

Finally, commit and push the changes so far:

$ git add config/config.exs .tool-versions ansible.cfg inventory playbooks/ mix.exs mix.lock
$ git commit -m "Add ansible-elixir-stack setup"
[master 4d3e102] Add ansible-elixir-stack setup
 11 files changed, 62 insertions(+), 3 deletions(-)
 create mode 100644 .tool-versions
 create mode 100644 ansible.cfg
 create mode 100644 inventory
 create mode 100644 playbooks/deploy.yml
 create mode 100644 playbooks/migrate.yml
 create mode 100644 playbooks/remove-app.yml
 create mode 100644 playbooks/setup.yml
 create mode 100644 playbooks/vars/main.yml
$ git push
Counting objects: 16, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (13/13), done.
Writing objects: 100% (16/16), 1.93 KiB | 0 bytes/s, done.
Total 16 (delta 6), reused 0 (delta 0)
To git@github.com:jwarlander/hello_phoenix.git
   3c39018..4d3e102  master -> master

First Deployment

Easy as pie:

$ ansible-playbook playbooks/setup.yml

PLAY [app-servers] ************************************************************ 

GATHERING FACTS *************************************************************** 
The authenticity of host '104.236.59.65 (104.236.59.65)' can't be established.
ECDSA key fingerprint is 38:7e:57:a6:b8:45:f0:f7:f6:cc:b8:06:3e:49:06:1c.
Are you sure you want to continue connecting (yes/no)? yes
ok: [104.236.59.65]

TASK: [HashNuke.elixir-stack | stop app] ************************************** 
skipping: [104.236.59.65]

TASK: [HashNuke.elixir-stack | delete project dir] **************************** 
skipping: [104.236.59.65]

...

TASK: [HashNuke.elixir-stack | install tool versions] ************************* 
ok: [104.236.59.65]
<job 398556488564.17139> polling, 1790s remaining
ok: [104.236.59.65]
<job 398556488564.17139> polling, 1780s remaining
...
<job 398556488564.17139> polling, 1430s remaining
changed: [104.236.59.65]
<job 398556488564.17139> finished on 104.236.59.65

...

TASK: [HashNuke.elixir-stack | migrate database] ****************************** 
skipping: [104.236.59.65]

TASK: [HashNuke.elixir-stack | run command] *********************************** 
skipping: [104.236.59.65]

TASK: [HashNuke.elixir-stack | debug msg=""] ***** 
skipping: [104.236.59.65]

PLAY RECAP ******************************************************************** 
104.236.59.65              : ok=72   changed=54   unreachable=0    failed=0   

NOTE: It takes a fairly long time, at least on the small $5 droplet I'm using.. I wasn't watching it closely, but perhaps 20 minutes.

Success!

The Phoenix default site is now available at http://104.236.59.65/ via NGINX (and http://104.236.59.65:3001/ directly via Phoenix).

Deploying Updates

Suppose we added a "Hello, World" page, and wanted the world to see it.. We'll edit web/controllers/page_controller.ex and web/router.ex accordingly; see diff below:

diff --git a/web/controllers/page_controller.ex b/web/controllers/page_controller.ex
index b1df263..b9864b3 100644
--- a/web/controllers/page_controller.ex
+++ b/web/controllers/page_controller.ex
@@ -4,4 +4,8 @@ defmodule HelloPhoenix.PageController do
   def index(conn, _params) do
     render conn, "index.html"
   end
+
+  def hello(conn, _params) do
+    render conn, "hello.html"
+  end
 end
diff --git a/web/router.ex b/web/router.ex
index f9c5500..f20457d 100644
--- a/web/router.ex
+++ b/web/router.ex
@@ -16,6 +16,7 @@ defmodule HelloPhoenix.Router do
     pipe_through :browser # Use the default browser stack

     get "/", PageController, :index
+    get "/hello", PageController, :hello
   end

   # Other scopes may use custom stacks.

Then we need the hello.html.eex template:

$ cat > web/templates/page/hello.html.eex
<div class="jumbotron">
  <h2>Hello World, from Phoenix!</h2>
</div>

Let's commit and push changes:

$ git add web/router.ex web/controllers/page_controller.ex web/templates/page/hello.html.eex
$ git commit -m "Add a 'Hello, World' page"
[master 8ffa37e] Add a 'Hello, World' page
 3 files changed, 8 insertions(+)
 create mode 100644 web/templates/page/hello.html.eex
$ git push
Counting objects: 12, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (11/11), done.
Writing objects: 100% (12/12), 1.33 KiB | 0 bytes/s, done.
Total 12 (delta 5), reused 0 (delta 0)
To git@github.com:jwarlander/hello_phoenix.git
   0499a57..8ffa37e  master -> master

Finally, deploy:

$ ansible-playbook playbooks/deploy.yml

PLAY [app-servers] ************************************************************ 

GATHERING FACTS *************************************************************** 
ok: [104.236.59.65]

TASK: [HashNuke.elixir-stack | stop app] ************************************** 
skipping: [104.236.59.65]

TASK: [HashNuke.elixir-stack | delete project dir] **************************** 
skipping: [104.236.59.65]

...

TASK: [HashNuke.elixir-stack | migrate database] ****************************** 
skipping: [104.236.59.65]

TASK: [HashNuke.elixir-stack | run command] *********************************** 
skipping: [104.236.59.65]

TASK: [HashNuke.elixir-stack | debug msg=""] ***** 
skipping: [104.236.59.65]

PLAY RECAP ******************************************************************** 
104.236.59.65              : ok=35   changed=17   unreachable=0    failed=0   

...and we've got a freshly updated app on our server saying "Hello World, from Phoenix!"

Hooray :)

Troubleshooting

If you run into any issues, look below to see if it's a known problem.

Local modifications exist in repository

If you run into the following error when deploying updates...

TASK: [HashNuke.elixir-stack | clone project] ********************************* 
failed: [104.236.59.65] => {"failed": true}
msg: Local modifications exist in repository (force=no).

FATAL: all hosts have already failed -- aborting

PLAY RECAP ******************************************************************** 
           to retry, use: --limit @/home/johwar/deploy.retry

104.236.59.65              : ok=9    changed=0    unreachable=0    failed=1   

..then you may have added a dependency but forgotten to do a "mix deps.get" and committing + pushing the updated mix.lock file before the previous deploy.

It's easily fixed by connecting to your server, and cleaning up the workspace:

root@hello-phoenix:~# su - deployer
deployer@hello-phoenix:~$ cd projects/hello_phoenix
deployer@hello-phoenix:~/projects/hello_phoenix$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   mix.lock

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    rel/

no changes added to commit (use "git add" and/or "git commit -a")
deployer@hello-phoenix:~/projects/hello_phoenix$ git checkout -- mix.lock

Done! Now try to deploy again.

Hex dependency resolution failed

You may experience the following issue when the project is being built:

TASK: [HashNuke.elixir-stack | fetch mix dependencies] ************************
failed: [46.101.206.116] => {"ansible_job_id": "199182298910.9826", "changed": true, "cmd": ["bash", "-lc", "mix deps.get"], "delta": "0:00:02.278515", "end": "2015-07-31 14:32:42.087474", "finished": 1, "rc": 1, "start": "2015-07-31 14:32:39.808959", "warnings": []}
stderr: Looking up alternatives for conflicting requirements on phoenix_html
 Activated version: 1.4.0
 From phoenix_ecto v0.9.0: ~> 2.0
 From mix.exs: ~> 1.4
** (Mix) Hex dependency resolution failed, relax the version requirements or unlock dependencies
stdout: Running dependency resolution

It likely means that your developer machine (where you ran mix deps.get earlier) is running an old version of Hex, which has a bug that causes it to resolve phoenix_ecto to 0.9.x, even though mix.exs actually says {:phoenix_ecto, "~> 0.8"}.

To fix, run mix local.hex, clean up, and fetch dependencies again:

$ mix local.hex
Found existing archive(s): hex.ez.
Are you sure you want to replace them? [Yn] y
2015-07-31 22:10:33 URL:https://s3.amazonaws.com/s3.hex.pm/installs/1.0.0/hex.ez [269416/269416] -> "/home/johwar/.mix/archives/hex.ez" [1]
* creating /home/johwar/.mix/archives/hex.ez
$ rm -rf mix.lock deps _build
$ mix deps.get
Running dependency resolution
Dependency resolution completed successfully
  bbmustache: v1.0.1
  conform: v0.14.5
  cowboy: v1.0.2
  cowlib: v1.0.1
  ...

Finally, commit and push the changes to mix.lock, and deploy again.

Thanks to Victoria Wagman for spotting this!