Deploying Elixir releases with Ansible
Elixir has been my go-to language for fun side projects for a while now. Most of the times I’m not using it, I start thinking “this would not have been a problem in Elixir”. But how do I deploy it?
As with everything else, I deploy my Elixir apps with Ansible, taking roughly the following steps:
- Installing Elixir, Erlang and other system package dependencies
- Get source code
- Install dependencies
- Build a release
- Run migrations and start the app
The tricky part is building the release without overwriting the previous release, which we will get into in a moment. Let’s first look over step one:
- name: install the erlang solutions apt key
apt_key:
id: 434975BD900CCBE4F7EE1B1ED208507CA14F4FCA
url: https://packages.erlang-solutions.com/debian/erlang_solutions.asc
state: present
- name: install the erlang solutions apt repository
apt_repository:
repo: deb https://packages.erlang-solutions.com/debian {{ ansible_distribution_release }} contrib
state: present
- name: install elixir and erlang dependencies
apt:
name:
- elixir
- erlang-asn1
- erlang-base
- erlang-crypto
- erlang-dev # yeccpre.hrl
- erlang-eunit # eunit_autoexport (prometheus)
- erlang-inets
- erlang-mnesia
- erlang-parsetools
- erlang-public-key
- erlang-runtime-tools
- erlang-ssl
- erlang-syntax-tools
state: present
Pretty standard stuff. Add Erlang application as you need them to the list of packages installed there.
Next up is cloning the source code, but here is where it gets special: I use the
deploy_helper
module to manage my build directory. The advantage is that I can
build releases into separate directories to keep them isolated whilst
maintaining dependencies and other compiled code in the same directory to keep
deploy times low. I use the default deploy_helper
configuration to simply
create a new release directory on each deployment (see the myapp_release
variable, you can combine this with your git
task), which allows even easier
(more human-readable) rollbacks in case things go south.
- name: set up the deployment directory
become: true
become_user: "{{ myapp_service_user }}"
deploy_helper:
release: "{{ myapp_version | default(omit) }}"
path: "{{ app_directory }}"
state: present
- name: checkout the source
become: true
become_user: "{{ myapp_service_user }}"
git:
dest: "{{ deploy_helper.shared_path }}/repository"
repo: "{{ myapp_repository_url }}"
version: "{{ myapp_version | default('master') }}"
umask: '0077'
notify:
- restart myapp
So far, so good. Let’s grab dependencies, build a release, and run migrations. An effort is made here to ensure that we only grab dependencies for production, to only report “changed” when we got new dependencies, compiled code or ran migrations, and
- name: download dependencies
become: true
become_user: "{{ myapp_service_user }}"
shell: umask 077 && exec /usr/bin/mix do local.hex --force, local.rebar --force, deps.get --only prod
args:
chdir: "{{ deploy_helper.shared_path }}/repository"
register: myapp_command_mix_deps_get
changed_when: "'All dependencies are up to date' not in myapp_command_mix_deps_get.stdout"
environment:
MIX_ENV: prod
- name: build a release
become: true
become_user: "{{ myapp_service_user }}"
shell: umask 077 && exec /usr/bin/nice /usr/bin/mix release --overwrite
args:
chdir: "{{ deploy_helper.shared_path }}/repository"
register: myapp_command_mix_release
changed_when: "'Compiling' in myapp_command_mix_release.stdout"
environment:
MIX_ENV: prod
- name: run migrations
become: true
become_user: "{{ myapp_service_user }}"
command: /usr/bin/mix ecto.migrate --all
args:
chdir: "{{ deploy_helper.shared_path }}/repository"
register: myapp_command_ecto_migrate
environment:
MIX_ENV: prod
PGSQL_URL: "{{ myapp_pgsql_url }}"
changed_when: "'Migrated' in myapp_command_ecto_migrate.stdout"
- name: copy release to current release directory
become: true
become_user: "{{ myapp_service_user }}"
copy:
src: "{{ deploy_helper.shared_path }}/repository/_build/prod/rel/myapp/"
remote_src: true
dest: "{{ deploy_helper.new_release_path }}"
- name: finalize the release
become: true
become_user: "{{ myapp_service_user }}"
deploy_helper:
path: "{{ myapp_directory }}"
release: "{{ deploy_helper.new_release }}"
state: finalize
Note that I use mix release --overwrite
here: after “overwriting” the previous
release, the new release is copied over into its own isolated directory.
The rest of my deployment role performs nothing special - it configures environment variables for the app, sets up the systemd service to run it, and starts & enables it (note handlers will restart it). If you want to read the full thing, you can check it out on my GitHub.
Keep on BEAMing!
reply via email