Ansible專欄文章之六:組織多個文件以及Role


回到: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_xxximport_xxx指令:

  • include_tasks和import_tasks用於引入外部任務文件;
  • import_playbook用於引入playbook文件;
  • include可用於引入幾乎所有內容文件,但建議不要使用它;

對於handler,因爲它本身也是task,所以它也能使用include_tasksimport_tasks來引入,但是這並不是想象中那麼簡單,後文再細說。

對於variable,使用include_vars(這是核心模塊提供的功能)或其它組織方式(如vars_files),沒有對應的import_vars

對於後文要介紹的Role,使用include_roleimport_roleroles指令。

既然某類內容文件既可以使用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_tasksimport_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_tasksimport_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_tasksinclude_tasks自身也是任務,既然是任務,就能使用task層次的指令。例如下面的示例:

handlers: 
  - name: h1
    include_tasks: handler.yml
    vars: 
      my_var: my_value
    when: my_var == "my_value"

但這兩個指令對task層次指令的處理方式不同,相關細節仍然保留到後文統一解釋。

6.4 組織變量

在Ansible中有很多種定義變量的方式,想要搞清楚所有這些散佈各個角落的知識,是一個很大的難點。好在,沒必要去過多關注,只需要掌握幾個常用的變量定義和應用的方式即可。此處我要介紹的是將變量定義在外部文件中,然後去引入這些外部文件中的變量。

引入保存了變量的文件有兩種方式:include_varsvars_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_xxximport_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_tasksimport_tasks引入它:

---
- include_tasks: mytask.yml
# 或者
#- import_tasks: mytask.yml

前面已經提到過include_xxximport_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_tasksimport_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_vardefault_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_roleimport_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

注意,在rolesimport_roleinclude_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.在靜態加載文件的指令上打標籤,等價於爲所加載文件中所有子任務打標籤。在動態加載文件的指令上打標籤,不會爲子任務打標籤,而是爲父任務自身打標籤

關於靜態、動態加載,本文最後會詳細說明。現在說結論:

  • 靜態加載的指令有:rolesincludeimport_tasksimport_role
  • 動態加載的指令只有include_xxx,包括include_tasksinclude_role

import_playbookinclude_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_xxximport_xxx從其它文件中加載,它們之間有區別。本文從頭到尾,對於它們的區別,我都只是簡單重複一個結論:import_xxx是playbook解析的階段加載,include_xxx是遇到指令的時候加載。現在,我要花點篇幅去解釋解釋includerolesinclude_xxximport_xxx之間的區別。

6.10.1 playbook解析、動態加載和靜態加載

還是這個結論:

  1. import_xxx是在playbook的解析階段加載文件
  2. 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_xxxinclude指令,比如import_tasksimport_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_tasksimport_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中的任務,因爲它們都是臨時動態加載的。

還有其它一些需要小心的陷阱,不過在知道它們的行爲之後,便能夠分析使用哪個指令以及在遇到錯誤的時候知道如何排查。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章