Ansible 是一個容易上手的 IT 自動化引擎,可自動執行雲端配置、應用程式部署和許多其他 IT 需求。存放 yml 描述檔並執行 ansible-playbook
的機器稱為 Control Node,遠端的所有參與佈署的機器稱為 Managed Node。Ansible 這個詞起源於科幻小說 Ender's Game 中的一種超光速通訊裝置,用來控制遠方的星際戰艦。
Ansible 比起其他自動化部屬的工具有兩大特點:
[webservers]
www1.example.com
www2.example.com
[dbservers]
db0.example.com
db1.example.com
[webservers]
底下的成員就屬於 webservers
群組,此群組的成員可以共享一些設定,或是做其他進階的操作。---
- hosts: webservers
serial: 5 # update 5 machines at a time
roles:
- common
- webapp
- hosts: content_servers
roles:
- common
- content
$ ansible-playbook /path/to/play.yml
,Ansible 會附加這些目錄:
path/to/modules
path/to/module_utils
path/to/plugins
play.yml
包含 - import_playbook: /path/to/subdir/play1.yml
,Ansible 會附加這些目錄:
path/to/subdir/modules
path/to/subdir/module_utils
path/to/subdir/plugins
role
來執行 play.yml
,Ansible 會附加這些目錄:
/path/to/roles/myrole/modules
/path/to/roles/myrole/module_utils
/path/to/roles/myrole/plugins
ansible.cfg
或環境變數中的特定目錄:
DEFAULT_MODULE_PATH
DEFAULT_MODULE_UTILS_PATH
DEFAULT_CACHE_PLUGIN_PATH
...
ANSIBLE_CONFIG
./ansible.cfg
~/.ansible.cfg
/etc/ansible/ansible.cfg
首先準備一個 Linux 系統和 python 環境,透過 pip 安裝:
$ pip install ansible
另外有 ansible-lint 會建議 ansible 寫法:
$ pip install ansible-lint
在 hello world 範例中,我只單純在 localhost 上示範一個簡單的 playbook,先不要管 inventory、roles 等用法。首先新增一個 hellowrold.yml
並貼上以下內容:
---
- name: Play with Ansible
hosts: localhost
tasks:
- name: Just execute df -h
ansible.builtin.command: df -h
register: output
changed_when: output.rc == 0
- name: Show stdout
ansible.builtin.debug:
msg: "{{ output.stdout_lines }}"
---
是撰寫 yml 的慣例,表示文件的開頭,不寫這行 Ansible 還是能正常執行。names
是這個 play 的名稱,我命名為 "Play with Ansible"
。在此範例中,name
、hosts
、tasks
被稱為 play keywords,Ansible 讀到這些 keyword 會做相應的動作,是比較需要背誦的部份。hosts
是這個 play 的布屬對象 (target),是以逗點隔開的 hostname 或是 ip 位址。tasks
是部屬對象所要執行的任務。name
、register
、changed_when
被稱為 task keywords。ansible.builtin.command
是一個 Ansible 內建的 module,它在所有布屬對象執行一個指令,在範例中我讓 localhost
執行 df -h
。register
會接收這個 task 的回傳訊息(即 df -h
的 stdout)並指派給 "output"
變數。以寫程式類比的話,相當於宣告了一個名為 output
的變數來儲存 df -h
的輸出。changed_when
是特別用來表示 targets 執行完 task 後,狀態有沒有被「改變」,狀態是否被改變的判斷條件是由撰寫 playbook 的人自訂的,在此範例中我定義當 df -h
的回傳值為 0 時即代表機器的狀態被改變了。如果這個 task 完全不會影響機器的狀態,可以把這行改為 change_when: false
。如果不寫這一行,Ansible 也能正常執行,不過 change_when
就永遠會是 true
,而且會被 ansible-lint 要求補上這一行。df -h
的每一行 stdout。透過 $ ansible-lint helloworld.yml
來檢查語法有無問題。
透過 $ ansible-playbook helloworld.yml
來執行 helloworld 輸出如下:
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [Play with Ansible] ***************************************************************************************************
TASK [Gathering Facts] *****************************************************************************************************
ok: [localhost]
TASK [Just execute df -h] **************************************************************************************************
changed: [localhost]
TASK [Show stdout] *********************************************************************************************************
ok: [localhost] => {
"msg": [
"Filesystem Size Used Avail Use% Mounted on",
"tmpfs 2.4G 3.1M 2.4G 1% /run",
"/dev/nvme1n1p4 232G 35G 185G 16% /",
"tmpfs 12G 112M 12G 1% /dev/shm",
"tmpfs 5.0M 4.0K 5.0M 1% /run/lock",
"tmpfs 12G 0 12G 0% /run/qemu",
"/dev/nvme0n1p1 256M 60M 197M 24% /boot/efi",
"tmpfs 2.4G 2.4M 2.4G 1% /run/user/1000",
"/dev/nvme0n1p5 239G 77G 162G 33% /media/lin/Data"
]
}
PLAY RECAP *****************************************************************************************************************
localhost : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
可以看到 Ansible 成功執行 df -h
並輸出結果在終端機上。
首先準備兩個 linux 主機,我自己是用 Virtual Box 裝兩個 Alpine-Virt Linux,在虛擬機中安裝 python3
並允許 root 連線,然後用 Host-only 網路來模擬遠端主機。另外本機要安裝 sshpass
才能進行接下來的操作。
在目錄中新增一個名為 inventory.ini
的檔案:
[vboxhost]
localhost ansible_user=lin ansible_password=mypassword
[vboxservers]
alpine1 ansible_host=192.168.56.101
alpine2 ansible_host=192.168.56.102
[allservers:children]
vboxservers
vboxhost
[vboxservers:vars]
ansible_user=root
ansible_password=mypassword
在這個檔案中我將兩台虛擬機分配在 vboxservers
、本機分配在 localservers
。並且透過 ansible_host
、ansible_user
、ansible_password
來設定 ssh 連線的變數。通常我們不會直接用 ansible_password
儲存密碼,而是用 ansible vault
或其他第三方軟體來保護密碼。
:children
語法來組合 group,將 vboxservers
和 vboxhost
共同歸類在 allservers
。:vars
來將重複使用的變數值一次指派給整個群組的機器。透過 $ ansible-inventory -i inventory.ini --list
可以確認目前 inventory 的配置:
{
"_meta": {
"hostvars": {
"alpine1": {
"ansible_host": "192.168.56.101",
"ansible_password": mypassword,
"ansible_user": "root"
},
"alpine2": {
"ansible_host": "192.168.56.102",
"ansible_password": mypassword,
"ansible_user": "root"
},
"localhost": {
"ansible_password": mypassword,
"ansible_user": "lin"
}
}
},
"all": {
"children": [
"allservers",
"ungrouped"
]
},
"allservers": {
"children": [
"vboxhost",
"vboxservers"
]
},
"vboxhost": {
"hosts": [
"localhost"
]
},
"vboxservers": {
"hosts": [
"alpine1",
"alpine2"
]
}
}
inventory 檔案可以有許多個,執行 ansible-playbook
時也可以引入多個 inventory,所以一般會將不同環境用到的設定分別放在不同的 inventory 檔案中。
然後在同樣的目錄中新增一個 ansible.cfg
,複製以下內容,讓 ansible 登入陌生的機器時自動儲存 finger print:
[defaults]
host_key_checking = False
接下來我將剛剛 helloworld.yml 的第 3 行 hosts
改為 vboxservers
:
---
- name: Use inventory
hosts: vboxservers
tasks:
- name: Just execute df -h
ansible.builtin.command: df -h
register: output
changed_when: output.rc == 0
- name: Show stdout
ansible.builtin.debug:
msg: "{{ output.stdout_lines }}"
此時目錄結構會長這樣:
.
├── ansible.cfg
├── hellowrod.yml
└── inventory.ini
最後,透過 $ ansible-playbook -i inventory.ini useinventory.yml
來載入 inventory.ini
並執行 helloworld.yml
:
PLAY [Use inventory] *********************************************************************************************************************************************************
TASK [Gathering Facts] *******************************************************************************************************************************************************
[WARNING]: Platform linux on host alpine2 is using the discovered Python interpreter at /usr/bin/python3.10, but future installation of another Python interpreter could
change the meaning of that path. See https://docs.ansible.com/ansible-core/2.13/reference_appendices/interpreter_discovery.html for more information.
ok: [alpine2]
[WARNING]: Platform linux on host alpine1 is using the discovered Python interpreter at /usr/bin/python3.10, but future installation of another Python interpreter could
change the meaning of that path. See https://docs.ansible.com/ansible-core/2.13/reference_appendices/interpreter_discovery.html for more information.
ok: [alpine1]
TASK [Just execute df -h] ****************************************************************************************************************************************************
changed: [alpine2]
changed: [alpine1]
TASK [Show stdout] ***********************************************************************************************************************************************************
ok: [alpine1] => {
"msg": [
"Filesystem Size Used Available Use% Mounted on",
"devtmpfs 10.0M 0 10.0M 0% /dev",
"shm 487.9M 0 487.9M 0% /dev/shm",
"/dev/sr0 149.0M 149.0M 0 100% /media/cdrom",
"tmpfs 487.9M 113.6M 374.2M 23% /",
"tmpfs 195.2M 120.0K 195.0M 0% /run",
"/dev/loop0 106.3M 106.3M 0 100% /.modloop"
]
}
ok: [alpine2] => {
"msg": [
"Filesystem Size Used Available Use% Mounted on",
"devtmpfs 10.0M 0 10.0M 0% /dev",
"shm 487.9M 0 487.9M 0% /dev/shm",
"/dev/sda3 5.8G 194.4M 5.3G 3% /",
"tmpfs 195.2M 120.0K 195.0M 0% /run",
"/dev/sda1 88.2M 24.3M 57.0M 30% /boot",
"tmpfs 487.9M 96.0K 487.8M 0% /tmp"
]
}
PLAY RECAP *******************************************************************************************************************************************************************
alpine1 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
alpine2 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
可以看到在兩台虛擬機上成功執行 df -h
。
Ansible Vault 可以加密 Ansible 使用的任何結構化檔案,預設的加密方法是 AES256。首先加密剛剛 inventory 練習中的 helloworld.yml
和 inventory.ini
:
$ ansible-vault encrypt helloworld.yml inventory.ini
此時將 helloworld.yml
和 inventory.ini
打開,會看到以下已被加密的內容:
$ANSIBLE_VAULT;1.1;AES256
376562366632666466386233376...
如果想解密檔案,則可以用 ansible-vault decrypt heelloworld.yml
。
單純想檢視檔案,則可以用 ansible-vault view heelloworld.yml
。
接下來透過 Ansible Playbook 解密並執行 helloworld.yml
:
$ ansible-playbook --ask-vault-pass -i inventory.ini helloworld.yml
或是先把密碼存在一個檔案,比如 mypassword
,然後從該檔案讀取密碼來解密:
$ echo 12345678 > mypassword
$ ansible-playbook --vault-password-file mypassword -i inventory.ini helloworld.yml
當需要加密的內容變多,而且不同環境要用不同的密碼時,可以透過 vault id 來管理這些秘密內容。vault id 只會為加密的內容附上一個標籤,並不會影響加解密的流程,所以用 vault id 加密的內容仍然可以透過前面提到的 --vault-password-file
解密。
用 vault id 加密的,但是可以幫助你使用以下選項來設定:
--vault-id label@source ...
label 是我們自自訂的標籤,例如 dev
、app
等。source 是讀取密碼的來源,可以從 stdin 輸入(prompt
)、或是從檔案(path/to/file
)輸入、或是從符合官方規範的腳本(path/to/client.py
)輸入。
首先透過 vault id 加密剛剛 inventory 練習中的 helloworld.yml 和 inventory.ini,給予 foo
標籤並使用 mypassword
檔案中的密碼加密:
$ echo 12345678 > mypassword1
$ echo 12341234 > mypassword2
$ ansible-vault encrypt --vault-id foo@mypassword1 inventory.ini
$ ansible-vault encrypt --vault-id bar@mypassword2 helloworld.yml
此時將 inventory.ini
打開,會看到被加密的內容有一個 foo
標籤:
$ANSIBLE_VAULT;1.2;AES256;foo
633364633161343033343161376666
接下來透過 Ansible Playbook 解密各個 vault-id 和密碼檔案並執行 helloworld.yml
:
$ ansible-playbook --vault-id foo@mypassword1 \
--vault-id bar@mypassword2 \
-i inventory.ini helloworld.yml
或是也可以不用輸入 vault-id 的資訊,讓 ansible 自行嘗試解密:
$ ansible-playbook --vault-id mypassword1 \
--vault-id mypassword2 \
-i inventory.ini helloworld.yml
從這個練習中可以發現,從 stdin 或檔案中讀取多個不同密碼,需要針對不同的密碼一一輸入或是填入 ansible-playbook 指令的 --vault-id
欄位,而使用腳本則可以自動根據不同的 label 來挑選正確的密碼。
剛才介紹的方法都是對整個檔案進行加密,但是加密整個檔案後反而對閱讀造成困擾,通常我們只是想把單個字串隱藏起來而已。此時就要用 encrypt_string
:
$ ansible-vault encrypt_string --vault-id foo@mypassword --name vboxservers_passwd xyz123
執行指令後,終端機會輸出被加密後的內容:
secret_var: !vault |
$ANSIBLE_VAULT;1.2;AES256;foo
66613064643635383932333531
...
將這整串被加密的變數複製進 ./group_var/vboxservers/foo_settings.yml
如下:
vboxservers_user: root
secret_var: !vault |
$ANSIBLE_VAULT;1.2;AES256;foo
66613064643635383932333531
...
將 inventory.ini
的檔案改寫如下:
[vboxhost]
localhost ansible_user=root ansible_password=123456
[vboxservers]
alpine1 ansible_host=192.168.56.101
alpine2 ansible_host=192.168.56.102
[allservers:children]
vboxservers
vboxhost
[vboxservers:vars]
ansible_user={{ vboxservers_user }}
ansible_password={{ vboxservers_passwd }}
此時整個專案的目錄會長這樣:
.
├── ansible.cfg
├── group_vars
│ └── vboxservers
│ └── foo_settings.yml
├── helloworld.yml
├── inventory.ini
└── mypassword
請注意不管加密或是明文的自訂變數,都一定要放在 ./group_var/vboxservers/
目錄裡,因為 Ansible 是靠相對路徑來找變數。以這個練習為例,vboxservers
群組中的自訂變數會從 group_vars
去尋找 vboxservers
目錄,而 foo_settings.yml
這個檔案可以隨意命名。
最後用以下指令讓 Ansible Playbook 解密並執行 helloworld.yml
:
$ ansible-playbook --vault-id foo@mypassword -i inventory.ini helloworld.yml
Roles 允許您基於已知的目錄結構自動載入相關的變數、檔案、任務和其他 Ansible 物件,將上述物件按照 Role 分組後,您可以輕鬆地重複使用它們並共享到其他 Ansible 專案。
Asible 官方維護了一個 Roles 的市集叫做 Galaxy,在 Galaxy 中提供的 Role 原始碼放在 Github 上。ansible-galaxy
可以從 Galaxy 搜尋、安裝、移除 Roles,可以把它想像成 Ansible 用的 pip。
首先新增一個 requirements.yml
,包含了安裝 nginx 和 dotnet 的 Roles:
- src: nginxinc.nginx
version: 0.23.2
- src: andrewrothstein.dotnet
version: v3.1.0
執行以下指令,從 Ansible Galaxy 下載所有相依的 Roles:
$ ansible-galaxy install -r requirements.yml -p roles
新增一個 main.yml
,內容如下:
---
- name: Setup Nginx using roles
hosts: alpine1
become: True
roles:
- nginxinc.nginx
- name: Setup dotnet
hosts: alpine1
become: True
roles:
- role: andrewrothstein.dotnet
vars:
dotnet_install_type: sdk
dotnet_ver: 3.1.402
dotnet_subdirs:
linux-x64: 'pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d'
linux-musl-x64: 'pr/e301fc5c-c8dd-4f8e-94ee-d19f3caf508f/a4191801aeb8cd813cf7057ac4d936a0'
osx-x64: 'pr/ac399dfa-04e1-49cf-be75-7112a9eec68f/60b1ca435b12e7b8beb6bb39b9cdf1c6'
dotnet_checksums:
linux-x64: sha512:42154efb5ad66ae3dcc300b2c0573a9537dd916fc48cbae92885a63a0b6d7f7c3a4366ca2298107783bc1f1913328f35e778dcda378da276cff3b8269495d5be
linux-musl-x64: sha512:30916407ee1f99c0f1398a45aa1a480b6d75c5e42488c877b7879ea68a03de07b29943e89e9324c3b14df4ca1d2723116a5c4812b2265cbb103488706aa56b70
osx-x64: sha512:68b5ecc76b588399d4f5bcc123caf0c1c1f26625bf21731737f004886f665ebe6559e9cc77f1265c678508726025f66862fb901ae95a25265bc3da4bed69335f
此時整個專案的目錄會長這樣:
.
├── ansible.cfg
├── group_vars
│ └── vboxservers
├── inventory.ini
├── main.yml
├── mypassword
├── requirements.yml
└── roles
├── andrewrothstein.dotnet
├── andrewrothstein.unarchive-deps
└── nginxinc.nginx
最後執行 Ansible Playbook:
$ ansible-playbook --vault-id mypassword -i inventory.ini main.yml
按照使用 Ansible 在 Azure 中建立 Linux 虛擬機器在 Cloud Shell 或從自己的 Linux 機器創建一個 Azure VM。
注意 admin_username
、ssh_public_keys
要替換成 Control Node 的資訊。
$ ansible-galaxy collection install azure.azcollection
---
- name: Create Azure Function
hosts: localhost
connection: local
tasks:
- name: Create a resource group
azure_rm_resourcegroup:
name: myResourceGroup
location: japaneast
- name: Create a storage account
azure_rm_storageaccount:
resource_group: myResourceGroup
name: jacktest0101
type: Standard_LRS
- name: Create a function app with app settings
azure_rm_functionapp:
resource_group: myResourceGroup
name: jacktestfunc0101
storage_account: jacktest0101
app_settings:
FUNCTIONS_WORKER_RUNTIME: dotnet