使用Ansible和Asdf在macOS和Ubuntu上自动安装Erlang、Elixir、Phoenix和Nerves的方法
当要在一台macOS或Ubuntu的PC上使用Asdf安装Erlang、Elixir、Phoenix和Nerves时,可以根据类似”M1 Mac上Elixir和Erlang的2021年12月最新安装指南”的文章手动安装。但是,如果要在两台或更多的PC上安装,可能会希望自动化这个过程。Ansible就是一个可以实现自动化的方法。
在本文中,我们将介绍如何使用 Ansible 和 Asdf 在 macOS 和 Ubuntu 的个人电脑上自动安装 Erlang、Elixir、Phoenix 和 Nerves。
这篇文章的中文译文是”使用Ansible和Asdf自动将Erlang、Elixir、Phoenix和Nerves安装到macOS和Ubuntu的机器上”。
这篇文章的前提
假设有一个主机和一个或多个目标机器。假设主机上已经安装了Ansible。目标机器可以是macOS或Ubuntu。同时,假设macOS目标机器上已经安装了Homebrew。假设所有目标机器可以使用公钥进行ssh登录,并且可以使用相同的密码在所有目标机器上sudo获取管理员权限。并且,目标机器的主机名分别是 target1, target2, …, target9。
库存清单.yml
将目标信息和共享变量写入 inventory/inventory.yml 文件中。
all:
hosts:
target[1:9]:
vars:
asdf: v0.8.1
erlang: latest
elixir: latest
phoenix: latest
nerves: latest
目标[1:9]表示target1、target2、…、target9,根据需要可以进行修改。可以分别指定Asdf、Erlang、Elixir、Phoenix、Nerves的版本。在这种情况下,Asdf的版本是v0.8.1,并且表明Asdf的其他版本是最新版。还可以将Erlang、Elixir、Phoenix、Nerves的版本指定为其他旧版本。
如果特别是在本地主机上安装的情况下,可以按照以下步骤进行。
all:
hosts:
localhost:
ansible_host: "127.0.0.1"
vars:
asdf: v0.8.1
erlang: latest
elixir: latest
phoenix: latest
nerves: latest
如果要在本地主机上安装,需要设置以使其可以通过ssh登录到本地主机。
ansible的配置文件
为了抑制警告,可以按照以下方式编写ansible.cfg。
[defaults]
interpreter_python=/usr/bin/python3
任务
为了再利用的目的,可以将Ansible任务作为组件进行描述。
安装Asdf到Ubuntu
---
- 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 }}"
在macOS上安装Asdf。
---
- 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 }}"
在Ubuntu上安装Erlang的前提库。
---
- 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')
在macOS上安装Erlang的前提库
---
- 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'
安装Ubuntu上的Nerves前提库。
---
- 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')
在macOS上安装Nerves的前置库。
---
- block:
- name: install prerequisite libraries for nerves
community.general.homebrew:
update_homebrew: true
name:
- fwup
- squashfs
- coreutils
- xz
- pkg-config
when: ansible_system == 'Darwin'
安装Erlang插件
---
- 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"
安装Elixir插件。
---
- 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"
安装 Erlang
---
- 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 }}"
安装Elixir
---
- 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"
安装Phoenix
-
- 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"
安装Nerves
---
- 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"
剧本
您可以根据这些任务构建Playbook。本节中会提供一些示例。
安装Asdf
- 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'
安装Erlang的前置库。
- 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'
安装 Nerves 的前提库。
- 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'
安装插件
- 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
安装Erlang和Elixir
- name: install erlang/elixir with asdf
hosts: all
tasks:
- include_tasks: ../tasks/0101_install_erlang.yml
- include_tasks: ../tasks/0102_install_elixir.yml
安装Phoenix
- name: install phoenix with asdf
hosts: all
tasks:
- include_tasks: ../tasks/0201_install_phoenix.yml
安装Nerves
- name: install nerves with asdf
hosts: all
tasks:
- include_tasks: ../tasks/0301_install_nerves.yml
怎么使用
按照以下步骤执行playbook。
ansible-playbook -f (ターゲット数) -i (inventoryファイル) (playbookファイル)
例如,要在target1、target2、…、target9上安装Erlang和Elixir,可以按如下步骤进行。
ansible-playbook -f 9 -i inventory/inventory.yml playbook/0100_install_erlang_elixir.yml
如果在执行playbook时需要管理员权限,请按照以下方式操作。
ansible-playbook -f (ターゲット数) -i (inventoryファイル) (playbookファイル) --ask-become-pass
例如,如果要将Asdf安装到target1、target2、…、target9之中,且目标中至少包含一台Ubuntu,则按照以下方式进行操作。
ansible-playbook -f 9 -i inventory/inventory.yml playbook/0010_install_asdf.yml --ask-become-pass