1. playbooks介紹
如果說ansible的modules是工具,inventory配置文件是原材料,那么playbook就是一封說明書,這里會記錄任務是如何如何執行的,當然如果你願意,這里也可以定義一些變量、連接參數等等。
playbook可以由單個或者多個play組成。
單個play示例:
---
- hosts: webservers vars: http_port: 80 max_clients: 200 remote_user: root tasks: - name: ensure apache is at the latest version yum: name: httpd state: latest - name: write the apache config file template: src: /srv/httpd.j2 dest: /etc/httpd.conf notify: - restart apache - name: ensure apache is running service: name: httpd state: started
上面的示例中所有的任務作用於webservers所包含的主機,通過root用戶連接到目的主機,對apache服務進行了安裝、配置、啟動等操作,當配置文件有更改時,會觸發hanlders里的重啟apache操作,vars里定義的“http_port”和 “ max_clients”將會在模版文件“/srv/httpd.j2”中遵循Jinja2語法被使用到。
playbooks是使用yaml語法格式,所以看起來比較通俗易懂。通過上面的示例可以看出一個play可以包含如下內容:
- hosts:主機組,后面定義的task將作用於該主機組的所有主機
- vars:變量定義,在后面的task中可以引用
- remote-user:連接參數,例如remote-user,become,become-user等等,這些參數將會覆蓋ansible.cfg配置文件里的參數
- tasks:任務,可以看作很多modules的集合,這些modules可以使用vars定義的變量
- handlers:觸發才會執行的task,很多情況下,當其他task被執行並且狀態有改變后,我們希望會觸發一些任務,那些被觸發的任務可以寫在這里
一個playbooks也可以編寫多個play,示例如下:
---
- hosts: webservers remote_user: root tasks: - name: ensure apache is at the latest version yum: name: httpd state: latest - name: write the apache config file template: src: /srv/httpd.j2 dest: /etc/httpd.conf - hosts: databases remote_user: root tasks: - name: ensure postgresql is at the latest version yum: name: postgresql state: latest - name: ensure that postgresql is started service: name: postgresql state: started
上面的示例中,第一個play通過root用戶連接到webservers主機組,進行了apache服務的安裝和配置操作;第二個play通過root用戶鏈接到databases主機組,進行了數據庫的安裝和啟動操作。
2.可重復利用的playbooks
上一章節中我們說到,一個playbooks可以放置多個play,一個play里面可以有多個tasks(modules),但是,當要管理的資源越來越多時,我們發現將所有play都寫在一個yml文件里會很臃腫,不好維護。
此時我們可以通過“import_playbook”方法引用其他的playbooks文件;
此時我們可以通過“import_playbook”方法引用其他的playbooks文件;使用“import_tasks”、“include_tasks”、“import_role”、“include_role”、“roles”引用其他的tasks文件。
import_playbook
比較簡單,直接上示例,文件main.yml:
- import_playbook: webservers.yml - import_playbook: databases.yml
上述示例中使用import_playbook將webservers.yml和databases.yml文件里的play引用到main.yml,和直接將兩個文件里的內容直接粘過來是一樣的效果,執行順序自然也會按照play定義的順序執行。
import_tasks和include_tasks
可以參考筆者之前寫的文章 ansible中include_tasks和import_tasks
import_role和include_role
ansible2.3引入了include_role,ansible 2.4版本后,新增了import_role,通過這兩個方法可以在tasks里面導入role,示例如下:
---
- hosts: webservers tasks: - debug: msg: "before we run our role"
- import_role: name: example - include_role: name: example - debug: msg: "after we ran our role"
從上面的示例可以看出,在tasks中使用import_role和include_role方法導入了role example,role里面的task會按順序執行。
當然我們也可以引用的同時定義變量:
---
- hosts: webservers roles: - common - role: foo_app_instance vars: dir: '/opt/a' app_port: 5000
- role: foo_app_instance vars: dir: '/opt/b' app_port: 5001
也可以給role打tag:
---
- hosts: webservers tasks: - import_role: name: foo tags: - bar - baz
使用條件語句(后面有詳細寫when語句用法):
---
- hosts: webservers tasks: - include_role: name: some_role when: "ansible_os_family == 'RedHat'"
roles
除了使用import_role和include_role導入role,我們也可以直接使用roles方法來導入,示例如下:
--- - hosts: webservers roles: - common - webservers ###OR - hosts: webservers roles: - role: '/path/to/my/roles/common'
和import和include方法相比,roles方法是僅僅可以導入role類型的playbook,而上述兩個方法可以在其他tasks中穿插一些role類型的playbook。
在生產中,roles是比較常用的所以后面的章節會有對roles的單獨講解,這里就不在展開了。
通過以上的總結,我們可以看出,在ansible里,如果我們想復用其他文件的playbooks,可以使用include、import、roles三種方法,據我所知也只有這三種方法。
2.1 動態和靜態
至此,我們知道了可以使用import*和include*導入其他的playbooks,那么這兩者的區別是什么呢?
在ansible 2.4版本中引入了dynamic和static的概念,在這之前只能使用include來導入其他的tasks文件,現在include也能用,但官方在考慮在未來版本廢棄掉。
靜態指所有import*的方法,動態指include*的方法。
關於動態和靜態的兩點區別,總結如下:
- import_tasks(Static)方法會在playbooks解析階段將父task變量和子task變量全部讀取並加載
- include_tasks(Dynamic)方法則是在執行play之前才會加載自己變量
- include_tasks方法調用的文件名稱可以加變量
- import_tasks方法調用的文件名稱不可以有變量
具體介紹可以參考筆者之前寫的文章ansible中include_tasks和import_tasks
3.playbooks變量
3.1定義變量
ansible中可以定義變量的地方可以有很多,在這里主要寫下playbooks里面的變量定義,其他部分的變量會在后續的“ansible基礎-變量”詳細闡述。
變量的定義通常使用YAML語法格式,示例如下:
--- vars: field1: one 字典變量: --- foo: field1: one field2: two
play中定義全局變量
---
- hosts: webservers vars: http_port: 80
上面示例中 http_port參數可以在這個play中的tasks、playbooks、roles中引用。
tasks中定義變量
當然,我們也可以在某個task中定義局部變量,這個變量只能在本task內使用,示例如下:
---
- hosts: node1 gather_facts: false tasks: - name: Use var debug vars: - name: weimeng - age: 26 debug: var: name,age
include和import中定義變量
include_role定義變量只在被引用的role中生效:
- hosts: node1 gather_facts: false tasks: - include_role: name: role_A vars: age: 24
include_tasks定義變量只在被引用的task中生效:
tasks: - import_tasks: wordpress.yml vars: wp_user: timmy - import_tasks: wordpress.yml vars: wp_user: alice - import_tasks: wordpress.yml vars: wp_user: bob
roles中定義變量
playbook引用role也可以直接定義變量,示例如下:
---
- hosts: webservers roles: - role: bar tags: ["foo"] # using YAML shorthand, this is equivalent to the above - { role: foo, tags: ["bar", "baz"] }
注冊變量
在playbook中,我們可以將一個task的執行結果注冊為一個變量,供另外一個task使用。例如:
---
- hosts: webservers roles: - role: bar tags: ["foo"] # using YAML shorthand, this is equivalent to the above - { role: foo, tags: ["bar", "baz"] }
將一個task的結果注冊為一個變量,然后通過這個變量判斷另外一個task是否執行,這是注冊變量很常用的方式。
通過命令行定義變量
在我們執行playbook時可以在命令行中指定自定義變量,例如:
ansible-playbook release.yml --extra-vars "version=1.23.45 other_variable=foo”
在同一個scope內,如果與其他地方的變量沖突,命令行指定的參數優先級最高。
3.2引用變量文件
上面介紹了在playbook中如何定義變量,那么變量能否和task一樣定義在單獨的yml文件內,然后使用類似於include_tasks的語句引用過來呢? 答案是肯定的。
在playbook內引用變量文件使用的是vars_files:語句,示例如下:
---
- hosts: all remote_user: root vars: favcolor: blue vars_files: - /vars/external_vars.yml tasks: - name: this is just a placeholder command: /bin/echo foo
在變量文件/vars/external_vars.yml中,我們只需要使用YAML語法格式進行變量定義即可。
3.3 facts
除了我們自定義的變量,ansible還支持另外一種變量,這個變量類似於puppet的facter,ansible叫做fact。
ansible的fact會根據目的主機的系統信息生成一個json格式的變量集合,我們在play中可以直接引用。例如比較常用的變量:ip地址、主機名、操作系統類型等等。
puppet的facter依賴ruby的一個安裝包,通過ruby程序收集系統信息,而ansible的fact是通過python程序收集。
我們可以通過setup模塊來獲取目的主機的fact信息:
ansible hostname -m setup
fact會在playbook執行之前收集信息,默認是打開的,我們也可以通過指定gather_fact參數為false/no/False關閉fact。在沒有配置fact cache的情況下,如果關閉fact,playbook的執行速度會有一個顯著的提升,示例如下:
---
- hosts: whatever gather_facts: no
3.4變量的使用
前面我們介紹了下變量的定義/引用方式和fact變量,那么在playbook中我們如何使用這些變量呢?
變量通常會在模版、條件判斷語句、新的變量定義等處能用到。
使用變量的方法很簡單,只需要將變量寫在兩個大括號內並且前后都有空格即可,同時我們必須將這個大括號用雙引號引起來,如果變量穿插在字符串內使用,雙引號也要將字符串部分引起來。
示例如下:
- hosts: app_servers vars: app_path: "{{ base_path }}/22"
如果一個變量定義比較復雜,例如列表、字典或fact(json格式),我們可以通過如下方式訪問:
列表變量訪問:
{{ foo[0] }}
字典變量訪問:
{{ foo[name] }}
或
{{ foo.name }}
json格式訪問變量訪問:
{{ansible_eth0["ipv4"]["address"] }}
或
{{ansible_eth0.ipv4.address }}
這里說一個小技巧,在我們排錯過程中很多情況我們要debug一些變量。此時,可以使用debug模塊輸出變量。
debug模塊有兩種使用方式,vars和msg :
---
- hosts: node1 gather_facts: false vars: - name: weimeng - age: 26 tasks: - name: Use var debug debug: var: name,age - name: Use msg debug debug: msg: "my name is {{ name }},and my age is {{ age }}"
輸入如下:
➜ lab-ansible ansible-playbook playbooks/task_vars.yml [WARNING]: Found variable using reserved name: name PLAY [node1] ******************************************************************* TASK [Use var debug] *********************************************************** ok: [node1] => { "name,age": "(u'weimeng', 26)" } TASK [Use msg debug] *********************************************************** ok: [node1] => { "msg": "my name is weimeng,and my age is 26" } PLAY RECAP ********************************************************************* node1 : ok=2 changed=0 unreachable=0 failed=0
通過對比我們可以看出,“vars”適用於直接debug變量,而“msg”可以摻雜一些字符串,我們可以根據實際情況來選擇使用。
本章節主要介紹了playbook的相關變量。ansible變量的知識點還是很多的,所以我計划在后邊會單獨介紹ansible的變量,這里就點到為止。
4. 條件語句
ansible條件語句不是很多,比較常用的就是when語句和循環語句。
當滿足一定的條件時,我們想要跳過某個task,這時候when語句出場了。當when語句的參數為true時,才會執行這個task,否則反之。
yum模塊的name可以以列表的形式指定多個安裝包,但是很多其他模塊是不支持列表的,例如file的path,copy的src,等等;或者說我們想迭代的將一個列表元素傳遞給某個模塊處理,如果有多少個元素寫多個task就很麻煩。此時我們可以使用ansible的循環語句loop(ansible 2.5以后),在2.5版本之前可以使用with_,loop類似於舊版本的with_list語句。
4.1 when語句
ansible的when語句用於判斷是否執行這個task,例如
tasks: - name: "shut down Debian flavored systems" command: /sbin/shutdown -t now when: ansible_os_family == "Debian" # note that Ansible facts and vars like ansible_os_family can be used # directly in conditionals without double curly braces
示例中如果系統的類型是“Debian”才會執行/sbin/shutdown -t now命令。
條件語句也可以使用“and”和“or”:
tasks: - name: "shut down CentOS 6 and Debian 7 systems" command: /sbin/shutdown -t now when: (ansible_distribution == "CentOS" and ansible_distribution_major_version == "6") or (ansible_distribution == "Debian" and ansible_distribution_major_version == "7")
條件也可以寫成列表的形式,這種形式和and語句起到一樣的效果:
tasks: - name: "shut down CentOS 6 systems" command: /sbin/shutdown -t now when: - ansible_distribution == "CentOS"
- ansible_distribution_major_version == "6"
register變量條件語句
通過對某個task的執行結果是否成功,決定另外一個task是否要執行:
tasks: - command: /bin/false register: result ignore_errors: True - command: /bin/something when: result is failed # In older versions of ansible use ``success``, now both are valid but succeeded uses the correct tense. - command: /bin/something_else when: result is succeeded - command: /bin/still/something_else when: result is skipped
變量是否被定義語句:
tasks: - shell: echo "I've got '{{ foo }}' and am not afraid to use it!" when: foo is defined - fail: msg="Bailing out. this play requires 'bar'" when: bar is undefined
4.2 循環語句
上面說到loop類似於舊版本的with_list語句,也就是說loop會將列表的元素逐個傳遞給上面的module,從而達到重復執行的目的。
最簡單的形式:
--- tasks: - command: echo { item } loop: [ 0, 2, 4, 6, 8, 10 ]
loop與when結合使用:
--- tasks: - command: echo { item } loop: [ 0, 2, 4, 6, 8, 10 ] when: item > 5
通常loop語句會結合各式各樣的filter去使用,例如“ loop: “{ { [\'alice\', \'bob\'] |product([\'clientdb\', \'employeedb\', \'providerdb\'])|list }}””,這個例子和with_nested語句起到一樣的效果。也就是說舊版本的with_ + lookup() 所能實現的,新版本的loop+filter同樣能實現。
5. 執行順序
一般playbook里的task執行順序和python一樣,由上至下,定義的順序即執行的順序。同樣的,使用include*和import*導入playbook或tasks也會安照導入順序執行。
5.1 per_tasks和post_tasks
當playbook中有使用roles導入task和自定義tasks時,我們會發現ansible總會先執行roles導入的task,然后執行自定義的tasks,例如:
- hosts: localhost gather_facts: no vars: - ff: 1 - gg: 2 tasks: - debug: var: ff roles: - role: role_B
輸出結果:
➜ lab-ansible ansible-playbook playbooks/roles_vars.yml PLAY [localhost] *************************************************************** TASK [role_B : debug] ********************************************************** ok: [localhost] => { "a": 2 } TASK [debug] ******************************************************************* ok: [localhost] => { "ff": 1 } PLAY RECAP ********************************************************************* localhost : ok=2 changed=0 unreachable=0 failed=0
從上面示例發現,雖然我們將tasks定義在了前面,但是tasks任務還是在roles任務之后執行。此時我們可以使用pre_task和post_task來強制指定執行順序,例如:
---
- hosts: localhost gather_facts: no vars: - ff: 1
- gg: 2 pre_tasks: - import_role: name: role_A vars: age: 23 roles: - role: role_B tasks: - debug: var: ff post_tasks: - debug: var: gg
總結下playbook里任務的執行順序:
-
使用“pre_tasks:”定義的任務
-
使用“roles:”引用的任務
-
使用“tasks:”自定義的任務
-
使用“post_tasks”定義的任務
5.2 handlers
在部署應用時,通常的步驟是安裝軟件包==>更改配置文件==>初始化數據庫==>啟動(重啟)服務;升級的步驟一般是:升級軟件包==>更改配置文件==>初始化數據庫==>重啟服務。我們發現不管是新部署還是升級,最后一步都是要重新加載程序的,也就是說當我們升級了軟件或者更改了配置文件都需要重啟一下應用。
為了實現觸發服務重啟,ansible使用handlers方法定義重啟的動作,handlers並不是每次執行playbook都會觸發,而是某些指定資源狀態改變時才會觸發指定的handlers(這里使用“資源”一詞借鑒於puppet)。
示例如下:
- name: template configuration file template: src: template.j2 dest: /etc/foo.conf notify: - restart memcached - restart apache
上面的示例中,當/etc/foo.conf文件內容有改動時(返回changed),會觸發重啟memcached和apache服務,如果文件內容沒有變化時(返回ok),則不會觸發handlers。
ansible執行過程中並不會立即觸發handlers動作,而是以play為單位,一個play執行完后最后才會觸發handlers。
這樣設計也是很合理的,試想在一個play內,如果觸發一次就執行一次handlers,那么除了最后一次的重啟,前面觸發的重啟都是無用功。
另外需要注意的一點是,handlers觸發的執行順序是按照定義順序執行,而不是按照notify指定的順序執行。
當然如果我們想要立即觸發,也是可以的,在play定義“- meta: flush_handlers”即可。
另外需要注意的一點是,handlers觸發的執行順序是按照定義順序執行,而不是按照notify指定的順序執行。
6.參考鏈接
- https://docs.ansible.com/ansible/latest/user_guide/playbooks_intro.html#
- https://docs.ansible.com/ansible/2.6/user_guide/playbooks_variables.html
- https://docs.ansible.com/ansible/2.6/user_guide/playbooks_reuse.html
- https://docs.ansible.com/ansible/2.6/user_guide/playbooks_templating.html
- https://docs.ansible.com/ansible/2.6/user_guide/playbooks_conditionals.html
- https://docs.ansible.com/ansible/2.6/user_guide/playbooks_loops.html
歡迎大家關注我的公眾號: