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." 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
:
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:
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
:
---
- 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
:
---
- 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
:
---
- 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
:
---
- 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
:
---
- 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
:
---
- 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
:
---
- 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
:
---
- 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
:
---
- 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
:
---
- 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
:
---
- 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
:
---
- 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
:
- 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
:
- 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
:
- 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
:
- 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
:
- 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
:
- name: install phoenix with asdf
hosts: all
tasks:
- include_tasks: ../tasks/0201_install_phoenix.yml
Install Nerves
playbook/0300_install_nerves.yml
- name: install nerves with asdf
hosts: all
tasks:
- include_tasks: ../tasks/0301_install_nerves.yml
Usage
You can run the playbook as follows:
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:
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:
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:
ansible-playbook -f 9 -i inventory/inventory.yml playbook/0010_install_asdf.yml --ask-become-pass
Top comments (0)