1. 程式人生 > >Ansible PlayBook語法(4)

Ansible PlayBook語法(4)

Ansible是新出現的自動化運維工具,基於Python開發,集合了眾多運維工具(puppet、cfengine、chef、func、fabric)的優點,實現了批量系統配置、批量程式部署、批量執行命令等功能,ansible是基於模組工作的,本身沒有批量部署的能力,真正具有批量部署的是ansible所執行的模組,ansible只是提供一種框架.

Playbook與Ad-Hoc相比,是一種完全不同的運用方式,而且是非常強大的,也是系統ansible命令的集合,其利用yaml語言編寫,執行過程ansbile-playbook命令根據自上而下的順序依次執行,簡單來說Playbook是一種簡單的配置管理系統與多機器部署系統的基礎,與現有的其他系統有不同之處,且非常適合於複雜應用的部署.



PlayBook語法例項

playbook是由一個或多個play組成的列表,play的主要功能在於將事先歸併為一組的主機裝扮成事先通過Ansible中的tasks定義好的角色(play的內容被稱為tasks,即任務),從根本上來講所謂tasks無非是呼叫Ansible的一個module,將多個play組織在一個playbook中即可以讓它們聯同起來按事先編排的機制一同工作.

下面來看一個基礎的playbook,其中只包含一個tasks任務:

---
- hosts: web_server                   #指定執行的主機組
  vars:                               #聲明瞭2個變數
    http_port: 80
    max_clients: 200
  remote_user: root                   #使用root許可權執行

  tasks:
  - name: ensure apache is at the latest version
    yum: pkg=httpd state=latest
  - name: write the apache config file  #將apache配置檔案拷貝到指定主機
    template: src=/etc/httpd/conf/httpd.conf dest=/etc/httpd.conf
    notify:                             #當上面拷貝工作完成以後,執行訊息傳送
    - restart apache
  - name: ensure apache is running      #設定開機自啟動
    service: name=httpd state=started

  handlers:
    - name: restart apache             #接收訊息,並執行重啟apache服務
      service: name=httpd state=restarted

第一行中,檔案開頭為---,這是YAML將檔案解釋為正確的文件的要求,YAML允許多個文件存在於一個檔案中,每個文件由 --- 符號分割,但Ansible只需要一個檔案存在一個文件即可,因此這裡需要存在於檔案的開始行第一行.

YAML對空格非常敏感,並使用空格來將不同的資訊分組在一起,在整個檔案中應該只使用空格而不使用製表符,並且必須使用一致的間距,才能正確讀取檔案,相同縮排級別的專案被視為同級元素.

以 - 開頭的專案被視為列表專案.作為雜湊或字典操作,它具有key:value格式的項,YAML文件基本上定義了一個分層的樹結構,其中位於左側是包含的元素.YAML副檔名通常為.yaml或者.yml,接下來,我們將分別講解 playbook 語言的多個特性.

PlayBook構成部分

◆Hosts(主機)與Users(使用者)◆

我們可以為playbook中的每一個play,個別的選擇操作的目標機器是哪些,以哪個使用者身份去完成要執行的步驟(called tasks)

---
- hosts: web_servers
  remote_user: root
  tasks:
    - name: test connection
      remote_user: yourname
      sudo: yes

playbook中的每一個play的目的都是為了讓某個或某些主機以某個指定的使用者身份執行任務.

命 令 參 數 參 數 解 釋
hosts 指定要執行指定任務的主機
remote_user 指定遠端主機上的執行任務的使用者,此處的意思是以root身份執行
user 與remote_user相同
sudo 如果設定為yes執行該任務組的使用者在執行任務的時候,獲取root許可權
sudo_user 指定使用那個使用者授權執行
connection 通過什麼方式連線到遠端主機,預設為ssh
gather_facts setup模組預設自動執行

◆Tasks 和 Action◆

每一個play包含了一個tasks列表(任務列表),任務列表中的各任務按次序逐個在hosts中指定的所有主機上執行即在所有主機上完成第一個任務後再開始第二個,在自上而下執行某playbook時如果中途發生錯誤,所有已執行任務都將回滾,因此在更正playbook後重新執行即可.

每一個tasks必須有一個名稱name,這樣在執行playbook時,從其輸出的任務執行資訊中可以很好的辨別出是屬於哪一個tasks的,如果沒有定義name,action的值將會用作輸出資訊中標記特定的tasks.tasks的目的是使用指定的引數執行模組,而在模組引數中可以使用變數.模組執行是冪等的,這意味著多次執行是安全的,因為其結果均一致.

每個tassk都應該有其name用於playbook的執行結果輸出,建議其內容儘可能清晰地描述任務執行步驟,如果未提供name則action的結果將用於輸出.

下面是一種基本的taska的定義,service moudle使用key=value格式的引數,這也是大多數module使用的引數格式:

tasks:
 - name: make sure apache is running
   service: name=httpd state=running

[在眾多模組中,只有command和shell模組僅需要給定一個列表而無需使用“key=value”格式如下]

tasks:
 - name: disable selinux
   command: /sbin/setenforce 0

[使用command module和shell module時,我們需要關心返回碼資訊,如果有一條命令,它的成功執行的返回碼不是0我們可以這樣做]
tasks:
 - name: run this command and ignore the result
   shell: /usr/bin/somecommand || /bin/true

[或者使用ignore_errors來忽略錯誤資訊]
tasks:
  - name: run this command and ignore the result
    shell: /usr/bin/somecommand
    ignore_errors: True

[如果action行看起來太長,可以使用space(空格)或者indent(縮排)隔開連續的一行]
tasks:
  - name: Copy ansible inventory file to client
    copy: src=/etc/ansible/hosts dest=/etc/ansible/hosts owner=root group=root mode=0644

◆Handlers 發生改變後執行◆

上面我們曾提到過,module具有“冪等”性,所以當遠端主機被人改動時,可以重放playbooks達到恢復的目的.playbooks本身可以識別這種改動,並且有一個基本的event system(事件系統),可以響應這種改動.

當發生改動時notify這個actions會在playbook的每一個tasks結束時被觸發,而且即使有多個不同的tasks通知改動的發生,notify actions只會被觸發一次.這樣可以避免多次有改變發生時每次都執行指定的操作,取而代之僅在所有的變化發生完成後一次性地執行指定操作.

下面一個小例子,當一個檔案的內容被改動時,重啟兩個services程序.

- name: template configuration file
  template: src=template.conf dest=/etc/foo.conf
  notify:
     - restart memcached
     - restart apache

handlers也是一些tasks的列表,通過名字來引用,它們和一般的tasks並沒有什麼區別handlers是由通知者進行notify,如果沒有被notify,handlers不會執行.不管有多少個通知者進行了notify,等到play中的所有tasks執行完成之後,handlers也只會被執行一次.

下面是一個 handlers 的示例:

handlers:
    - name: restart memcached
      service:  name=memcached state=restarted
    - name: restart apache
      service: name=apache state=restarted

Handlers最佳的應用場景是用來重啟服務,或者觸發系統重啟操作,除此以外很少用到了.

◆幾個常用小例項◆

1.通過playbook新增使用者,給遠端主機新增使用者lyshark.

---
- name: create user
  hosts: all
  user: root
  gather_facts: False   #在執行命令前是否去獲取setup資訊
  vars:
  - user: lyshark

  tasks:
  - name: create user
    user: name={{ user }}

2.通過playbook刪除使用者,刪除遠端主機lyshark的賬號.

---
- name: create user
  hosts: all
  user: root
  gather_facts: False
  vars:
  - user: lyshark
  tasks:
  - name: create user
    user: name={{ user }} state=absent remove=yes

3.安裝 httpd 服務,將本地準備好的配置檔案 copy 過去,並且啟動服務.

---
- hosts: all
  remote_user: root
  gather_facts: False
  tasks:
    - name: install apache on CentOS 7
      yum: name=httpd state=present
    - name: copy httpd conf
      copy: src=/etc/httpd/conf/httpd.conf dest=/etc/httpd/conf/httpd.conf
    - name: start apache service
      service: name=httpd state=started 

4.通過playbook 安裝apache修改埠,並帶有vars變數.

---
- hosts: all
  remote_user: root
  gather_facts: False
  vars:
    src_http_dir: /etc/httpd/conf
    dest_http_dir: /etc/httpd/conf

  tasks:
    - name: install apache on CentOS 7
      yum: name=httpd state=present
    - name: copy httpd conf
      copy: src={{ src_http_dir }}/httpd.conf dest={{ dest_http_dir }}/httpd.conf
      notify:
        - systemctl restart httpd
    - name: start apache service
      service: name=httpd state=restarted

  handlers:                      #當httpd.conf檔案發生改變時,就觸發handler進行服務重啟.
    - name: restart apache
      service: name=httpd state=restarted


PlayBook常用模組

Playbook的模組與在Ansible命令列下使用的模組有一些不同.這主要是因為在playbook中會使用到一些facts變數和一些通過setup模組從遠端主機上獲取到的變數,有些模組沒法在命令列下執行,就是因為它們需要這些變數.而且即使那些可以在命令列下工作的模組也可以通過playbook的模組獲取一些更高階的功能.

◆template模組◆

在實際應用中,我們的配置檔案有些地方可能會根據遠端主機的配置的不同而有稍許的不同,template可以使用變數來接收遠端主機上setup收集到的facts資訊,針對不同配置的主機,定製配置檔案,用法大致與copy模組相同.

命 令 參 數 參 數 解 釋
attributes 檔案或目錄的屬性
backup 如果原目標檔案存在,則先備份目標檔案
block_end_string 標記塊結束的字串
block_start_string 標記塊的開始的字串
dest 目標檔案路徑
follow 是否遵循目標中的檔案連結
force 是否強制覆蓋,預設為yes
group 目標檔案或目錄的所屬組
owner 目標檔案或目錄的所屬主
mode 目標檔案的許可權
newline_sequence 指定用於模板檔案的換行符序列
src 源模板檔案路徑
trim_blocks 如果這設定為True,則刪除塊後的第一個換行符
validate 在複製之前通過命令驗證目標檔案,如果驗證通過則複製
variable_end_string 標記列印語句結束的字串
variable_start_string 標記列印語句開頭的字串
[官方簡單示例]

- template: src=/mytemplates/foo.j2 dest=/etc/file.conf owner=bin group=wheel mode=0644
- template: src=/mytemplates/foo.j2 dest=/etc/file.conf owner=bin group=wheel mode="u=rw,g=r,o=r"
- template: src=/mine/sudoers dest=/etc/sudoers validate='visudo -cf %s'

[playbook的引用該模板配置檔案的方法示例]
- name: Setup BIND
  host: all
  tasks:
    - name: configure BIND
      template: src=/etc/httpd/conf/httpd.conf dest=/etc/httpd/conf/httpd.conf owner=root group=root mode=0644

[或者是下面這樣寫,作用是相同的]
- template:
    src: /etc/httpd/conf/httpd.conf
    dest: /etc/file.conf
    owner: root
    group: root
    mode: 0644

[建立DOS樣式文字檔案]
- template:
    src: config.ini.j2
    dest: /share/windows/config.ini
    newline_sequence: '\r\n'

◆set_fact模組◆

set_fact模組可以自定義facts,這些自定義的facts可以通過template或者變數的方式在playbook中使用,如果你想要獲取一個程序使用的記憶體的百分比,則必須通過set_fact來進行計算之後得出其值,並將其值在playbook中引用.

[[email protected] ~]# echo "# Configure the buffer pool" >> /etc/my.cnf
[[email protected] ~]# echo "innodb_buffer_pool_size = {{ innodb_buffer_pool_size_mb|int }}M" >> /etc/my.cnf
[[email protected] ~]# vim set_fact.yml
---
- name: Configure Mariadb
  hosts: all
  tasks:
    - name: install Mariadb
      yum: name=mariadb-server state=installed

    - name: Calculate InnoDB buffer pool size
      set_fact: innodb_buffer_pool_size_mb={{ ansible_memtotal_mb / 2 }}
    - name: Configure Mariadb
      template: src=/etc/my.cnf dest=/etc/my.cnf owner=root group=root mode=0644

    - name: Start Mariadb
      service: name=mariadb state=started enabled=yes
  handlers:
    - name: restart Mariadb
      service: name=mariadb state=restarted

◆pause模組◆

在playbook執行的過程中暫停一定時間或者提示使用者進行某些操作,要為每個主機暫停、等待、休眠,可以使用wait_for模組,如果您想提前暫停而不是設定為過期,或者您需要完全中止劇本執行.

命 令 參 數 參 數 解 釋
echo 控制鍵入時是否顯示鍵盤輸入
minutes 暫停多少分鐘
seconds 暫停多少秒
prompt 列印一串資訊提示使用者操作
[暫停5分鐘以建立應用程式快取]
- pause:
    minutes: 5

[有用的提醒,提醒您注意更新後的內容]
- pause:
    prompt: "Make sure org.foo.FooOverload exception is not present"

[暫停以後獲得一些資訊]
- pause:
    prompt: "Enter a secret"
    echo: no

◆wait_for模組◆

wait_for模組是在playbook的執行過程中,等待某些操作完成以後再進行後續操作.

命 令 參 數 參 數 解 釋
active_connection_states 被計為活動連線的TCP連線狀態列表
connect_timeout 在下一個任務執行之前等待連線的超時時間
delay 等待一個埠或者檔案或者連線到指定的狀態
exclude_hosts 在查詢狀態的活動TCP連線時要忽略的主機或IP的列表drained
host wait_for模組等待的主機的地址,預設為127.0.0.1
msg 這會覆蓋正常的錯誤訊息,使其不符合所需的條件
port wait_for模組等待的主機的埠
path 檔案路徑,只有當這個檔案存在時,下一任務才開始執行,即等待該檔案建立完成
search_regex 可以用來匹配檔案或套接字連線中的字串,預設為多行正則表示式
sleep 檢查之間睡眠的秒數,在2.3之前,這被硬編碼為1秒
state 等待的狀態,狀態有started,stoped,present或started,absent
timeout wait_for的等待的超時時間,預設為300秒
- name: create task
  hosts: all
  tasks:

    [等待8080埠已正常監聽,才開始下一個任務,直到超時]
    - wait_for: port=8080 state=started
    [等待8000埠正常監聽,每隔10s檢查一次,直至等待超時]
    - wait_for: port=8081 delay=10
    [等待8000埠直至有連線建立]
    - wait_for: host=0.0.0.0 port=8000 delay=10 state=drained
    [等待8000埠有連線建立,如果連線來自192.168.8.66或者192.168.8.8則忽略]
    - wait_for: host=0.0.0.0 port=8000 state=drained exclude_hosts=192.168.8.66,192.168.8.8
    [等待/tmp/foo檔案已建立]
    - wait_for: path=/tmp/foo
    [等待/tmp/foo檔案已建立,而且該檔案中需要包含completed字串]
    - wait_for: path=/tmp/foo search_regex=completed
    [等待/var/lock/file.lock被刪除]
    - wait_for: path=/var/lock/file.lock state=absent
    [等待指定的程序被銷燬]
    - wait_for: path=/proc/3466/status state=absent
    [等待openssh啟動,10s檢查一次]
    - local_action: wait_for port=22 host="{{ ansible_ssh_host | default(inventory_hostname) }}" search_regex=OpenSSH delay=10

◆assemble模組◆

assemble模組用於組裝檔案,即將多個零散的檔案(稱為碎片),合併一個目標檔案.

命 令 參 數 參 數 解 釋
attributes 檔案或目錄應的屬性
backup 建立一個備份檔案(如果yes),包括時間戳資訊
decrypt 控制使用保管庫對原始檔進行自動解密
delimiter 分隔檔案內容的分隔符
dest 使用所有原始檔的連線建立的檔案,合併後的大檔案路徑
group 合併後的大檔案的所屬組
owner 合併後的大檔案的所屬主
ignore_hidden 組裝時,是否忽略隱藏檔案,預設為no
mode 合併後的大檔案的許可權,指定檔案許可權
regexp 在regex匹配檔名時彙編檔案
src 原始檔(即零散檔案)的路徑
validate 與template的validate相同,指定命令驗證檔案
[指定原始檔(即零散檔案)的路徑,合併一個目標檔案someapp.conf]
- assemble:
    src: /etc/someapp/fragments
dest: /etc/someapp/someapp.conf

[指定一個分隔符,將在每個片段之間插入]
- assemble:
    src: /etc/someapp/fragments
    dest: /etc/someapp/someapp.conf
delimiter: '### START FRAGMENT ###'

[在SSHD傳遞驗證後,將新的sshd_config檔案複製到目標地址]
- assemble:
    src: /etc/ssh/conf.d/
    dest: /etc/ssh/sshd_config
    validate: '/usr/sbin/sshd -t -f %s'

◆add_host模組◆

add_host模組使用變數在清單中建立新的主機組,以便在以後的相同劇本中使用.獲取變數以便我們可以更充分地定義新主機,add_host模組在playbook執行的過程中,動態的新增主機到指定的主機組中.

[新增主機到webservers組中,主機的變數foo的值為42]
- name: add host to group 'just_created' with variable foo=42
  add_host:
    name: "{{ ip_from_ec2 }}"
    groups: just_created
    foo: 42

[將主機新增到多個組]
- name: add host to multiple groups
  add_host:
    hostname: "{{ new_ip }}"
    groups:
      - group1
      - group2

[向主機新增一個非本地埠的主機]
- name: add a host with a non-standard port local to your machines
  add_host:
name: "{{ new_ip }}:{{ new_port }}"

[新增一個通過隧道到達的主機別名]
- name: add a host alias that we reach through a tunnel (Ansible <= 1.9)
  add_host:
    hostname: "{{ new_ip }}"
    ansible_ssh_host: "{{ inventory_hostname }}"
ansible_ssh_port: "{{ new_port }}"

[新增一個通過隧道到達的主機別名]
- name: add a host alias that we reach through a tunnel (Ansible >= 2.0)
  add_host:
    hostname: "{{ new_ip }}"
    ansible_host: "{{ inventory_hostname }}"
    ansible_port: "{{ new_port }}"

◆group_by模組◆

group_by模組在playbook執行的過程中,動態的建立主機組.

[建立主機組]
- group_by:
    key: machine_{{ ansible_machine }}

[建立類似kvm-host的主機組]
- group_by:
    key: virt_{{ ansible_virtualization_type }}_{{ ansible_virtualization_role }}

[建立巢狀主機組]
- group_by:
    key: el{{ ansible_distribution_major_version }}-{{ ansible_architecture }}
    parents:
      - el{{ ansible_distribution_major_version }}

◆debug模組◆

debug模組在執行過程中列印語句,可用於除錯變數或表示式中輸出資訊.

[為每個主機列印IP地址和閘道器]
- debug:
    msg: "System {{ inventory_hostname }} has uuid {{ ansible_product_uuid }}"

- debug:
    msg: "System {{ inventory_hostname }} has gateway {{ ansible_default_ipv4.gateway }}"
   when: ansible_default_ipv4.gateway is defined

- shell: /usr/bin/uptime
  register: result

- debug:
    var: result       [直接將上一條指令的結果作為變數傳遞給var,由debug打印出result的值]
    verbosity: 2

- name: Display all variables/facts known for a host
  debug:
    var: hostvars[inventory_hostname]
    verbosity: 4

◆fail模組◆

fail模組用於終止當前playbook的執行,通常與條件語句組合使用,當滿足條件時,終止當前play的執行,也可以直接由failed_when取代.

[執行失敗錢打印出自定義資訊]
- fail:
    msg: "The system may not be provisioned according to the CMDB status."
  when: cmdb_status != "to-be-staged"


PlayBook條件判斷

在有的時候play的結果依賴於變數,fact或者是前一個任務的執行結果,從而需要使用到條件語句.

◆when◆

有的時候在特定的主機需要跳過特定的步驟,例如在安裝包的時候,需要指定主機的作業系統型別,或者是當作業系統的硬碟滿了之後,需要清空檔案等,可以使用when語句來做判斷.when關鍵字後面跟著的是python的表示式,在表示式中你能夠使用任何的變數或者fact,當表示式的結果返回的是false,便會跳過本次的任務.

when基本用法

---
- name: Install VIM
  hosts: all  tasks:
    - name:Install VIM via yum
      yum: name=vim-enhanced state=installed
      when: ansible_os_family =="RedHat"
    - name:Install VIM via apt
      apt: name=vim state=installed
      when: ansible_os_family =="Debian"
    - name: Unexpected OS family
      debug: msg="OS Family {{ ansible_os_family }} is not supported" fail=yes
      when: not ansible_os_family =="RedHat" or ansible_os_family =="Debian"

條件語句還有一種用法,它還可以讓你當達到一定的條件的時候暫停下來,等待你的輸入確認.一般情況下,當ansible遭遇到error時,它會直接結束執行.那其實你可以當遭遇到不是預期的情況的時候給使用pause模組,這樣可以讓使用者自己決定是否繼續執行任務:

- name: pause for unexpected conditions
  pause: prompt="Unexpected OS"
  when: ansible_os_family !="RedHat"

在when中使用jinja2的語法

tasks:
  - command: /bin/false
    register: result        # 將命令執行的結果傳遞給result變數
    ignore_errors: True     # 忽略錯誤
  - command: /bin/something
    when: result|failed     # 如果註冊變數的值 是任務failed則返回true
  - command: /bin/something_else
    when: result|success    # 如果註冊變數的值是任務success則返回true
  - command: /bin/still/something_else
    when: result|skipped    # 如果註冊變數的值是任務skipped則返回true
  - command: /bin/foo
    when: result|changed    # 如果註冊變數的值是任務changed則返回true
- hosts: all
  user: root
  vars:
    epic: true
  tasks:    
     - shell: echo "This certainly is epic!"      
       when: epic
     - shell: echo "This certainly is not epic!"
       when: not epic

如果變數不存在,則可以通過jinja2的'defined'命令跳過

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 not defined

when在迴圈語句中的使用方法

tasks:
    - command: echo {{ item }}
      with_items: [ 0, 2, 4, 6, 8, 10 ]
      when: item > 56    #在include和roles中使用when:
# 在include中使用的示例:- include: tasks/sometasks.yml
      when: "'reticulating splines' in output"
# 在roles中使用的示例:- hosts: webservers
      roles:
         - { role: debian_stock_config, when: ansible_os_family == 'Debian' }

◆條件匯入◆

有些時候,你也許想在一個Playbook中以不同的方式做事,比如說在debian和centos上安裝apache,apache的包名不同,除了when語句,還可以使用下面的示例來解決:

---
- hosts: all
  remote_user: root
  vars_files:
    - "vars/common.yml"
    - [ "vars/{{ ansible_os_family }}.yml", "vars/os_defaults.yml" ]
  tasks:
  - name: make sure apache is running
    service: name={{ apache }} state=running

很多不同的yml檔案只是包含鍵和值,如下:

[[email protected] ~]# for vars/CentOS.yml
apache: httpd
somethingelse: 42

如果作業系統是“CentOS”,Ansible匯入的第一個檔案將是“vars/CentOS.yml”,緊接著是“/var/os_defaults.yml”,如果這個檔案不存在.而且在列表中沒有找到,就會報錯.在Debian系統中,最先檢視的將是“vars/Debian.yml”而不是“vars/CentOS.yml”,如果沒找到,則尋找預設檔案“vars/os_defaults.yml”.

◆with_first_found◆

有些時候,我們想基於不同的作業系統,選擇不同的配置檔案,及配置檔案的存放路徑,可以藉助with_first_found來解決:

- name: template a file
   template: src={{ item }} dest=/etc/myapp/foo.conf
   with_first_found:
     - files:
        - {{ ansible_distribution }}.conf
        - default.conf
       paths:
        - search_location_one/somedir/
        - /opt/other_location/somedir/

◆failed_when◆

failed_when其實是ansible的一種錯誤處理機制,是由fail模組使用了when條件語句的組合效果.示例如下:

- name: this command prints FAILED when it fails
  command: /usr/bin/example-command -x -y -z
  register: command_result
  failed_when: "'FAILED' in command_result.stderr"

我們也可以直接通過fail模組和when條件語句,寫成如下:

- name: this command prints FAILED when it fails
  command: /usr/bin/example-command -x -y -z
  register: command_result
  ignore_errors: True

- name: fail the play if the previous command did not succeed
  fail: msg="the command failed"
  when: "'FAILED' in command_result.stderr"

◆changed_when◆

當我們控制一些遠端主機執行某些任務時,當任務在遠端主機上成功執行,狀態發生更改時,會返回changed狀態響應,狀態未發生更改時,會返回OK狀態響應,當任務被跳過時,會返回skipped狀態響應.我們可以通過changed_when來手動更改changed響應狀態,示例如下:

- shell: /usr/bin/billybass --mode="take me to the river"
  register: bass_result
  changed_when: "bass_result.rc != 2"    # 只有該條task執行以後,bass_result.rc的值不為2時,才會返回changed狀態

# 永遠不會報告“改變”的狀態
- shell: wall 'beep'
  changed_when: False    # 當changed_when為false時,該條task在執行以後,永遠不會返回changed狀態


PlayBook迴圈語句

在使用Ansible做自動化運維的時候,免不了的要重複執行某些操作,如:新增幾個使用者,建立幾個MySQL使用者併為之賦予許可權,操作某個目錄下所有檔案等等.好在playbook支援迴圈語句,可以使得某些需求很容易而且很規範的實現.

◆with_items◆

with_items是playbooks中最基本也是最常用的迴圈語句.

tasks:
- name:Secure config files
    file: path=/etc/{{ item }} mode=0600 owner=root group=root
    with_items:
        - my.cnf
        - shadow
        - fstab
    # 或
    with_items:"{{ somelist }}"

上面的例子說明在/etc下建立許可權級別為0600,屬主屬組都是root三個檔案,分別為my.cnf、shadow、fstab.
使用with_items迭代迴圈的變數可以是個單純的列表,也可以是一個較為複雜的資料結果,如字典型別:

tasks:
- name: add several users
  user: name={{ item.name }} state=present groups={{ item.groups }}
  with_items:
    - { name: 'testuser1', groups: 'root' }
    - { name: 'testuser2', groups: 'root' }

◆with_nested巢狀迴圈◆

tasks:
- name: give users access to multiple databases
  mysql_user: name={{ item[0] }} priv={{ item[1] }}.*:ALL append_privs=yes password=foo
  with_nested:
    - [ 'alice', 'bob' ]
    - [ 'clientdb', 'employeedb', 'providerdb' ]

item[0]是迴圈的第一個列表的值['alice','bob'].
item[1]是第二個列表的值,表示迴圈建立alice和bob兩個使用者,並且為其賦予在三個資料庫上的所有許可權.

也可以將使用者列表事先賦值給一個變數:

tasks:
- name: here, 'users' contains the above list of employees
  mysql_user: name={{ item[0] }} priv={{ item[1] }}.*:ALL append_privs=yes password=foo
  with_nested:
    - "{{users}}"
    - [ 'clientdb', 'employeedb', 'providerdb' ]

◆with_dict◆

with_dict可以遍歷更復雜的資料結構,假如有如下變數內容:

users:
  alice:
    name: Alice Appleworth
    telephone: 123-456-7890
  bob:
    name: Bob Bananarama
    telephone: 987-654-3210

現在需要輸出每個使用者的使用者名稱和手機號:

tasks:
  - name: Print phone records
    debug: msg="User {{ item.key }} is {{ item.value.name }} ({{ item.value.telephone }})"
    with_dict: "{{ users }}"

◆with_fileglob檔案匹配遍歷◆

可以指定一個目錄,使用with_fileglob可以迴圈這個目錄中的所有檔案,示例如下:

tasks:
- name:Make key directory 
      file: path=/root/.sshkeys ensure=directory mode=0700 owner=root group=root     
- name:Upload public keys 
      copy: src={{ item }} dest=/root/.sshkeys mode=0600 owner=root group=root     
      with_fileglob:
        - keys/*.pub
- name:Assemble keys into authorized_keys file
      assemble: src=/root/.sshkeys dest=/root/.ssh/authorized_keysmode=0600 owner=root group=root

◆with_subelement遍歷子元素◆

假如現在需要遍歷一個使用者列表,並建立每個使用者,而且還需要為每個使用者配置以特定的SSH key登入,變數檔案內容如下:

users:
  - name: alice
    authorized:
      - /tmp/alice/onekey.pub
      - /tmp/alice/twokey.pub
    mysql:
        password: mysql-password
        hosts:
          - "%"
          - "127.0.0.1"
          - "::1"
          - "localhost"
        privs:
          - "*.*:SELECT"
          - "DB1.*:ALL"
  - name: bob
    authorized:
      - /tmp/bob/id_rsa.pub
    mysql:
        password: other-mysql-password
        hosts:
          - "db1"
        privs:
          - "*.*:SELECT"
          - "DB2.*:ALL"

[playbook中定義如下:]
- user: name={{ item.name }} state=present generate_ssh_key=yes
  with_items: "`users`"
- authorized_key: "user={{ item.0.name }} key='{{ lookup('file', item.1) }}'"
  with_subelements:
     - users
     - authorized

[也可以遍歷巢狀的子列表:]
- name: Setup MySQL users
  mysql_user: name={{ item.0.name }} password={{ item.0.mysql.password }} host={{ item.1 }} priv={{ item.0.mysql.privs | join('/') }}
  with_subelements:
    - users
- mysql.hosts

◆with_sequence迴圈整數序列◆

with_sequence可以生成一個自增的整數序列,可以指定起始值和結束值,也可以指定增長步長. 引數以key=value的形式指定,format指定輸出的格式.數字可以是十進位制、十六進位制、八進位制:

- hosts: all
  tasks:
    # create groups
    - group: name=evens state=present
    - group: name=odds state=present
    # create some test users
    - user: name={{ item }} state=present groups=evens
      with_sequence: start=0 end=32 format=testuser%02d
    # create a series of directories with even numbers for some reason
    - file: dest=/var/stuff/{{ item }} state=directory
      with_sequence: start=4 end=16 stride=2    # stride用於指定步長
    # a simpler way to use the sequence plugin
    # create 4 groups
    - group: name=group{{ item }} state=present
      with_sequence: count=4

◆with_random_choice隨機選擇◆

從列表中隨機取一個值:

- debug: msg={{ item }}
  with_random_choice:
     - "go through the door"
     - "drink from the goblet"
     - "press the red button"
     - "do nothing"

◆do-Util迴圈◆

重複執行shell模組,當shell模組執行的命令輸出內容包含"all systems go"的時候停止,重試5次,延遲時間10秒.retries預設值為3,delay預設值為5,任務的返回值為最後一次迴圈的返回結果.

- action: shell /usr/bin/foo
  register: result
  until: result.stdout.find("all systems go") != -1
  retries: 5
  delay: 10

◆迴圈註冊變數◆

在迴圈中使用register時,儲存的結果中包含results關鍵字,該關鍵字儲存模組執行結果的列表.

---
 - hosts: all
   tasks:
     - shell: echo "{{ item }}"
      with_items:
          - one
          - two
      register: echo

變數內容如下:

TASK [command] *******************************************************
skipping: [192.168.10.20] => (item=one)
skipping: [192.168.10.20] => (item=two)

PLAY RECAP ***********************************************************
192.168.10.20              : ok=1    changed=0    unreachable=0    failed=0

遍歷註冊變數的結果:

- name: Fail if return code is not 0
  fail:
    msg: "The command ({{ item.cmd }}) did not have a 0 return code"
  when: item.rc != 0
  with_items: "`echo`.`results`"

◆with_together遍歷資料並行集合◆

- hosts: all
  remote_user: root
  vars:
    alpha: [ 'a','b','c','d']
    numbers: [ 1,2,3,4 ]
  tasks:
    - debug: msg="{{ item.0 }} and {{ item.1 }}"
      with_together:
         - "{{ alpha }}"
         - "{{ numbers }}"

輸出執行結果如下:

ok: [192.168.10.20] => (item=[u'a', 1]) => {
    "changed": false,
    "item": [
        "a",
        1
    ],
    "msg": "a and 1"
}
ok: [192.168.10.20] => (item=[u'b', 2]) => {
    "changed": false,
    "item": [
        "b",
        2
    ],
    "msg": "b and 2"
}
ok: [192.168.10.20] => (item=[u'c', 3]) => {
    "changed": false,
    "item": [
        "c",
        3
    ],
    "msg": "c and 3"
}
ok: [192.168.10.20] => (item=[u'd', 4]) => {
    "changed": false,
    "item": [
        "d",
        4
    ],
    "msg": "d and 4"
}


PlayBook部署實驗

Ansible的PlayBook檔案格式為YAML語言,所以希望讀者在編寫PlayBook前對YAML語法有一定的瞭解,否則在執行PlayBook的時候經常碰到語法錯誤提示,這裡我們通過介紹批量部署apache服務為例,介紹一下apache.yaml這個PlayBook的具體應用寫法,如果你對YAML語言沒有了解的話,請自行去百度學習.

1.首先在當前目錄下建立一個目錄,用來儲存與apache相關的配置檔案和程式,這裡要注意的是,我們應該在本機搭建一個環境,除錯好後再進行部署,除錯環節此處略過.

[[email protected] ~]# mkdir playbook

[[email protected] ~]# ll
total 0
drwxr-xr-x. 2 root root 6 Dec  2 06:43 playbook

[[email protected] playbook]# cp -a /etc/httpd/conf/httpd.conf ./
[[email protected] playbook]# cp -a /var/www/html/index.html  ./
[[email protected] playbook]# ls -l
total 16
-rw-r--r--. 1 root root 11755 Dec  2 06:46 httpd.conf
-rw-r--r--. 1 root root    15 Dec  2 06:44 index.html

2.通過編寫一個apache.yaml劇本,來實現批量部署的案例,這裡是Yum安裝所以很簡單,首先我們先來看一下這個PlayBook的程式碼:

[[email protected] playbook]# cat apache.yaml

  1 ---                                        #表示該檔案是YAML檔案,非必需
  2 - hosts: all                               #playbook針對的主機,all表示所有
  3   tasks:                                   #表示一個集合
  4    - name: Install httpd                   #說明資訊,安裝httpd服務
  5      yum: name=httpd state=present         #通過YUM模組安裝資訊
  6
  7    - name: Copy httpd.conf                 #說明資訊,複製配置檔案
  8      template: src=./httpd.conf dest=/etc/httpd/conf/httpd.conf owner=root group=root mode=0755
  9
 10    - name: Copy index.html                 #說明檔案,複製網頁目錄
 11      template: src=./index.html dest=/var/www/html/index.html owner=root group=root mode=0755
 12      notify:                               #傳送訊息,當上一條語句執行完畢後,執行下面的模組
 13         - Restart Apache Server
 14
 15   handlers:                                #訊息傳送到這裡,指定主機執行模組
 16      - name: Restart Apache Server         #與上面的notify內容相對應
 17        service: name=httpd state=restarted #重啟httpd服務

3.當我們配置完成YAML劇本以後,再來配置一個Inventory的hosts主機檔案,也就是指定要執行此操作的主機列表,此處我們定義以下主機列表:

[[email protected] playbook]# cat hosts

[apache]                  #指定Apache這個組,組中有成員192.168.10.20/30兩個IP
192.168.10.20
192.168.10.30

#[test]
#192.168.10.1..100        #也可以這樣指定一個範圍
#192.168.10.1[0:100]      #也可以這樣寫

[apache:vars]             #指定我們使用的Python直譯器路徑
ansible_python_interpreter=/usr/bin/python2.7

4.接下來我們對apache.yaml使用 --syntax-check 命令引數,檢查一下PlayBook語法是否正確:

[[email protected] playbook]# ansible-playbook apache.yaml --syntax-check

playbook: apache.yaml

5.緊接著使用--list-task引數顯示apache.yaml,PlayBook檔案中所有的task名稱如下所示:

[[email protected] playbook]# ansible-playbook apache.yaml --list-task

playbook: apache.yaml

  play #1 (all): all    TAGS: []
    tasks:
      Install httpd     TAGS: []
      Copy httpd.conf   TAGS: []
      Copy index.html   TAGS: []

5.緊接著使用--list-hosts引數顯示apache.yaml,PlayBook檔案中所有的task名稱如下所示:

[[email protected] playbook]# ansible-playbook apache.yaml --list-hosts

playbook: apache.yaml

  play #1 (all): all    TAGS: []
    pattern: [u'all']
    hosts (2):
      192.168.10.20
      192.168.10.30

6.確認過以後,直接使用下面的命令一鍵部署,我們寫好的PlayBook劇本,此時我們等它一會.

[[email protected] playbook]# ansible-playbook -i hosts apache.yaml

PLAY RECAP ******************************************************************************
192.168.10.20              : ok=5    changed=4    unreachable=0    failed=0
192.168.10.30              : ok=5    changed=4    unreachable=0    failed=0

當然playbook還支援互動式地執行 task 我們可以指定 -step 引數即可,apache.yaml 是一個相對簡單的 Playbook 檔案,在我們的實際工作中可能會遇到各種複雜的需求,但 Playbook 的靈活性非常強大,下面我們來看幾個常用引數,如下:

---
- hosts: 192168.10.10:192.168.10.100  #指定一個主機執行範圍
  patterns
  remote_user: root              #指定遠端SSH認證使用者
  sudo: yes                      #啟用Sudo操作
  sudo_user: lyshark             #指定Sudo的使用者
  gather_facts: no               #設定facts資訊收集
  accelerate: no                 #設定accelerate模式
  accelerate_port: 5099          #設定accelerate埠
  max_fail_percentage: 30        #設定失敗百分比
  connection: local              #設定遠端連線方式
  serial: 15                     #設定變數

Ansible 的 playbook 寫法很豐富,功能很強大,只有掌握了 playbook 每一個引數之後,我們才能寫出強大而且靈活性很高的 Playbook ,這也是我們在工作中接觸和使用最多的地方.

本章內容參考自(70%轉載+30%書本知識):http://blog.51cto.com/13525470/2112720