回到:Ansible系列文章
各位讀者,請您:由於Ansible使用Jinja2模板,它的模板語法{% raw %} {{}} {% endraw %}和{% raw %} {%%} {% endraw %}和博客系統的模板使用的符號一樣,在渲染時會產生衝突,儘管我盡我努力地花了大量時間做了調整,但無法保證已經全部都調整。因此,如果各位閱讀時發現一些明顯的詭異的錯誤(比如像這樣的空的
行內代碼),請一定要回復我修正這些渲染錯誤。
6.更大的舞臺(1):組織多個文件以及Role
在上一篇文章的最後,我將初始化配置服務器的多個任務組合到了單個playbook中,這種組織方式的可讀性和可維護性都很差,整個playbook看上去也非常凌亂。如圖:
所以,我又將各類任務分類後單獨存放在各自的playbook中,然後在入口playbook文件中使用import_playbook
指令來組織這些playbook,如此一來,各類任務分門別類且實現了自治,維護起來更爲清晰、方便。如圖:
Ansible中除了可以將play進行分類自治,還提供了其它幾種內容的組織方式,可組織的內容包括:
- playbook(或play)
- task
- variable
- handler(實際上handler也是task,只不過編寫在handlers指令內部)
此外,Ansible還提供了更爲規範的組織方式:Role以及Ansible 2.8才加入的新功能Collection。本文將對各種組織文件的方式和Role逐一進行探索,並簡單介紹Collection。
6.1 使用include還是import?
將各類文件分類存放後,最終需要在某個入口文件去彙集引入這些外部文件。加載這些外部文件通常可以使用include
指令、include_xxx
指令和import_xxx
指令,其中xxx表示內容類型。
在早期Ansible版本,組織文件的方式均使用include
指令,但隨着版本的更迭,Ansible對這方面做了更爲細緻的區分。雖然目前仍然支持include
,但早已納入廢棄的計劃,所以現在不要再使用include
指令,在後文中我也不會使用include
指令。
對於playbook(或play)或task,可以使用include_xxx
或import_xxx
指令:
- include_tasks和import_tasks用於引入外部任務文件;
- import_playbook用於引入playbook文件;
- include可用於引入幾乎所有內容文件,但建議不要使用它;
對於handler,因爲它本身也是task,所以它也能使用include_tasks
、import_tasks
來引入,但是這並不是想象中那麼簡單,後文再細說。
對於variable,使用include_vars
(這是核心模塊提供的功能)或其它組織方式(如vars_files
),沒有對應的import_vars
。
對於後文要介紹的Role,使用include_role
或import_role
或roles
指令。
既然某類內容文件既可以使用include_xxx
引入,也可以使用import_xxx
引入,那麼就有必要去搞清楚它們有什麼區別。本文最後我會詳細解釋它們,現在我先把結論寫在這:
include_xxx
指令是在遇到它的時候才加載文件並解析執行,所以它是動態解析的;import_xxx
是在解析playbook的時候解析的,也就是說在執行playbook之前就已經解析好了,所以它也稱爲靜態加載。
6.2 組織task
在此前的所有示例中,一直都是將所有任務編寫在單個playbook文件中。但Ansible允許將任務分離到不同的文件中,然後去引入外部任務文件。
用示例來解釋會非常簡單。假設,兩個playbook文件pb1.yml和pb2.yml。
pb1.yml文件內容如下:
---
- name: play1
hosts: localhost
gather_facts: false
tasks:
- name: task1 in play1
debug:
msg: "task1 in play1"
# - include_tasks: pb2.yml
- import_tasks: pb2.yml
pb2.yml文件內容如下:
- name: task2 in play1
debug:
msg: "task2 in play1"
- name: task3 in play1
debug:
msg: "task3 in play1"
執行pb1.yml:
$ ansible-playbook pb1.yml
上面是在pb1.yml文件中通過import_tasks
引入了額外的任務文件pb2.yml,對於此處來說,將import_tasks
替換成include_tasks
也能正確工作,不會有任何影響。
但如果是在循環中(比如loop),則只能使用include_tasks
而不能再使用import_tasks
。
6.2.1 在循環中include文件
修改pb1.yml和pb2.yml文件內容:
pb1.yml內容如下,注意該文件中的include_tasks
指令:
---
- name: play1
hosts: localhost
gather_facts: false
tasks:
- name: task1 in play1
debug:
msg: "task1 in play1"
- name: include two times
include_tasks: pb2.yml
loop:
- ONE
- TWO
pb2.yml內容如下,注意該文件中的{{item}}
變量引用:
- name: task2 in play1
debug:
msg: "task2 in {{item}}"
執行pb1.yml文件,觀察執行結果:
$ ansible-playbook pb1.yml
TASK [task1 in play1] ************************
ok: [localhost] => {
"msg": "task1 in play1"
}
TASK [include two times] *********************
included: /root/ansible/pb2.yml for localhost
included: /root/ansible/pb2.yml for localhost
TASK [task2 in play1] ************************
ok: [localhost] => {
"msg": "task2 in ONE"
}
TASK [task2 in play1] ************************
ok: [localhost] => {
"msg": "task2 in TWO"
}
上面是在loop
循環中加載兩次pb2.yml文件,該文件中的任務被執行了兩次,並且在pb2.yml中能夠引用外部文件(pb1.yml)中定義的變量{{item}}
。
分析一下上面的執行流程:
正是因爲include_tasks
指令是在遇到它的時候才進行加載解析以及執行,所以在pb2.yml中才能使用變量{{item}}
。
如果將上面loop循環中的include_tasks
換成import_tasks
呢?語法會報錯,後面我會詳細解釋。
6.3 組織handler
handler其本質也是task,所以也可以使用include_tasks
或import_tasks
來加載外部任務文件。但是它們引入handler任務文件的方式有很大的差別。
先看include_tasks
引入handler任務文件的示例:
pb1.yml的內容:
---
- name: play1
hosts: localhost
gather_facts: false
handlers:
- name: h1
include_tasks: handler1.yml
tasks:
- name: task1 in play1
debug:
msg: "task1 in play1"
changed_when: true
notify:
- h1
注意在tasks的任務中加了一個指令changed_when: true
,它用來強制指定它所在任務的changed狀態,如果條件爲真,則changed=1
,否則changed=0
。使用這個指令是因爲debug模塊默認不會引起changed=1
行爲,所以只能使用該指令來強制其狀態爲changed=1
。
當Ansible監控到了changed=1
,notify指令會生效,它會去觸發對應的handler,它觸發的handler的名稱是handler1
,其作用是使用include_tasks
指令引入handler1.yml文件。
下面是handler1.yml文件的內容:
---
- name: h11
debug:
msg: "task h11"
注意兩個名稱,一個是notify觸發handler的任務名稱("h1"),一個是引入文件中任務的名稱("h11"),它們是兩個任務。
再來看import_tasks
引入handler文件的示例,注意觀察名稱的不同點。
如下是pb1.yml文件的內容:
---
- name: play1
hosts: localhost
gather_facts: false
handlers:
- name: h2
import_tasks: handler2.yml
tasks:
- name: task1 in play1
debug:
msg: "task1 in play1"
changed_when: true
notify:
- h22
下面是使用import_tasks
引入的handler2.yml文件的內容:
---
- name: h22
debug:
msg: "task h22"
在引入handler任務文件的時候,include_tasks
和import_tasks
的區別表現在:
- 使用
include_tasks
時,notify指令觸發的handler名稱是include_tasks
任務本身的名稱 - 使用
import_tasks
時,notify指令觸發的handler名稱是import_tasks
所引入文件內的任務名稱
將上面的兩個示例合在一起,或許要更清晰一點:
---
- name: play1
hosts: localhost
gather_facts: false
handlers:
- name: h1
include_tasks: handler1.yml
- name: h2
import_tasks: handler2.yml
tasks:
- name: task1 in play1
debug:
msg: "task1 in play1"
changed_when: true
notify:
- h1 # 注意h1和h22名稱的不同
- h22
其實分析一下就很容易理解爲什麼notify觸發的名稱要不同:
include_tasks
是在遇到這個指令的時候才引入文件的,所以notify不可能去觸發外部handler文件裏的名稱(h11),外部handler文件中的名稱在其引入之前根本就不存在import_tasks
是在解析playbook的時候引入的,換句話說,在執行play之前就已經把外部handler文件的內容引入並替換在handler的位置處,而原來的名稱(h2)則被覆蓋了
最後,不要忘了import_tasks
或include_tasks
自身也是任務,既然是任務,就能使用task層次的指令。例如下面的示例:
handlers:
- name: h1
include_tasks: handler.yml
vars:
my_var: my_value
when: my_var == "my_value"
但這兩個指令對task層次指令的處理方式不同,相關細節仍然保留到後文統一解釋。
6.4 組織變量
在Ansible中有很多種定義變量的方式,想要搞清楚所有這些散佈各個角落的知識,是一個很大的難點。好在,沒必要去過多關注,只需要掌握幾個常用的變量定義和應用的方式即可。此處我要介紹的是將變量定義在外部文件中,然後去引入這些外部文件中的變量。
引入保存了變量的文件有兩種方式:include_vars
和vars_files
。此外,還可以在命令行中使用-e
或--extra-vars
選項來引入。
6.4.1 vars_files
先介紹vars_files
,它是一個play級別的指令,可用於在解析playbook的階段引入一個或多個保存了變量的外部文件。
例如,pb.yml文件如下:
---
- name: play1
hosts: localhost
gather_facts: false
vars_files:
- varfile1.yml
- varfile2.yml
tasks:
- debug:
msg: "var in varfile1: {{var1}}"
- debug:
msg: "var in varfile2: {{var2}}"
pb.yml文件通過vars_files
引入了兩個變量文件,變量文件的寫法要求遵守YAML或JSON格式。下面是這兩個文件的內容:
# 下面是varfile1.yml文件的內容
---
var1: "value1"
var11: "value11"
# 下面是varfile2.yml文件的內容
---
var2: "value2"
var22: "value22"
需要說明的是,vars_files
指令是play級別的指令,且是在解析playbook的時候加載並解析的,所以所引入變量的變量是play範圍內可用的,其它play不可使用這些變量。
6.4.2 include_vars
include_vars
指令也可用於引入外部變量文件,它和vars_files
不同。一方面,include_vars
是模塊提供的功能,它是一個實實在在的任務,所以在這個任務執行之後纔會創建變量。另一方面,既然include_vars
是一個任務,它就可以被一些task級別的指令控制,如when
指令。
例如:
---
- name: play1
hosts: localhost
gather_facts: false
tasks:
- name: include vars from files
include_vars: varfile1.yml
when: 3 > 2
- debug:
msg: "var in varfile1: {{var1}}"
上面示例中引入變量文件的方式是直接指定文件名include_vars: varfile1.yml
,也可以明確使用file
參數來指定路徑。
- name: include vars from files
include_vars:
file: varfile1.yml
如果想要引入多個文件,可以使用循環的方式。例如:
- name: include two var files
include_vars:
file: "{{item}}"
loop:
- varfile1.yml
- varfile2.yml
tasks:
- name: include vars from files
include_vars:
file: "{{item}}"
with_first_found:
- varfile1.yml
- varfile2.yml
- default.yml
# 等價於:
tasks:
- name: include vars from files
include_vars:
file: "{{ lookup('first_found',any_files) }}"
vars:
any_files:
- varfile1.yml
- varfile2.yml
- default.yml
此外,include_vars
還能從目錄中導入多個文件,默認會遞歸到子目錄中。例如:
- name: Include all files in vars/all
include_vars:
dir: vars/all
6.4.3 --extra-vars選項
ansible-playbook命令的-e
選項或--extra-vars
選項也可以用來定義變量或引入變量文件。
# 定義單個變量
$ ansible-playbook -e 'var1="value1"' xxx.yml
# 定義多個變量
$ ansible-playbook -e 'var1="value1" var2="value2"' xxx.yml
# 引入單個變量文件
$ ansible-playbook -e '@varfile1.yml' xxx.yml
# 引入多個變量文件
$ ansible-playbook -e '@varfile1.yml' -e '@varfile2.yml' xxx.yml
因爲是通過選項的方式來定義變量的,所以它所定義的變量是全局的,對所有play都有效。
通常來說不建議使用-e選項,因爲這對用戶來說是不透明也不友好的,要求用戶記住要定義哪些變量。
6.5 組織playbook文件
當單個playbook文件中的任務過多時,或許就是將任務劃分到多個文件中的時刻。我想各位在經過上一篇文章的"洗禮"後,應該能體會這需求是多麼的迫切。
import_playbook
指令可用於引入playbook文件,它是一個play級別的指令,其本質是引入外部文件中的一個或多個play。
例如,pb.yml是入口playbook文件,此文件中引入了其它playbook文件,其內容如下:
---
# 引入其它playbook文件
- import_playbook: pb1.yml
- import_playbook: pb2.yml
# 文件本身的play
- name: play in self
hosts: localhost
gather_facts: false
tasks:
- debug: 'msg="file pb.yml"'
pb1.yml文件是一個完整的playbook,它可以包含一個或多個play,其內容如下:
---
- name: play in pb1.yml
hosts: localhost
gather_facts: false
tasks:
- debug: 'msg="imported file: pb1.yml"'
pb2.yml文件也是一個完整的playbook,其內容如下:
---
- name: play in pb2.yml
hosts: localhost
gather_facts: false
tasks:
- debug: 'msg="imported file: pb2.yml"'
6.6 更爲規範的組織方式:Role
前面介紹了組織各種文件的方式,它們都非常實用,但是各種yml文件多了,特別是多個playbook任務混在一起時,很容易混亂。
例如:
.
├── default.yml
├── handler_restart_mysql.yml
├── handler_restart_nginx.yml
├── main.yml
├── mysql.yml
├── nginx.yml
├── var_mysql.yml
└── var_nginx.yml
或許我們可以按照自己的文件組織方式,將各文件進行分類,比如nginx任務相關的放在nginx目錄下,mysql相關的放在mysql目錄下,nginx相關的變量放在nginx/vars
目錄中,mysql相關的handler放在mysql/handlers
目錄中,等等。
當然,使用Role和手動使用include_xxx
、import_xxx
並不衝突,有時候也確實需要手動去引入其它文件。
所以關於Role,需要學習的就是它的文件組織方式,我將會一一介紹。不過在此之前,先簡單看看整個Role的結構。
6.6.1 Role文件結構一覽
Role可以組織任務、變量、handler以及其它一些內容,所以一個完整的Role裏包含的目錄和文件可能較多,手動去創建所有這些目錄和文件是一件比較煩人的事,好在可以使用ansible-galaxy init ROLE_NAME
命令來快速創建一個符合Role文件組織規範的框架。關於ansible galaxy,我稍後會簡單介紹一下它。
例如,下面創建了一個名爲first_role
的Role:
$ ansible-galaxy init first_role
$ tree
.
└── first_role
├── defaults
│ └── main.yml
├── files
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── README.md
├── tasks
│ └── main.yml
├── templates
├── tests
│ ├── inventory
│ └── test.yml
└── vars
└── main.yml
可以使用ansible-galaxy init --help
查看更多選項。比如,使用--init-path
選項指定創建的Role路徑:
$ ansible-galaxy init --init-path /etc/ansible/roles first_role
可以看到,這裏面已經包含了不少目錄和文件,這些目錄的含義稍後我會一一解釋,不過從部分文件名中,大概能看出一個Role包含了任務、變量、handler等。這些目錄或目錄裏的文件允許不存在,在沒有使用到相關文件的時候並不強制這些文件或目錄存在。
因爲有可能同時會有多個Role,比如創建一個Nginx的Role,再創建一個MySQL的Role,還創建一個Haproxy的Role,所以爲了組織多個Role,通常會將每個Role放在一個稱爲roles的目錄下。即:
$ tree -L 2
.
└── roles
├── first_role
└── second_role
有了Role之後,就可以將Role當作一個不可分割的任務整體來對待,一個Role相當於是一個完整的功能。但在此需要明確一個層次上的概念,Role只是用於組織一個或多個任務,原來在play級別中使用tasks
指令來定義任務,現在使用roles
指令來引入Role中定義的任務。當然,roles
指令和tasks
指令並不衝突,它們可以共存。
通過下面的圖,應能幫助理解Role的角色。
既然Role是一個完整的任務體系,擁有Role之後就可以去使用它,或者也可以分發給別人使用,但是一個Role僅僅只是目錄而已,如何去使用這個Role呢?
所以,還需要提供一個被ansible-playbook
執行的入口playbook文件(就像main()函數一樣),在這個入口文件中引入一個或多個roles目錄下的Role。入口文件的名稱可以隨意,比如www.yml、site.yml、main.yml等,但注意它們和roles目錄在同一個目錄下。
例如:
.
├── enter.yml
└── roles
├── first_role/
└── second_role/
上面和roles同目錄的enter.yml文件內容如下,此文件中使用roles
指令引入了roles目錄內的兩個Role。
---
- name: play with role
hosts: nginx
gather_facts: false
roles:
- first_role
- second_role
如果遵循了Role規範,入口文件中可以直接使用Role名稱來引入roles目錄下的Role(正如上面的示例),也可以指定Role的路徑來引入。
下面再一一介紹Role詳細的內容。
6.6.2 定義Role的task
Role的任務定義在roles/xxx/tasks/main.yml
文件中,main.yml是該Role任務的入口,在執行Role的時候會自動執行main.yml中的任務。可以直接將所有任務定義在此文件中,也可以定義在其它文件中,然後在main.yml文件中去引入。
以first_role
這個Role爲例,例如,直接將任務定義在main.yml文件中:
---
- name: task in main.yml
debug:
msg: "task in main.yml"
或者,將任務定義在roles/xxx/tasks/
目錄下的其它文件中,如mytask.yml:
---
- name: task in main.yml
debug:
msg: "task in main.yml"
然後在roles/xxx/tasks/main.yml
中通過include_tasks
或import_tasks
引入它:
---
- include_tasks: mytask.yml
# 或者
#- import_tasks: mytask.yml
前面已經提到過include_xxx
和import_xxx
的區別,這裏不對其展開描述,後面還會詳細解釋。
Role的任務文件定義好後,然後在Role的入口文件(即roles同目錄下的playbook文件)enter.yml中引入這個Role:
---
- name: play1
hosts: localhost
gather_facts: false
roles:
- first_role
執行它:
$ ansible-playbook enter.yml
6.6.3 定義Role的handler
handler和task類似,它定義在roles/xxx/handlers/main.yml
中,當Role的task觸發了對應的handler,會自動來此文件中尋找。
仍然要說的是,可以將handler定義在其它文件中,然後在roles/xxx/handlers/main.yml
使用include_tasks
或import_tasks
指令來引入,而且前面也提到過這兩者在handler上的區別和注意事項。
例如,roles/first_role/handlers/main.yml
中定義瞭如下簡單的handler:
---
- name: handler for test
debug:
msg: "a simple handler for test"
在roles/first_role/tasks/main.yml
中通過notify觸發該Handler:
---
- name: task in main.yml
debug:
msg: "task in main.yml"
changed_when: true
notify: handler for test
然後執行:
$ ansible-playbook enter.yml
6.6.4 定義Role的變量
這兩個文件之間的區別在於,defaults/main.yml
中定義的變量優先級低於vars/main.yml
中定義的變量。事實上,defaults/main.yml
中的變量優先級幾乎是最低的,基本上其它任何地方定義的變量都可以覆蓋它。
6.6.5 Role用到的外部文件和模板文件
有時候需要將Ansible端的文件拷貝到遠程節點上,比如拷貝本地已經寫好的MySQL配置文件my.cnf到多個遠程節點上,拷貝本地寫好的腳本文件到多個遠程節點執行,等等。
這時候在進行拷貝的模塊中可以指定這些文件的絕對路徑。但在Role中,可以將這些文件放在roles/xxx/files/
或roles/xxx/templates/
目錄下,遵守了這個Role規範,就可以在模塊中直接指定文件名稱,而不用加上路徑前綴(當然,加上也不會錯)。
例如,Role中有一個copy模塊的任務,想要拷貝roles/first_role/files/my.cnf
到目標節點的/etc/my.cnf,則:
- name: copy file
copy:
src: my.cnf # 直接指定文件名my.cnf即可
dest: /etc/my.cnf
這些模塊知道去roles/xxx/files/
目錄或roles/xxx/templates/
下搜索對應文件的原因,在於這些模塊的代碼內部定義了文件搜索路徑,不同的模塊搜索路徑不同,且可能不止一個搜索路徑。
例如對於Role中的template模塊任務(template模塊目前尚未介紹,之後遇到的時候再解釋,或者各位可自搜其用法,現在將其當作copy模塊即可),如果其參數src=my.cnf
,則依次搜索如下路徑:
roles/first_role/templates/my.cnf
roles/first_role/my.cnf
roles/first_role/tasks/templates/my.cnf
roles/first_role/tasks/my.cnf
templates/my.cnf
my.cnf
一般來說,需要考慮源文件存放位置的模塊包括copy、script、template模塊,前兩個模塊以及其它可能的模塊,一般會先搜索roles/xxx/files/
目錄,但不會搜索templates目錄,而template模塊則會先搜索templates目錄而不會搜索files目錄。
換句話說,除了template模塊外,其它模塊使用到的文件很可能都應該存放於roles/xxx/files/
目錄。如果不確定某個模塊的搜索路徑,測試一番即可,或者直接看報錯信息中給出的路徑搜索過程。
6.6.6 Role中定義的模塊和插件
對於絕大多數需求,使用Ansible已經提供的模塊和插件就能解決問題,但有時候確實有些需求需要自己去寫模塊或插件,Ansible也支持用戶自定義的模塊和插件。
對於Role來說,如果這個Role需要額外使用自己編寫的模塊或插件,則模塊放在roles/xxx/librarys/
目錄下,而插件放在各自對應類型的目錄下:
roles/xxx/action_plugins/
roles/xxx/lookup_plugins/
roles/xxx/callback_plugins/
roles/xxx/connection_plugins/
roles/xxx/filter_plugins/
roles/xxx/strategy_plugins/
roles/xxx/cache_plugins/
roles/xxx/test_plugins/
roles/xxx/shell_plugins/
一般情況用不上自定義模塊或插件,目前各位瞭解即可。
6.6.7 定義Role的依賴關係
換句話說,有些任務必須先行,這些先行任務就是被依賴的任務。
按照Role規範,被依賴的先行任務都定義在roles/xxx/meta/main.yml
文件中。
例如:
---
dependencies:
- second_role
- third_role
注意,Role的dependencies
指令只能指定被依賴的Role,不能直接指定被依賴的任務。例如,下面是錯誤的依賴定義:
---
dependencies:
- debug: msg="check it"
當真正開始執行Role的時候,會先檢查是否有依賴任務,如果有,則先執行依賴任務,依賴任務執行完後再開始執行普通任務。
6.6.8 動手寫一個Role
瞭解完Role各個目錄和文件的意義後,可以開始動手寫一個Role來體驗一番。
就以first_role
爲例,這個Role沒有具體的功能,全部都是debug
模塊的調試信息,所以這個Role非常簡單,這個Role唯一的意義是:學會寫最簡單的Role並看懂執行流程。
首先在defaults/main.yml中定義一個變量default_var
。
---
default_var: default_value
然後在vars/main.yml中定義兩個變量my_var
和default_var
:
---
my_var: my_value
default_var: overrided_default_value
顯然vars/main.yml中的default_var
會覆蓋defaults/main.yml中的default_var
。
定義完變量之後,就可以在task、handler甚至template模板文件中使用這些變量。當然,在實際編寫Role的時候,一般不可能預先知道要定義哪些變量,通常都是在編寫task的過程中來變量文件中添加變量的。
然後是tasks/main.yml文件,在此文件中定義了一個使用變量的任務,並引入了一個外部task文件t.yml。內容如下:
---
- name: task1
debug:
msg: "task in my_var: {{my_var}}"
- name: include t.yml
import_tasks: t.yml
在t.yml中定義了一個任務,且通過notify觸發一個handler,其內容爲:
---
- name: task in t.yml
debug:
msg: "default_var: {{default_var}}"
changed_when: true
notify: "go to handler"
然後去handlers/main.yml中定義對應的handler即可,其內容爲:
---
- name: go to handler
debug:
msg: "new_var: {{new_var}}"
這個Role就這麼簡單,因爲沒有定義依賴關係,也沒有拷貝文件,所以roles/first_role/{meta,files,templates}
這三個目錄都可以刪掉。
寫好Role後,再提供一個被ansible-playbook
命令執行的入口playbook文件,然後在此playbook文件中去加載對應的Role並執行。例如,這個入口文件名爲enter.yml,其內容如下:
---
- name: play1
hosts: localhost
gather_facts: false
roles:
- role: first_role
vars:
new_var: new_value
最後執行該入口文件:
$ ansible-playbook enter.yml
PLAY [play1] ****************************************
TASK [first_role : task1] ***************************
ok: [localhost] => {
"msg": "task in my_var: my_value"
}
TASK [first_role : task in t.yml] *******************
changed: [localhost] => {
"msg": "default_var: overrided_default_value"
}
RUNNING HANDLER [first_role : go to handler] ********
ok: [localhost] => {
"msg": "new_var: new_value"
}
6.7 使用Role:roles、include_role和import_role
寫好Role之後就是使用Role,即在一個入口playbook文件中去加載Role。
加載Role的方式有多種:
例如前面使用的是roles
,如下:
---
- name: play1
hosts: localhost
gather_facts: false
roles:
- first_role
上面通過roles
指令來定義要解析和執行的Role,可以同時指定多個Role,且也可以加上role:
參數,例如:
roles:
- first_role
- role: seconde_role
- role: third_role
也可以使用include_role
和import_role
來引入Role,但需注意,這兩個指令是tasks級別的,也正因爲它們是task級別,使得它們可以和其它task共存。
例如:
---
- hosts: localhost
gather_facts: false
tasks:
- debug:
msg: "before first role"
- import_role:
name: first_role
- include_role:
name: second_role
- debug:
msg: "after second role"
這三種引入Role的方式都可以爲對應的Role傳遞參數,例如:
---
- hosts: localhost
gather_facts: false
roles:
- role: first_role
varvar: "valuevalue"
vars:
var1: value1
tasks:
- import_role:
name: second_role
vars:
var1: value1
- include_role:
name: third_role
vars:
var1: value1
有時候需要讓某個Role按需執行,比如對於目標節點是CentOS 7時執行Role7而不執行Role6,目標節點是CentOS 6時執行Role6而不是Role7,這可以使用when
指令來控制。
例如:
---
- hosts: localhost
gather_facts: false
roles:
# 下面是等價的,分別採用YAML和Json語法書寫
- role: first_role
when: xxx
- {role: ffirst_role, when: xxx}
tasks:
- import_role:
name: second_role
when: xxx
- include_role:
name: third_role
when: xxx
注意,在roles
、import_role
和include_role
三種方式中,when
指令的層次。
通常來說,無論使用哪種方式來引入Role都可以,只是某些場景下需要小心一些陷阱。
6.8 查看任務和打標籤tags
從結果中還看到play和task的後面都帶有TAGS: []
,它是標籤。當在play或task級別使用tags
指令後就表示爲此play或task打了標籤。
1.可以在task級別爲單個任務打一個或多個標籤,多個任務可以打同一個標籤名。
例如:
- name: yum install ntp
yum:
name: ntp
state: present
tags:
- initialize
- pkginstall
- ntp
- name: started ntpd
service:
name: ntpd
state: started
tags:
- ntp
- initialize
當任務具有了標籤之後,就可以在ansible-playbook命令行中使用--tags
來指定只有帶有某標記的任務才執行,也可以使用--skip-tags
選項明確指定不要執行某個任務。
# 只執行第一個任務
$ ansible-playbook test.yml --tags "pkginstall"
# 兩個任務都執行
$ ansible-playbook test.yml --tags "ntp,initialize"
# 第一個任務不執行
$ ansible-playbook test.yml --skip-tags "pkginstall"
如果想要確定tag篩選之後會執行哪些任務,加上--list-tasks
即可:
$ ansible-playbook test.yml --tags "ntp" --list-tasks
2.可以在play級別打標籤,這等價於對play中的所有任務都打上標籤。
例如:
- name: play1
hosts: localhost
gather_facts: false
tags:
- tag1
- tag2
pre_tasks:
- debug: "msg='pre_task1'"
- debug: "msg='pre_task2'"
tasks:
- debug: "msg='task1'"
- debug: "msg='task2'"
這會爲4個任務都打tag1和tag2標籤。
$ ansible-playbook a.yml --list-tasks
playbook: a.yml
play #1 (localhost): play1 TAGS: [tag1,tag2]
tasks:
debug TAGS: [tag1, tag2]
debug TAGS: [tag1, tag2]
debug TAGS: [tag1, tag2]
debug TAGS: [tag1, tag2]
3.在靜態加載文件的指令上打標籤,等價於爲所加載文件中所有子任務打標籤。在動態加載文件的指令上打標籤,不會爲子任務打標籤,而是爲父任務自身打標籤。
關於靜態、動態加載,本文最後會詳細說明。現在說結論:
- 靜態加載的指令有:
roles
、include
、import_tasks
、import_role
- 動態加載的指令只有
include_xxx
,包括include_tasks
、include_role
import_playbook
和include_playbook
因爲本身就是play級別或高於play級別,所以不能爲這兩個指令打標籤。
例如,在b.yml文件中有兩個任務:
---
- debug: "msg='task1 in b.yml'"
- debug: "msg='task2 in b.yml'"
在c.yml中也有兩個任務:
---
- debug: "msg='task1 in c.yml'"
- debug: "msg='task2 in c.yml'"
然後在a.yml中分別使用import_tasks
指令引入b.yml,使用include_tasks
指令引入c.yml,同時爲這兩個指令打標籤:
- name: play1
hosts: localhost
gather_facts: false
tasks:
- import_tasks: b.yml
tags: [tag1,tag2]
- include_tasks: c.yml
tags: [tag3,tag4]
這會爲b.yml中的兩個任務打上tag1和tag2標籤,還會爲a.yml中的include_tasks任務自身打上標籤tag3和tag4。
$ ansible-playbook a.yml --list-tasks
playbook: a.yml
play #1 (localhost): play1 TAGS: []
tasks:
debug TAGS: [tag1, tag2]
debug TAGS: [tag1, tag2]
include_tasks TAGS: [tag3, tag4]
關於是否要打標籤,衆說紛紜。我個人的看法是不要單獨爲任務打標籤,要麼爲整個Role打標籤,要麼爲靜態加載進來的整個文件打標籤,如果手動在任務級別上打標籤,標籤數量一多,playbook會顯得非常混亂。
6.9 Ansible Galaxy和Collection
很多時候我們想要實現的Ansible部署需求其實別人已經寫好了,所以我們自己不用再動手寫(甚至不應該自己寫),直接去網上找別人已經寫好的輪子即可。
Ansible Galaxy(https://galaxy.ansible.com/)是一個Ansible官方的Role倉庫,世界各地的人都在裏面分享自己寫好的Role,我們可以直接去Galaxy上搜索是否有自己想要的Role,如果有符合自己心意的,直接安裝便可。當然,我們也可以將寫好的Role分享出去給別人使用。
Ansible提供了一個ansible-galaxy
命令行工具,可以快速創建、安裝、管理由該工具維護的Role。它常用的命令有:
# 安裝Role:
ansible-galaxy install username.role_name
# 移除Role:
ansible-galaxy remove username.role_name
# 列出已安裝的Role:
ansible-galaxy list
# 查看Role信息:
ansible-galaxy info username.role_name
# 搜索Role:
ansible-galaxy search role_name
# 創建Role
ansible-galaxy init role_name
# 此外還有:'delete','import', 'setup', 'login'
# 它們都用於管理galaxy.ansible.com個人賬戶或裏面的Role
# 無視它們
例如,前面已經用該命令快速創建過一個Role,免去了手動創建Role的一堆目錄和文件。
$ ansible-galaxy init --init-path /etc/ansible/roles first_role
當從Galaxy中搜索到了Role之後,可以直接使用ansible-galaxy install author.rolename
來安裝,之所以要加上作者名author,是因爲不同的人可能會上傳名稱相同的Role。
例如,我搜索到了一個"helloworld"的測試Role:https://galaxy.ansible.com/chusiang/helloworld,
點進去後,就能看到安裝方式。比如:
$ ansible-galaxy install chusiang.helloworld
- downloading role 'helloworld', owned by chusiang
- downloading role from ......
- extracting chusiang.helloworld to /root/.ansible/roles/chusiang.helloworld
- chusiang.helloworld (master) was installed successfully
默認情況下,ansible-galaxy install
安裝Role的位置順序是:
- ~/.ansible/roles
- /usr/share/ansible/roles
- /etc/ansible/roles
可以使用-p
或--roles-path
選項指定安裝路徑。
$ ansible-galaxy install -p roles/ chusiang.helloworld
安裝完成後,就可以直接使用這個Role。例如,創建一個enter.yml文件,並在此文件中引入該Role,其內容如下:
---
- name: role from galaxy
hosts: localhost
gather_facts: false
roles:
- role: chusiang.helloworld
然後執行:
$ ansible-playbook enter.yml
雖然Ansible Galaxy中有大量的Role,但有時候我們也會在Github上搜索Role,而且Galaxy倉庫上的Role大多也都在Github上。ansible-galaxy install
也可以直接從git上下載安裝Role。
例如,上面"helloworld" Role存放在https://github.com/chusiang/helloworld.ansible.role,直接從github上安裝它:
$ ansible-galaxy install -p roles/ git+https://github.com/chusiang/helloworld.ansible.role.git
注意,從git安裝和從Galaxy上安裝的Role名稱可能不一樣。例如,下面roles/目錄下有兩個"helloworld" Role,但名稱不同:
$ ansible-galaxy list -p roles
# /root/ansible/role_test/roles
- first_role, (unknown version)
- chusiang.helloworld, master
- helloworld.ansible.role, (unknown version)
# /root/.ansible/roles
- chusiang.helloworld, master
# /usr/share/ansible/roles
# /etc/ansible/roles
Ansible Collection
對於文件組織結構,在Ansible 2.8以前只支持Role的概念,但Ansible 2.8中添加了一項目前仍處於實驗性的功能Collection,它以包的管理模式來結構化管理Ansible playbook涉及到的各個文件。
比如,我們可以將整個寫好的功能構建、打包,然後分發出去,別人就可以使用ansible-galaxy
(要求Ansible 2.9)去安裝這個打包好的文件,這爲自動化構建和部署帶來了很大的便利。
如下,是一個collection的目錄組織結構示例:
long/ # author name
└── testing # collection name
├── docs/
├── galaxy.yml
├── plugins/
│ ├── modules/
│ │ └── module1.py
│ ├── inventory/
│ └── .../
├── README.md
├── roles/
│ ├── role1/
│ ├── role2/
│ └── .../
├── playbooks/
│ ├── files/
│ ├── vars/
│ ├── templates/
│ └── tasks/
└── tests/
目前Ansible Galaxy上的Collection還非常少,在我寫這篇文章的時候,Ansible Galaxy上目前只提交了11個collection。
關於Collection更詳細的內容,我不多作介紹,目前它還處於試驗階段,各位如有興趣可自行參考官方手冊的說明:https://docs.ansible.com/ansible/latest/galaxy/user_guide.html。
6.10 playbook的執行順序
最後,再解釋一下Ansible從開始執行playbook到執行結束中間經歷的大致過程,讓各位對Ansible的工作流程有一個全局的認識。
這裏所介紹的不涉及執行策略,比如一次性選中幾個節點執行、執行完後是否立即切入下一個任務、下一個節點執行等等,這裏所說的流程,是對每個節點而言,Ansible將以何種順序去執行。
當Ansible解析完inventory之後,就進入解析playbook的階段,解析完playbook之後,纔開始執行第一個play。
每個play中可能有多種、多項任務,它們的執行順序依次爲:
上面的邏輯應該非常容易理解,但幾個事項需說明:
roles
指令加載的Role比tasks
中的任務先執行- 每個階段的handler默認都在當前階段所有任務完成之後纔開始執行,且重複觸發的handler將只執行一次
例如下面的playbook:
---
- name: play1
hosts: localhost
gather_facts: false
pre_tasks:
- name: pre_task1
debug:
msg: "hello pretask"
changed_when: true
notify: "notify me"
roles:
- role: first_role
- role: second_role
tasks:
- name: task1
debug:
msg: "hello task"
changed_when: true
notify: "notify me"
post_tasks:
- name: post_task1
debug:
msg: "hello posttask"
changed_when: true
notify: "notify me"
handlers:
- name: notify me
debug:
msg: "I am handler"
整個play的執行流程似乎非常簡單,但是playbook或play中可能不是直接定義內容,而是通過include_xxx
或import_xxx
從其它文件中加載,它們之間有區別。本文從頭到尾,對於它們的區別,我都只是簡單重複一個結論:import_xxx
是playbook解析的階段加載,include_xxx
是遇到指令的時候加載。現在,我要花點篇幅去解釋解釋include
、roles
、include_xxx
、import_xxx
之間的區別。
6.10.1 playbook解析、動態加載和靜態加載
還是這個結論:
import_xxx
是在playbook的解析階段加載文件include_xxx
是遇到指令的時候加載文件
只要理解了這兩個結論,所有相關的現象都能理解。
那麼playbook的解析是什麼意思,它做了什麼事呢?
第一個要明確的是playbook解析處於哪個階段執行:inventory解析完成後、play開始執行前的階段。
第二個要明確的是playbook解析做了哪些哪些事。一個簡單又直觀的描述是,playbook解析過程中,會掃描playbook文件中的內容,然後檢查語法並轉換成Ansible認識的內部格式,以便讓Ansible去執行。
更具體一點,在解析playbook期間:
根據這些描述,再試着來理解一下下面這些現象或結論(有些在前文已經出現過),應該不會難理解了。
(1).在循環中,使用include_xxx
而不能使用import_xxx
。
例如,某個等待被加載的文件b.yml內容如下:
---
- name: task1
debug: "msg='hello'"
- name: task2
debug: "msg='world'"
然後a.yml中使用循環通過include_tasks
去加載b.yml:
tasks:
- name: loop task
include_tasks: b.yml
loop: [1,2]
這並沒有什麼問題,當開始執行到這個父級別循環任務的時候,每循環一輪去加載一次這個文件然後執行這個文件中的所有子任務。
但如果a.yml中使用import_tasks
去加載b.yml,在解析playbook的時候,就已經將b.yml中的任務替換到這個指令的位置處,假設這裏不會報錯,那麼在執行到這個循環任務的時候,這個任務的內容大概變成了這樣:
tasks:
- name: loop task
- name: task1
debug: "msg='hello'"
- name: task2
debug: "msg='world'"
loop: [1,2]
這看上去不倫不類,顯然會出現語法錯誤。事實上也確實如此,各位可以測試一下然後觀察報錯的階段正是語法檢查階段,並不是在執行任務的階段報錯的。
但是要給各位提個醒,對於task級別的import_xxx
或include
指令,比如import_tasks
、import_role
,它並非直接原文插入,而是先解析父級別任務的指令,並將這些指令複製到所加載文件中每個子任務上,然後再原文替換(前面已經接觸過一個tags指令,它會將標籤複製到所有子任務上)。
所以,在不報錯的假設下,上面示例在解析後應該是類似這樣的:
tasks:
- name: task1
debug: "msg='hello'"
loop: [1,2]
- name: task2
debug: "msg='world'"
loop: [1,2]
這看上去沒有語法錯誤,但明顯已經違背了我們的期望,所以Ansible在解析階段就檢測這種不合理行爲。
(2).使用include_tasks
時,其加載文件內定義的變量不能在調用它的外部(即父級任務)使用。
例如下面when
指令結合include_xxx
的示例。
在b.yml中有兩個任務,都定義了num變量。
---
- name: task1
debug:
msg: "{{num}}"
vars:
num: 4
- name: task2
debug:
msg: "{{num}}"
vars:
num: 2
在a.yml中使用include_tasks
去加載b.yml,並加上when
或其它task級別的指令來使用變量num
:
---
- name: play1
hosts: localhost
gather_facts: false
tasks:
- name: task1
include_tasks: b.yml
when: num > 3
這會報錯,提示使用了未定義變量,各位可自行測試並觀察一下。注意這不是語法錯誤(即playbook的解析階段是成功的),而是執行這個任務時的運行時錯誤。報錯的原因在於執行該任務時,會先解析task級別的指令when
,然後再執行模塊任務,所以解析when
條件的時候,b.yml文件尚未加載。
如果將include_tasks
替換成import_tasks
則不會出錯。因爲在使用import_tasks
時是將when
指令複製到b.yml中的所有任務上,所以playbook解析完後等價於:
---
- name: play1
hosts: localhost
gather_facts: false
tasks:
- name: task1
debug:
msg: "{{num}}"
vars:
num: 4
when: num > 3
- name: task2
debug:
msg: "{{num}}"
vars:
num: 2
when: num > 3
(3).當在handlers
指令中通過include_tasks
和import_tasks
加載任務文件時,在notify
指令中指定handler名稱的方式不同。
例如,h.yml文件中定義了兩個handler等待被notify,其內容如下:
---
- name: handler1
debug: 'msg="hello handler"'
- name: handler2
debug: 'msg="world handler"'
在a.yml中定義了兩個任務,都觸發剛纔定義的兩個handler,但因爲使用不同指令來加載h.yml,使得notify指令中的名稱也不一樣。
tasks:
- name: task1
debug: 'msg="hello task"'
changed_when: true
notify: "notify me"
- name: task2
debug: 'msg="world task"'
changed_when: true
notify:
- "handler1"
- "handler2"
handlers:
- name: notify me
include_tasks: h.yml
- name: dont notify me
import_tasks: h.yml
如果按照前面所描述的,將import_tasks
加載的文件內容替換到a.yml中,再去理解爲何notify的名稱不同就很容易了。如下是替換後的內容:
handlers:
- name: notify me
include_tasks: h.yml
- name: handler1
debug: 'msg="hello handler"'
- name: handler2
debug: 'msg="world handler"'
經過這幾個示例,我想各位已經意識到了,使用include_tasks
時,這個指令自身佔用一個任務,使用import_tasks
的時候,這個指令自身沒有任務,它所在的任務會在解析playbook的時候被其加載的子任務覆蓋。
(4).play的name指令中如果使用了變量,則這個變量必須是在解析playbook時就已經定義好的。換句話說,play名稱中的變量不能是task中定義的變量。這應該不難理解。
(5).無法使用--list-tags
列出include_xxx
中的tags,無法使用--list-tasks
列出include_xxx
中的任務,因爲它們都是臨時動態加載的。
還有其它一些需要小心的陷阱,不過在知道它們的行爲之後,便能夠分析使用哪個指令以及在遇到錯誤的時候知道如何排查。