---
tags: notes
---
# 學習 Ansible
[TOC]
## Ansible 簡介
Ansible 是一個容易上手的 IT 自動化引擎,可自動執行雲端配置、應用程式部署和許多其他 IT 需求。存放 yml 描述檔並執行 `ansible-playbook` 的機器稱為 Control Node,遠端的所有參與佈署的機器稱為 Managed Node。Ansible 這個詞起源於科幻小說 *Ender's Game* 中的一種超光速通訊裝置,用來控制遠方的星際戰艦。

Ansible 比起其他自動化部屬的工具有兩大特點:
- 在遠端只需要 python 環境與 ssh server,不需要其他 agent 也沒有額外的自定義安全基礎設施,因此易於部署。
- 它使用 YAML,以 Ansible Playbook 的形式,允許您以接近英語的方式描述您的自動化作業。

## Ansible 的術語:
- **Modules**
透過 Ansible 推送到各個節點的腳本稱為 "module",Ansible 預設會透過 ssh 來執行 module。通常你會透過現有的 module 來達成需求,但必要時也可以自己開發 module,開發的語言有 python、ruby、bash 等等。許多相關的 Module 集合起來會變成 Collection,例如 Azure.Azcollection。
- **Module utilities**
當多個 module 使用相同的程式碼時,Ansible 將這些函數存儲為 module utilities,以最大限度地減少重複和維護。module utilities 只能用 python 或 powershell 開發。
- **Plugins**
用來擴充 Ansible 的核心功能,如轉換資料、記錄輸出、連接到 inventory 等。Plugin 與 Module 不同的點在於 Module 定義了一個介面讓使用者自行設定一些參數,再根據輸入來對遠端做相應的操作;Plugin 主要執行在 Control Node,用來處理 Ansible 核心的輸入和輸出。
- **Inventory**
Ansible 在一個 INI、YAML 等紀錄它管理的 inventory,該文件讓你幫機器的 ip 分組,格式類似:
```
[webservers]
www1.example.com
www2.example.com
[dbservers]
db0.example.com
db1.example.com
```
在上方範例中,`[webservers]` 底下的成員就屬於 `webservers` 群組,此群組的成員可以共享一些設定,或是做其他進階的操作。
如果您的基礎設施中存在其他來源 (source) 例如 EC2、OpenStack 等,Ansible 也可以連接到該來源,從這些來源中動態的抓取更多 inventory、group、variable 的資訊。

- **Playbooks**
Playbook 可以精細地編排 (orchestrate) 基礎設設施,並非常詳細地控制一次要處理多少台機器。一個基礎設施可能持續使用很多年,因此 Ansible 盡量不推出新的特殊語法或功能,讓你不用為了維護久遠以前的基礎設施而花很多時間複習 Ansible 語法。
下方是一個簡單的 playbook:
```yml
---
- hosts: webservers
serial: 5 # update 5 machines at a time
roles:
- common
- webapp
- hosts: content_servers
roles:
- common
- content
```
- **Ansible search path**
如果您編寫自己的 Modules、Plugins 來擴展 Ansible 的核心功能,您可能在 Ansible 控制節點的不同位置有多個名稱相似或相同的檔案。search path 決定了 Ansible 將在指定的 playbook 使用哪些文件。當 Ansible 找到給定運行中包含的每個 playbook 和 role 時,它會將自動將可能有關的目錄附加到搜索路徑中,並自動載入找到的檔案。
Ansible 按以下幾種狀況自動載入 modules、Module utilities、plugins:
+ 如果您執行 `$ 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 的設定會依序從以下路徑載入:
```
ANSIBLE_CONFIG
./ansible.cfg
~/.ansible.cfg
/etc/ansible/ansible.cfg
```
## 安裝 Ansible
首先準備一個 Linux 系統和 python 環境,透過 pip 安裝:
```
$ pip install ansible
```
另外有 ansible-lint 會建議 ansible 寫法:
```
$ pip install ansible-lint
```
## Lab 1: 在 localhost 執行 Hello World
在 hello world 範例中,我只單純在 localhost 上示範一個簡單的 playbook,先不要管 inventory、roles 等用法。首先新增一個 `hellowrold.yml` 並貼上以下內容:
```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 }}"
```
- 第 1 行:`---` 是撰寫 yml 的慣例,表示文件的開頭,不寫這行 Ansible 還是能正常執行。
- 第 2 行:`names` 是這個 play 的名稱,我命名為 `"Play with Ansible"`。在此範例中,`name`、`hosts`、`tasks` 被稱為 [play keywords](https://docs.ansible.com/ansible/latest/reference_appendices/playbooks_keywords.html#play),Ansible 讀到這些 keyword 會做相應的動作,是比較需要背誦的部份。
- 第 3 行:`hosts` 是這個 play 的布屬對象 (target),是以逗點隔開的 hostname 或是 ip 位址。
- 第 4 行:`tasks` 是部屬對象所要執行的任務。
- 第 5 行:自訂任務的名稱。`name`、`register`、`changed_when` 被稱為 [task keywords](https://docs.ansible.com/ansible/latest/reference_appendices/playbooks_keywords.html#task)。
- 第 6 行:`ansible.builtin.command` 是一個 Ansible 內建的 module,它在所有布屬對象執行一個指令,在範例中我讓 `localhost` 執行 `df -h`。
- 第 7 行:`register` 會接收這個 task 的回傳訊息(即 `df -h` 的 stdout)並指派給 `"output"` 變數。以寫程式類比的話,相當於宣告了一個名為 `output` 的變數來儲存 `df -h` 的輸出。
- 第 8 行:`changed_when` 是特別用來表示 targets 執行完 task 後,狀態有沒有被「改變」,狀態是否被改變的判斷條件是由撰寫 playbook 的人自訂的,在此範例中我定義當 `df -h` 的回傳值為 0 時即代表機器的狀態被改變了。如果這個 task 完全不會影響機器的狀態,可以把這行改為 `change_when: false`。如果不寫這一行,Ansible 也能正常執行,不過 `change_when` 就永遠會是 `true`,而且會被 ansible-lint 要求補上這一行。
- 第 9 行:第二個任務。
- 第 10 行:用內建的 debug module 來輸出訊息到終端機。
- 第 11 行:輸出 `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` 並輸出結果在終端機上。
## Lab 2: 使用 inventory 連到遠端主機
首先準備兩個 linux 主機,我自己是用 Virtual Box 裝兩個 Alpine-Virt Linux,在虛擬機中安裝 `python3` 並允許 root 連線,然後用 Host-only 網路來模擬遠端主機。另外本機要安裝 `sshpass` 才能進行接下來的操作。
在目錄中新增一個名為 `inventory.ini` 的檔案:
```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` 或其他第三方軟體來保護密碼。
- 第 8 行透過 `:children` 語法來組合 group,將 `vboxservers` 和 `vboxhost` 共同歸類在 `allservers`。
- 第 12 行透過 `:vars` 來將重複使用的變數值一次指派給整個群組的機器。
透過 `$ ansible-inventory -i inventory.ini --list` 可以確認目前 inventory 的配置:
```json
{
"_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:
```cfg=
[defaults]
host_key_checking = False
```
接下來我將剛剛 helloworld.yml 的第 3 行 `hosts` 改為 `vboxservers`:
```yml=
---
- 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`。
## Vault
### Lab 3: 加密整份文件
Ansible Vault 可以加密 Ansible 使用的任何結構化檔案,預設的加密方法是 AES256。首先加密剛剛 inventory 練習中的 `helloworld.yml` 和 `inventory.ini`:
```
$ ansible-vault encrypt helloworld.yml inventory.ini
```
此時將 `helloworld.yml` 和 `inventory.ini` 打開,會看到以下已被加密的內容:
```yml=
$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
```
### Lab 4: vault id
當需要加密的內容變多,而且不同環境要用不同的密碼時,可以透過 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` 標籤:
```yml=
$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 來挑選正確的密碼。
### Lab 5: 加密單個秘密,並透過 group_var 引入
剛才介紹的方法都是對整個檔案進行加密,但是加密整個檔案後反而對閱讀造成困擾,通常我們只是想把單個字串隱藏起來而已。此時就要用 `encrypt_string`:
```
$ ansible-vault encrypt_string --vault-id foo@mypassword --name vboxservers_passwd xyz123
```
執行指令後,終端機會輸出被加密後的內容:
```yml
secret_var: !vault |
$ANSIBLE_VAULT;1.2;AES256;foo
66613064643635383932333531
...
```
將這整串被加密的變數複製進 `./group_var/vboxservers/foo_settings.yml` 如下:
```yml=
vboxservers_user: root
secret_var: !vault |
$ANSIBLE_VAULT;1.2;AES256;foo
66613064643635383932333531
...
```
將 `inventory.ini` 的檔案改寫如下:
```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
```
## Lab 6: Roles & Ansible Galaxy
Roles 允許您基於已知的目錄結構自動載入相關的變數、檔案、任務和其他 Ansible 物件,將上述物件按照 Role 分組後,您可以輕鬆地重複使用它們並共享到其他 Ansible 專案。
Asible 官方維護了一個 Roles 的市集叫做 Galaxy,在 Galaxy 中提供的 Role 原始碼放在 Github 上。`ansible-galaxy` 可以從 Galaxy 搜尋、安裝、移除 Roles,可以把它想像成 Ansible 用的 pip。
首先新增一個 `requirements.yml`,包含了安裝 nginx 和 dotnet 的 Roles:
```yml
- 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`,內容如下:
```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 資源
### Lab 7: 創建 Azure VM
按照[使用 Ansible 在 Azure 中建立 Linux 虛擬機器](https://learn.microsoft.com/zh-tw/azure/developer/ansible/vm-configure?tabs=ansible)在 Cloud Shell 或從自己的 Linux 機器創建一個 Azure VM。
注意 `admin_username`、`ssh_public_keys` 要替換成 Control Node 的資訊。
### Lab 8: 創建 Azure Function
```
$ ansible-galaxy collection install azure.azcollection
```
```yml
---
- 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
```
## 參考資料
- [Ansible architecture](https://docs.ansible.com/ansible/latest/dev_guide/overview_architecture.html)
- [How to build your inventory](https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html)
- [Encrypting content with Ansible Vault](https://docs.ansible.com/ansible/latest/user_guide/vault.html)
- [How To Use Ansible Vault to Protect Sensitive Playbook Data](https://www.digitalocean.com/community/tutorials/how-to-use-vault-to-protect-sensitive-ansible-data)
- [Best Practices](https://docs.ansible.com/ansible/2.8/user_guide/playbooks_best_practices.html)
- [[Ansible Tutorial] - Ansible Galaxy Tutorial](https://www.youtube.com/watch?v=7CGl82eVOO4)
- [azure.azcollection.azure_rm_functionapp module – Manage Azure Function Apps](https://docs.ansible.com/ansible/latest/collections/azure/azcollection/azure_rm_functionapp_module.html)
- [azure.azcollection.azure_rm_storageaccount module – Manage Azure storage accounts](https://docs.ansible.com/ansible/latest/collections/azure/azcollection/azure_rm_storageaccount_module.html)