專題 - 使用 Ansible 進行快速佈署 - Playbook 初探
上次更新日期 2020/11/07
Ansible 可以達成快速佈署,而且也能減輕 IT 人員的負擔。但是,如果依據前一章的設定,單純透過 ad hoc 單一指令來運作的話,那麼不就還得要重新撰寫 document?因為這些指令的內容還是得要記憶,對吧?這樣不好。 所以,就像 shell scripts 一樣,將指令寫成批次檔的概念,我們將 ad hoc 的內容寫成名為 playbook 的樣式, 讓 ansible 根據 playbook 去執行即可喔!未來要執行資料時,將 playbook 改一改,執行下去,全部受管控的 managed hosts 就全部更新了!方便快速得很!
Playbook 與 YAML 語法
- 什麼是 playbook 呢?
- 就像 shell script 簡單的說,可以將一系列的指令彙整成為類似批次檔一樣。 playbook 可以將前一章提到的 ansible ad hoc 指令, 彙整到一個文字檔,然後透過『 ansible-playbook filename.yml 』的方式,來將該動作執行完畢。而 playbook 又是純文字檔, 相當方便修改、彙整、紀錄等。
- 前一章有個 ansible ad hoc 內容是這樣:
$ ansible webserver1 -m copy -a 'src=/etc/hosts dest=/etc/hosts'
你可以將這一行改寫成為底下的模樣:--- - name: copy /etc/hosts to managed hosts hosts: webserver1 tasks: - name: copy /etc/hosts to another hosts copy: src: /etc/hosts dest: /etc/hosts
上面這個就是『 YAML 』格式的 playbook 內容!
- YAML 格式:
- 基本上, YAML 格式的要求是:『同一個階層的設定,需要使用相同的縮排』,至於縮排幾個空白字元,就沒有特別規定。
所以,簡單的說, YAML 對於縮排的需求有兩個基本的規則,一定要符合才行:
- 每一個不同元素,只要其階層相同,那縮排必須要相同。如同上面的範例中,name, hosts, tasks 三個元素的階層相同, 所以縮排就一定要相同。
- 子元件必須要有更多的縮排,其縮排空白必須要比父元件多。以上面的 copy 為例,他是 tasks 的子元件, 所以就一定要在 tasks 有更多的縮排才行!
- 為符合 yaml 的格式設計,如果使用 vim 這個程式編輯器,那你可以在 ~/.vimrc 裡面增加這一行,讓 vim 偵測到副檔名為 .yml 時,
就自動以 2 個字元替代 tab 按鍵的產生:
$ whoami $ vim ~/.vimrc autocmd FileType yaml setlocal ai ts=2 sw=2 et
如此一來,未來我們就可以直接使用 [tab] 來達成對齊之外,編輯的時候, .yml 格式也會自動縮排對齊!
- 基本上, YAML 格式的要求是:『同一個階層的設定,需要使用相同的縮排』,至於縮排幾個空白字元,就沒有特別規定。
所以,簡單的說, YAML 對於縮排的需求有兩個基本的規則,一定要符合才行:
- playbook 格式:
- 除了 Yaml 的格式之外,playbook 也有其特定的格式喔!剛剛看到的 playbook 就是一個基礎格式,我們可以分析一下上面的檔案,
你會發現到幾個基本的規則:
- 整個 playbook 的運作開頭是由三個減號 (---) 開始的。
- 之後第一個 play 接一個減號與一個空白 (- ),注意到是減號與空白喔!
- 通常每個 play 會有三個指標 (keys),分別是 name, hosts, tasks。
- 任務 (tasks) 底下可能會有好幾個連續的動作,每個動作同樣都以減號空白 (- ) 為開始, 然後接著模組名稱,在模組名稱底下則帶入該模組所需要的參數。
- name 的功能: (其實,基本功能就是標題, label 的功能!)
- 通常每個 playbook 可能會有好幾個 play,每個 play 裡面可能又有好幾個任務 (tasks),每個任務裡面都有獨自的模組與參數這樣。
- 那如果以 ansible ad hoc 的角度來看,我們就是反覆不斷的執行指令而已。
- playbook 為了讓大家知道該指令的功能是什麼,或者是該任務 (tasks) 的任務是什麼,因此給了 name 的參數, 後面接的訊息,就是執行 playbook 時,會提供給 IT 人員查看該動作意義的訊息。
- 在 playbook 中,name 雖然是非必要的,但是 ansible 建議一定要寫!這才未來執行,才知道那是啥東西!
- hosts 的功能:很簡單啊!就是 hosts/group 主機名稱列表的主機名稱群!
- tasks 的功能:就是許多任務
- 每一個任務依舊使用 (- ) 開頭,接的第一個指標通常也就是 name,紀錄該任務的功能說明
- 接下來就是模組名稱,然後在模組名稱底下,就是該模組的參數, 也就是透過 ansible-doc 查到的參數與參數值,中間都用冒號 (:) 隔開而已。
- 除了 Yaml 的格式之外,playbook 也有其特定的格式喔!剛剛看到的 playbook 就是一個基礎格式,我們可以分析一下上面的檔案,
你會發現到幾個基本的規則:
- 撰寫與執行第一支 playbook:
- 撰寫 playbook:
- 注意,要將 playbook 放置到個別的工作目錄裡面比較好喔!
- 注意,副檔名是 .yml 喔!
- 開始撰寫 copy_hosts.yml 這個 playbook 內容看看:
$ whoami; pwd student /home/student/ansible-init $ vim copy_hosts.yml --- - name: copy /etc/hosts to managed hosts hosts: webserver1 tasks: - name: copy /etc/hosts to another hosts copy: src: /etc/hosts dest: /etc/hosts
- 檢查 playbook 是否有語法方面的問題:
$ ansible-playbook --syntax-check copy_hosts.yml ERROR! We were unable to read either as JSON nor YAML, these are the errors we got from each: JSON: Expecting value: line 1 column 1 (char 0) Syntax Error while loading YAML. mapping values are not allowed in this context The error appears to be in '/home/student/ansible-init/copy_hosts.yml': line 3, column 11, but maybe elsewhere in the file depending on the exact syntax problem. The offending line appears to be: - name: copy /etc/hosts to managed hosts hosts: webserver1 ^ here
如上所示,最後 3 行顯示錯誤的地方,原來是 hosts 的縮排多了一個空白字元的結果。改完之後重複執行一次, 系統就會提示沒問題了! - 測試一下僅執行環境檢查的 dry run 結果,此動作僅檢查環境,不會實際運作喔!
$ ansible-playbook -C copy_hosts.yml PLAY [copy /etc/hosts to managed hosts] ********************************************* TASK [Gathering Facts] ************************************************************** ok: [webserver1] TASK [copy /etc/hosts to another hosts] ********************************************* ok: [webserver1] PLAY RECAP ************************************************************************** webserver1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
你會發現到中括號 [] 裡面,大部分就是 name 設計的名稱!這樣真的比較好理解工作項目! 所以, name 要寫清楚一點喔!最後一行則是顯示執行的結果,大部分都是 ok 的! - 實際執行此 playbook:
$ ansible-playbook copy_hosts.yml
結果會跟上面一樣,只是,這次就真的是執行而不是僅檢查環境而已!
- 撰寫 playbook:
- 實做練習:
- 模仿上面的 playbook 設定,然後翻到前一章,找到新增用戶的指令如下:
python3 -c "from passlib.hash import sha512_crypt; \ import getpass; \ print(sha512_crypt.using(rounds=5000).hash(getpass.getpass()))" Password: $6$I9E8lDkx9ThYK5y0$c7icc4zDLaLyBE3zacseBQeKBmkhUpNTUC.0RrK/6Od... ansible webserver1 -m user -a 'user=demouser uid=3000 password=$6$I9E8lDkx9T..'
- 現在,新增一個新的用戶,名稱為 myuser1,uid 為 3101,密碼為加密過後的 itismyuser。 請將該設定值寫成名為 user_add.yml 的 playbook
- 嘗試檢查語法,若沒問題,就直接執行這個 playbook 看看
- 使用 ansible ad hoc 的指令功能,使用 id 檢查一下這個帳號 myuser1 是否已經存在其系統中了?
- 模仿上面的 playbook 設定,然後翻到前一章,找到新增用戶的指令如下:
一個 playbook 使用範例:啟動 httpd 服務
- 範例規劃:安裝 http 以及處理首頁檔案
- 管理員大概很常被要求要幫用戶處理 web server 的啟動與首頁的製作。大致上安裝、啟動、開機啟動、設置首頁檔案、防火牆的設定流程是不變的。 我們可以透過 playbook 來設定好這些資料!
- 安裝的流程應該是使用 yum 模組的,安裝的軟體為 httpd,使用底下的方式找到 yum 的參數:
$ ansible-doc yum ..... - name A package name or package specifier with version, like `name-1.0'. ..... - state Whether to install (`present' or `installed', `latest'), or remove (`absent' or `removed') a package. `present' and `installed' will simply ensure that a desired package is installed. `latest' will update the specified package if it's not of the latest available version. `absent' and `removed' will remove the specified package. Default is `None', however in effect the default action is `present' unless the `autoremove' option is enabled for this module, then `absent' is inferred. .....
通常會使用 latest 強制安裝到最新,至於 installd 與 present 則是要求要有安裝。 如果要移除,才使用 removed 。 - 啟動與開機啟動需要透過 service 模組的支援,所以檢查 service 的參數:
$ ansible-doc service ..... - enabled Whether the service should start on boot. ..... = name Name of the service. ..... - state `started'/`stopped' are idempotent actions that will not run commands unless necessary. `restarted' will always bounce the service. `reloaded' will always reload.
是否要開機啟動,透過 enabled 為 true 或 false 來設計,至於 state 則是立刻啟動/關閉/重新啟動的設計。 - 修改網頁,如果是新的網頁,然後只具有一兩行的話,可以使用 copy 的 content 內容來處理即可。
- 開始撰寫 playbook :
- 假設檔名設定為 web_first.yml 好了:
$ whoami; pwd student /home/student/ansible-init $ vim web_first.yml --- - name: setup web server and add first web page hosts: webserver1 tasks: - name: install httpd packages yum: name: httpd state: latest - name: start and enable httpd service service: name: httpd enabled: true state: started - name: create first web page for index.html copy: content: 'name: vbird tsai\nID: g090axxx\n' dest: /var/www/html/index.html
- 開始檢查語法是否有問題:
$ ansible-playbook --syntax-check web_first.yml playbook: web_first.yml
- 測試執行檢查環境:
$ ansible-playbook -C web_first.yml PLAY [setup web server and add first web page] ************************************** TASK [Gathering Facts] ************************************************************** ok: [webserver1] TASK [install httpd packages] ******************************************************* changed: [webserver1] TASK [start and enable httpd service] *********************************************** changed: [webserver1] TASK [create first web page for index.html] ***************************************** changed: [webserver1] PLAY RECAP ************************************************************************** webserver1 : ok=4 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
這代表指令執行應該會成功 (ok=4),而且可能會有改變任務的項目有三個 (chaged=3)。
- 假設檔名設定為 web_first.yml 好了:
- 執行、假查與測試:
- 開始執行 playbook 囉!
$ ansible-playbook web_first.yml PLAY [setup web server and add first web page] ************************************** TASK [Gathering Facts] ************************************************************** ok: [webserver1] TASK [install httpd packages] ******************************************************* changed: [webserver1] TASK [start and enable httpd service] *********************************************** changed: [webserver1] TASK [create first web page for index.html] ***************************************** changed: [webserver1] PLAY RECAP ************************************************************************** webserver1 : ok=4 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 $ ansible webserver1 -m command -a 'cat /var/www/html/index.html' webserver1 | CHANGED | rc=0 >> name: vbird tsai\nID: g090axxx\n
這樣看起來,確實是有成功的執行了將首頁貼上去的功能! - 使用 curl 檢查一下網頁囉!
$ curl webserver1 curl: (7) Failed to connect to webserver1 port 80: No route to host $ ansible webserver1 -m command -a 'systemctl status httpd' webserver1 | CHANGED | rc=0 >> ● httpd.service - The Apache HTTP Server Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled;vendor preset:disabled) Active: active (running) since Sun 2020-10-25 00:12:09 CST; 2min 53s ago Docs: man:httpd.service(8) Main PID: 28961 (httpd) Status: "Running, listening on: port 80" Tasks: 213 (limit: 11482) Memory: 39.1M CGroup: /system.slice/httpd.service ├─28961 /usr/sbin/httpd -DFOREGROUND ├─28962 /usr/sbin/httpd -DFOREGROUND ├─28963 /usr/sbin/httpd -DFOREGROUND ├─28964 /usr/sbin/httpd -DFOREGROUND └─28965 /usr/sbin/httpd -DFOREGROUND $ ansible webserver1 -m command -a 'firewall-cmd --list-all' webserver1 | CHANGED | rc=0 >> public (active) target: default icmp-block-inversion: no interfaces: ens3 sources: services: cockpit dhcpv6-client ssh ports: protocols: masquerade: no forward-ports: source-ports: icmp-blocks: rich rules:
連不上 httpd 呢!原來是防火牆搞的鬼!我們都忘記啟動用戶端的防火牆 http 了!
- 開始執行 playbook 囉!
- 增加任務與重複執行 playbook:
- 我們忘記加上 managed host 防火牆,所以現在要補強!防火牆使用 firewalld 模組:
$ ansible-doc firewalld ..... - immediate Should this configuration be applied immediately, if set as permanent. [Default: False] type: bool ..... - permanent Should this configuration be in the running firewalld configuration or persist across reboots. [Default: (null)] type: bool ..... - service Name of a service to add/remove to/from firewalld. The service must be listed in output of firewall-cmd --get- services. [Default: (null)] type: str ..... = state Enable or disable a setting. For ports: Should this port accept (enabled) or reject (disabled) connections. The states `present' and `absent' can only be used in zone level operations (i.e. when no other parameters but zone and state are set). (Choices: absent, disabled, enabled, present) type: str .....
我們目前只需要啟動 http 而已,所以可以改寫 web_first.yml 檔案了: - 改寫 web_first.yml 檔案:
$ vim web_first.yml ..... - name: add http port to firewalld firewalld: immediate: true permanent: true service: http state: enabled $ ansible-playbook --syntax-check web_first.yml playbook: web_first.yml $ ansible-playbook web_first.yml PLAY [setup web server and add first web page] ************************************** TASK [Gathering Facts] ************************************************************** ok: [webserver1] TASK [install httpd packages] ******************************************************* ok: [webserver1] TASK [start and enable httpd service] *********************************************** ok: [webserver1] TASK [create first web page for index.html] ***************************************** ok: [webserver1] TASK [add http port to firewalld] *************************************************** changed: [webserver1] PLAY RECAP ************************************************************************** webserver1 : ok=5 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 $ curl webserver1 name: vbird tsai\nID: g090axxx\n
這樣就搞定了!上面看起來有被更動的,也只有最後一個防火牆而已,因此就變成了 chaged 囉!
- 我們忘記加上 managed host 防火牆,所以現在要補強!防火牆使用 firewalld 模組:
- 實做練習:想要在 /etc/hosts 裡面『增加』一行字。
- 因為是『增加』一行字,不是替代文字,因此,不能使用 copy 模組。此時,還有另外一個模組, 嘗試使用『 lineinfile 』模組,請自行查詢這個模組的可用參數。
- 建立名為 hosts_modify.yml 的檔案,這個 playbook 的重點是:將『 172.16.200.254 bigdata 』加入 /etc/hosts 內。
- 執行該 playbook 去影響 webserver1 主機
- 使用 ansible ad hoc 的功能,去看看 /etc/hosts 有沒有成功的加入了該行資料?
- 重複執行該 playbook,該行字會不會被持續加入呢?
Playbook 與 YAML 可用額外語法
- 執行 playbook 的額外資訊:
- ansible-playbook 如果要增加額外的訊息,可以加上 -v 這個參數喔:
- -v :任務 (task) 的結果會顯示出來
- -vv :設定資料與結果都會顯示出來
- -vvv :包含連線到 managed hosts 之間的網路連線資訊也會顯示
- -vvvv :更多額外的外掛資訊等,都會顯示喔!
- 測試執行 copy_hosts 的結果:
$ ansible-playbook -vv copy_hosts.yml ansible-playbook 2.9.14 config file = /home/student/ansible-init/ansible.cfg configured module search path = ['/home/student/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules'] ansible python module location = /usr/lib/python3.6/site-packages/ansible executable location = /usr/bin/ansible-playbook python version = 3.6.8 (default, Apr 16 2020, 01:36:27) [GCC 8.3.1 20191121 (Red Hat 8.3.1-5)] Using /home/student/ansible-init/ansible.cfg as config file PLAYBOOK: copy_hosts.yml ************************************************************ 1 plays in copy_hosts.yml PLAY [copy /etc/hosts to managed hosts] ********************************************* TASK [Gathering Facts] ************************************************************** task path: /home/student/ansible-init/copy_hosts.yml:2 ok: [webserver1] META: ran handlers TASK [copy /etc/hosts to another hosts] ********************************************* task path: /home/student/ansible-init/copy_hosts.yml:5 changed: [webserver1] => {"changed": true, "checksum": "423f985239e1e7eb5d52becf62da d1fac3e3b475", "dest": "/etc/hosts", "gid": 0, "group": "root", "md5sum": "677944b 84b84bd729177b22a039b94c2", "mode": "0644", "owner": "root", "secontext": "system_u:object_r:net_conf_t:s0", "size": 344, "src": "/home/sysadm/.ansible/tmp/ ansible-tmp-1603562867.5132856-62179-209476908331227/source", "state": "file", "uid" : 0} META: ran handlers META: ran handlers PLAY RECAP ************************************************************************** webserver1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
比較重要的是 chaged 那一段,可以看到內容就有如 ansible ad hoc 執行的訊息顯示流程! 私訊非常完整。同時注意,該段訊息是以大括號 { } 將訊息包起來的喔!
- ansible-playbook 如果要增加額外的訊息,可以加上 -v 這個參數喔:
- YAML 的額外語法說明
- 除了之前談到的縮排之外,如果 yuml 需要註解,可以在任何地方增加 # 即可在後面加入註解:
# 這是註解 -name: add file # 這也可以是註解
- YAML 的字串,通常不用設定啥,就是字串了!只是,如果你需要定義更清楚,可以使用單、雙引號處理:
this is string words. 'this is string words.' "this is string words."
上面的語法都是被接受的! - 如果有一行內容文字太長,你可能要分成好幾行,這樣比較好閱讀。這時,你可以使用兩個分隔字元來處理看看:
- name: This playbook will treate your managed hosts for | its http service and php language and mysql SQL server. hosts: webserver1 ...
如果使用管線符號 (|) 來處理,則底下的文字會主動貼到上面這行來。 要注意的是,playbook 主要判斷內容的依據是冒號 (:),所以底下幾行都會黏到第一行去!第二種語法是:- name: This playbook will treate your managed hosts for > its http service and php language and mysql SQL server. hosts: webserver1 ...
兩者都可以達成這些目標喔! - 使用字典格式 (dictionaries),亦即透過大括號 { } 處理:
# 原始的格式是這樣: - name: copy /etc/hosts to another hosts copy: src: /etc/hosts dest: /etc/hosts # 可以改成這樣子: - name: copy /etc/hosts to another hosts copy: { src: /etc/hosts, dest: /etc/hosts }
- YAML 的列表功能,舉例來說,當你要安裝的軟體很多時,可以這樣處理:
--- - name: setup web server and add first web page hosts: - webserver1 - dbserver1 tasks: - name: install httpd packages yum: name: - httpd - php - mariadb state: latest
使用 (- ) 作為開頭,同樣需要縮排喔!這樣就可以接上許多列表的資料了!
- 除了之前談到的縮排之外,如果 yuml 需要註解,可以在任何地方增加 # 即可在後面加入註解:
- 實做練習:加上 ftp 的服務功能
- 實施的主機群,請使用 website 這個主機群組!
- 設計一個 FTP 匿名登入伺服器的功能,需要的軟體主要是 vsftpd 這個服務,請查詢 yum 模組, 找到正確的參數,安裝好 vsftpd 模組
- 這個服務每次開機都要啟動,請找 service 模組,同樣啟動這個服務!
- 防火牆記得要放行 ftp 才行。
- 設計一個 readme.txt 檔案,放置到 myuser1 的家目錄當中, 內容就填寫『 This FTP daemon setted by Ansible 』即可。記得,使用者、群組要修改成為 myuser1 所有才對喔!
- 最後,使用 get_url 的模組,自我確認一下 ftp://webserver1/readmt.txt ,使用 myuser1 登入 (注意, 密碼為 itismyuser 喔!)是否可以將檔案儲存到 /dev/shm/checkftp.txt 當中?(因為使用 FTP 的關係, 你可能需要使用 ftp://username:password@hostname/dir/file 的格式來處理才行!)
- 上述 playbook 請寫入名為 ftp_first.yml 檔案中,並且測試與執行。