Install Erlang, Elixir, Phoenix, and Nerves automatically to machines on macOS and Ubuntu by Ansible and Asdf
Tags: elixir, erlang, ansible
Published: 2022-01-24 21:13:38

Suppose you install Erlang, Elixir, Phoenix and Nerves to a machine on macOS or Ubuntu by Asdf. In that case, you may do it by your hand, following such articles like ["Perfect Steps of Installing Erlang and Elixir to Apple Silicon Mac."](https://dev.to/zacky1972/perfect-steps-of-installalling-erlang-and-elixir-to-apple-silicon-mac-2021-dec-edition-4iae) But if you do it to two or more machines? You may want to make such jobs automated. Ansible is such an approach.

This article will explain how we automatically install Erlang, Elixir, Phoenix, and Nerves to machines on macOS and Ubuntu by Ansible and Asdf.

Japanese edition is here: https://qiita.com/zacky1972/items/38a9ebb53bbc406fabb7

## Prerequisite

Suppose you have a host machine and one or more target machines. You should install Ansible on the host. Suppose the targets run on macOS or Ubuntu. And you should install Homebrew on them on macOS. Moreover, suppose you can log in to all targets by `ssh` with your public key and become an administrator with the same `sudo` password. Finally, suppose the hostnames of the targets are `target1, target2, ..., target9`.

## inventory.yml

You should write the information of the targets and common variables to `inventory/inventory.yml`:

```yaml
all:
  hosts:
    target[1:9]:
  vars:
    asdf: v0.8.1
    erlang: latest
    elixir: latest
    phoenix: latest
    nerves: latest
```

`target[1:9]` means `target1, target2, ..., target9`. You can rename it as you need. You can also specify each version for Asdf, Erlang, Elixir, Phoenix, and Nerves. In this case, the version of Asdf you will install is `v0.8.1`, and the versions of the others are the latest. You may specify older Erlang, Elixir, Phoenix, and Nerves versions.

Especially, you may install them to `localhost` as follows:

```yaml
all:
  hosts:
    localhost:
      ansible_host: "127.0.0.1"
  vars:
    asdf: v0.8.1
    erlang: latest
    elixir: latest
    phoenix: latest
    nerves: latest
```

If so, of course, you should enable to log in by `ssh` to `localhost`.

## ansible.cfg

To suppress warnings, you may write `ansible.cfg` as follows:

```
[defaults]
interpreter_python=/usr/bin/python3
```

## Tasks

For reusability, you can write Ansible tasks as components.

### Install Asdf for Ubuntu

`tasks/0010_install_asdf_linux.yml`:

```yaml
---
- block:
  - name: Install dependencies of asdf
    become: true
    apt:
      update_cache: yes
      cache_valid_time: 86400 # 1day
      name:
        - curl
        - git
      state: latest
  - name: Install asdf
    git:
      repo: https://github.com/asdf-vm/asdf.git
      dest: "{{ ansible_user_dir }}/.asdf"
      depth: 1
      version: "{{ asdf | quote }}"
    register: result
  - name: asdf update
    shell: "bash -lc 'cd {{ ansible_user_dir }}/.asdf && git pull'"
    ignore_errors: yes
    when: result is failed
  - name: set env vars
    lineinfile:
      dest: "{{ shrc }}"
      state: present
      line: "{{ item.line }}"
    with_items:
    - line: ". $HOME/.asdf/completions/asdf.{{ sh }}"
      regexp: '^ \. \$HOME/\.asdf/completions/asdf\.{{ sh }}'
    - line: '. $HOME/.asdf/asdf.sh'
      regexp: '^ \. \$HOME/\.asdf/asdf\.sh'
  when: ansible_system == 'Linux' and (ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian')
  vars:
    - shrc: "{{ ansible_user_dir | quote }}/.{{ ansible_user_shell | basename | quote }}rc"
    - sh: "{{ ansible_user_shell | basename | quote }}"
```

### Install Asdf for macOS

`tasks/0010_install_asdf_macos.yml`:

```yaml
---
- block:
  - name: install asdf by Homebrew
    community.general.homebrew:
      update_homebrew: true
      name:
        - asdf
  - name: set env vars (bash)
    lineinfile:
      dest: "{{ shprofile }}"
      state: present
      line: "{{ item.line }}"
    with_items:
    - line: ".  $(brew --prefix asdf)/etc/bash_completion.d/asdf.bash"
      regexp: '^ \. \$(brew --prefix asdf)/etc/bash_completion\.d/asdf\.bash'
    - line: '. $(brew --prefix asdf)/libexec/asdf.sh'
      regexp: '^ \. \$(brew --prefix asdf)/libexec/asdf\.sh'
    when: sh == 'bash'
  - name: set env vars (zsh)
    lineinfile:
      dest: "{{ shrc }}"
      state: present
      line: "{{ item.line }}"
    with_items:
    - line: ". $(brew --prefix)/share/zsh/site-functions"
      regexp: '^ \. \$(brew --prefix)/share/zsh/site-functions'
    - line: '. $(brew --prefix asdf)/libexec/asdf.sh'
      regexp: '^ \. \$(brew --prefix asdf)/libexec/asdf\.sh'
    when: sh == 'zsh'
  when: ansible_system == 'Darwin'
  vars:
    - shprofile: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename | regex_replace('$', '_') | regex_replace('zsh_', 'z') }}profile"
    - shrc: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename }}rc"
    - sh: "{{ ansible_user_shell | basename | quote }}"
```

### Install prerequisite libraries for Erlang on Ubuntu

`tasks/0011_install_erlang_prerequisite_linux.yml`:

```yaml
---
- block:
  - name: install prerequisite libraries for erlang 
    become: true
    apt:
      update_cache: yes
      cache_valid_time: 86400 # 1day
      state: latest
      name:
      - build-essential
      - autoconf
      - m4
      - libncurses5-dev
      - libwxgtk3.0-gtk3-dev
      - libgl1-mesa-dev
      - libglu1-mesa-dev
      - libpng-dev
      - libssh-dev
      - unixodbc-dev
      - xsltproc
      - fop
      - libxml2-utils
      - libncurses-dev
      - openjdk-11-jdk
  when: ansible_system == 'Linux' and (ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian')
```

 
### Install prerequisite libraries for Erlang on macOS

`tasks/0011_install_erlang_prerequisite_macos.yml`:

```yaml
---
- block:
  - name: install prerequisite libraries for erlang 
    community.general.homebrew:
      update_homebrew: true
      name:
        - autoconf
        - openssl@1.1
        - openssl@3
        - wxwidgets
        - libxslt
        - fop
        - openjdk
  when: ansible_system == 'Darwin'
```

### Install prerequisite libraries for Nerves on Ubuntu

`tasks/0013_install_nerves_prerequisite_linux.yml`:

```yaml
---
- block:
  - name: install prerequisite libraries for nerves
    become: true
    apt:
      update_cache: yes
      cache_valid_time: 86400 # 1day
      state: latest
      name:
      - automake
      - autoconf
      - git
      - squashfs-tools
      - ssh-askpass
      - pkg-config
      - curl
      - libssl-dev
      - libncurses5-dev
      - bc
      - m4
      - unzip
      - cmake
      - python
  when: ansible_system == 'Linux' and (ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian')
```

### Install prerequisite libraries for Nerves on macOS

`tasks/0013_install_nerves_prerequisite_macos.yml`:

```yaml
---
- block:
  - name: install prerequisite libraries for nerves 
    community.general.homebrew:
      update_homebrew: true
      name:
        - fwup 
        - squashfs
        - coreutils
        - xz
        - pkg-config
  when: ansible_system == 'Darwin'
```

### Install Erlang plugin

`tasks/0021_install_erlang_plugin.yml`:

```yaml
---
- block:
  - name: sh env
    ansible.builtin.shell:
    args:
      cmd: "{{ shenv_cmd }}"
      chdir: '{{ ansible_user_dir }}/'
    register: shenv
  - name: asdf plugin add erlang
    ansible.builtin.shell: |
      {{ source }} 
      asdf plugin add erlang
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    failed_when: result.rc != 0 and result.stderr | regex_search('(Plugin named .* already added)') == '' 
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  vars:
    - asdfsh: "{{ ansible_user_dir | quote }}/.asdf/asdf.sh"
    - profile: "{{ ansible_user_dir | quote }}/.profile"
    - shprofile: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename | regex_replace('$', '_') | regex_replace('zsh_', 'z') }}profile"
    - shrc: "{{ ansible_user_dir | quote }}/.{{ ansible_user_shell | basename  | quote }}rc"
    - shenv_cmd: "if [ -e {{ asdfsh }} ]; then echo '{{ asdfsh }}'; fi; if [ -e {{ shprofile }} ]; then echo '{{ shprofile }}'; fi; if [ -e {{ profile }} ]; then echo '{{ profile }}'; fi; if [ -e {{ shrc }} ]; then echo '{{ shrc }}'; fi"
```

### Install Elixir plugin

`tasks/0022_install_elixir_plugin.yml`:

```yaml
---
- block:
  - name: sh env
    ansible.builtin.shell:
    args:
      cmd: "{{ shenv_cmd }}"
      chdir: '{{ ansible_user_dir }}/'
    register: shenv
  - name: asdf plugin add elixir
    ansible.builtin.shell: |
      {{ source }} 
      asdf plugin add elixir
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    failed_when: result.rc != 0 and result.stderr | regex_search('(Plugin named .* already added)') == '' 
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  vars:
    - asdfsh: "{{ ansible_user_dir | quote }}/.asdf/asdf.sh"
    - profile: "{{ ansible_user_dir }}/.profile"
    - shprofile: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename | regex_replace('$', '_') | regex_replace('zsh_', 'z') }}profile"
    - shrc: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename }}rc"
    - shenv_cmd: "if [ -e {{ asdfsh }} ]; then echo '{{ asdfsh }}'; fi; if [ -e {{ shprofile }} ]; then echo '{{ shprofile }}'; fi; if [ -e {{ profile }} ]; then echo '{{ profile }}'; fi; if [ -e {{ shrc }} ]; then echo '{{ shrc }}'; fi"
```

### Install Erlang

`tasks/0101_install_erlang.yml`:

```yaml
---
- block:
  - name: sh env
    ansible.builtin.shell:
    args:
      cmd: "{{ shenv_cmd }}"
      chdir: '{{ ansible_user_dir }}/'
    register: shenv
  - name: asdf install erlang (for Linux)
    ansible.builtin.shell: |
      {{ source }} 
      asdf install erlang {{ erlang | quote }}
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    when: ansible_system == 'Linux'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  - name: show result
    debug:
      var: result
  - name: asdf install erlang (macOS OTP version 24.1.x or earlier)
    ansible.builtin.shell: |
      {{ source }} 
      {{ install_erlang_ssl_1_1 }}
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    when: (erlang != 'latest' and erlang is version_compare('24.2', '<')) and ansible_system == 'Darwin'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  - name: show result
    debug:
      var: result
    when: (erlang != 'latest' and erlang is version_compare('24.2', '<')) and ansible_system == 'Darwin'
  - name: asdf install erlang (macOS OTP 24.2 or later)
    ansible.builtin.shell: |
      {{ source }} 
      {{ install_erlang_ssl_3 }}
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    when: (erlang == 'latest' or (erlang is version_compare('24.2', '>='))) and ansible_system == 'Darwin'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  - name: show result
    debug:
      var: result
    when: (erlang == 'latest' or (erlang is version_compare('24.2', '>='))) and ansible_system == 'Darwin'
  - name: asdf global erlang
    ansible.builtin.shell: |
      {{ source }} 
      asdf global erlang {{ erlang | quote }}
    args:
      executable: '{{ ansible_user_shell }}'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  vars:
    - asdfsh: "{{ ansible_user_dir | quote }}/.asdf/asdf.sh"
    - profile: "{{ ansible_user_dir }}/.profile"
    - shprofile: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename | regex_replace('$', '_') | regex_replace('zsh_', 'z') }}profile"
    - shrc: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename }}rc"
    - shenv_cmd: "if [ -e {{ asdfsh }} ]; then echo '{{ asdfsh }}'; fi; if [ -e {{ shprofile }} ]; then echo '{{ shprofile }}'; fi; if [ -e {{ profile }} ]; then echo '{{ profile }}'; fi; if [ -e {{ shrc }} ]; then echo '{{ shrc }}'; fi"
    - install_erlang_ssl_1_1: "KERL_CONFIGURE_OPTIONS=\"--with-ssl=$(brew --prefix openssl@1.1) --with-odbc=$(brew --prefix unixodbc)\" CC=\"/usr/bin/gcc -I$(brew --prefix unixodbc)/include\" LDFLAGS=-L$(brew --prefix unixodbc)/lib asdf install erlang {{ erlang | quote }}"
    - install_erlang_ssl_3: "KERL_CONFIGURE_OPTIONS=\"--with-ssl=$(brew --prefix openssl@3) --with-odbc=$(brew --prefix unixodbc)\" CC=\"/usr/bin/gcc -I$(brew --prefix unixodbc)/include\" LDFLAGS=-L$(brew --prefix unixodbc)/lib asdf install erlang {{ erlang | quote }}"
```

### Install Elixir

`tasks/0102_install_elixir.yml`:

```yaml
---
- block:
  - name: sh env
    ansible.builtin.shell:
    args:
      cmd: "{{ shenv_cmd }}"
      chdir: '{{ ansible_user_dir }}/'
    register: shenv
  - name: asdf install elixir
    ansible.builtin.shell: |
      {{ source }}
      asdf install elixir {{ elixir | quote }}
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  - name: show result
    debug:
      var: result
  - name: asdf install elixir
    ansible.builtin.shell: |
      {{ source }}
      asdf global elixir {{ elixir | quote }}
    args:
      executable: '{{ ansible_user_shell }}'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  vars:
    - asdfsh: "{{ ansible_user_dir | quote }}/.asdf/asdf.sh"
    - profile: "{{ ansible_user_dir }}/.profile"
    - shprofile: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename | regex_replace('$', '_') | regex_replace('zsh_', 'z') }}profile"
    - shrc: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename }}rc"
    - shenv_cmd: "if [ -e {{ asdfsh }} ]; then echo '{{ asdfsh }}'; fi; if [ -e {{ shprofile }} ]; then echo '{{ shprofile }}'; fi; if [ -e {{ profile }} ]; then echo '{{ profile }}'; fi; if [ -e {{ shrc }} ]; then echo '{{ shrc }}'; fi"
```

### Install Phoenix

`tasks/0201_install_phoenix.yml`:

```yaml
---
- block:
  - name: sh env
    ansible.builtin.shell:
    args:
      cmd: "{{ shenv_cmd }}"
      chdir: '{{ ansible_user_dir }}/'
    register: shenv
  - name: install prerequisite
    ansible.builtin.shell: |
      {{ source }}
      mix local.rebar --force
      mix local.hex --force
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  - name: install Phoenix (latest)
    ansible.builtin.shell: |
      {{ source }}
      mix archive.install hex phx_new --force
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    when: phoenix == 'latest'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  - name: install Phoenix (not latest)
    ansible.builtin.shell: |
      {{ source }}
      mix archive.install hex phx_new {{ phoenix }} --force
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    when: phoenix != 'latest'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  vars:
    - asdfsh: "{{ ansible_user_dir | quote }}/.asdf/asdf.sh"
    - profile: "{{ ansible_user_dir }}/.profile"
    - shprofile: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename | regex_replace('$', '_') | regex_replace('zsh_', 'z') }}profile"
    - shrc: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename }}rc"
    - shenv_cmd: "if [ -e {{ asdfsh }} ]; then echo '{{ asdfsh }}'; fi; if [ -e {{ shprofile }} ]; then echo '{{ shprofile }}'; fi; if [ -e {{ profile }} ]; then echo '{{ profile }}'; fi; if [ -e {{ shrc }} ]; then echo '{{ shrc }}'; fi"
```

### Install Nerves

`tasks/0301_install_nerves.yml`:

```yaml
---
- block:
  - name: sh env
    ansible.builtin.shell:
    args:
      cmd: "{{ shenv_cmd }}"
      chdir: '{{ ansible_user_dir }}/'
    register: shenv
  - name: install Nerves (latest)
    ansible.builtin.shell: |
      {{ source }}
      mix local.rebar --force
      mix local.hex --force
      mix archive.install hex nerves_bootstrap --force
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    when: nerves == 'latest'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  - name: install Nerves (not latest)
    ansible.builtin.shell: |
      {{ source }}
      mix local.rebar --force
      mix local.hex --force
      mix archive.install hex nerves_bootstrap {{ nerves }} --force
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    when: nerves != 'latest'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  vars:
    - asdfsh: "{{ ansible_user_dir | quote }}/.asdf/asdf.sh"
    - profile: "{{ ansible_user_dir }}/.profile"
    - shprofile: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename | regex_replace('$', '_') | regex_replace('zsh_', 'z') }}profile"
    - shrc: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename }}rc"
    - shenv_cmd: "if [ -e {{ asdfsh }} ]; then echo '{{ asdfsh }}'; fi; if [ -e {{ shprofile }} ]; then echo '{{ shprofile }}'; fi; if [ -e {{ profile }} ]; then echo '{{ profile }}'; fi; if [ -e {{ shrc }} ]; then echo '{{ shrc }}'; fi"
```

## Playbooks

Then, you can assemble a playbook from the tasks. This section shows some samples.

### Install Asdf

`playbook/0010_install_asdf.yml`:

```yaml
- name: install asdf
  hosts: all
  tasks:
    - include_tasks: ../tasks/0010_install_asdf_linux.yml
      when: ansible_system == 'Linux' and (ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian')
    - include_tasks: ../tasks/0010_install_asdf_macos.yml
      when: ansible_system == 'Darwin'
```

### Install prerequisites of Erlang

`playbook/0011_install_erlang_prerequisite.yml`:

```yaml
- name: install prerequisites of erlang
  hosts: all
  tasks:
    - include_tasks: ../tasks/0011_install_erlang_prerequisite_linux.yml
      when: ansible_system == 'Linux' and (ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian')
    - include_tasks: ../tasks/0011_install_erlang_prerequisite_macos.yml
      when: ansible_system == 'Darwin'
```

### Install prerequisites of Nerves

`playbook/0013_install_nerves_prerequisite.yml`:

```yaml
- name: install prerequisites of nerves
  hosts: all
  tasks:
    - include_tasks: ../tasks/0013_install_nerves_prerequisite_linux.yml
      when: ansible_system == 'Linux' and (ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian')
    - include_tasks: ../tasks/0013_install_nerves_prerequisite_macos.yml
      when: ansible_system == 'Darwin'
```

### Install plugins

`playbook/0020_install_plugins.yml`:

```yaml
- name: install erlang/elixir plugins for asdf
  hosts: all
  tasks:
    - include_tasks: ../tasks/0021_install_erlang_plugin.yml
    - include_tasks: ../tasks/0022_install_elixir_plugin.yml
```

### Install Erlang and Elixir

`playbook/0100_install_erlang_elixir.yml`:

```yaml
- name: install erlang/elixir with asdf
  hosts: all
  tasks:
    - include_tasks: ../tasks/0101_install_erlang.yml
    - include_tasks: ../tasks/0102_install_elixir.yml
```

### Install Phoenix

`playbook/0200_install_phoenix.yml`:

```yaml
- name: install phoenix with asdf
  hosts: all
  tasks:
    - include_tasks: ../tasks/0201_install_phoenix.yml
```

### Install Nerves

`playbook/0300_install_nerves.yml`

```yaml
- name: install nerves with asdf
  hosts: all
  tasks:
    - include_tasks: ../tasks/0301_install_nerves.yml
```

## Usage

You can run the playbook as follows:

```sh
ansible-playbook -f (number of targets) -i (inventory file) (playbook file)
```

For example, you can install Erlang and Elixir to `target1, target2, ..., target9` as follows:

```sh
ansible-playbook -f 9 -i inventory/inventory.yml playbook/0100_install_erlang_elixir.yml
```

If you need to become the administrator when running the playbook, you should run it as follows:

```sh
ansible-playbook -f (number of targets) -i (inventory file) (playbook file) --ask-become-pass
```

For example, you can install Asdf to `target1, target2, ..., target9` when the targets include one or more Ubuntu machines as follows:

```sh
ansible-playbook -f 9 -i inventory/inventory.yml playbook/0010_install_asdf.yml --ask-become-pass
```