Linux伺服器 Linux伺服器

資工所專業課程上課教材

資工所專業課程 > 課程內容 > 專題 - 使用 Ansible 進行快速佈署 - 迴圈, 條件控制, 錯誤排除等功能

專題 - 使用 Ansible 進行快速佈署 - 迴圈, 條件控制, 錯誤排除等功能

上次更新日期 2020/11/27

前面提到的 playbook 裡面,某些時刻我們會需要寫兩段一模一樣的 ansible code,只因為該 code 不接受類似陣列的項目! 無法進行重複的作業。這就好像我們在 Linux 使用 id 去檢查帳號資訊時,只能接一個參數是相同的道理。以前使用 id 時, 可以透過迴圈來處理,那麼 ansible 說是要取代 shell script 來快速佈署系統環境,沒可能沒有迴圈嘛?是的!ansible 有迴圈、有條件控制、還有錯誤克服的情境,雖然跟 shell script 差很多!語法完全不同!不過,意義倒是相同的, 就是減少重複的 code 啦!

在 playbook 使用 loop, item, when 進行迴圈與條件控制

  1. 簡單的迴圈設計:
    1. 在還不知道有迴圈可以使用時,如果你想要知道某個服務有沒有啟用,應該是要這樣做:
      # 注意身份與所在目錄!很重要!別搞混!
      $ whoami; pwd
      student
      /home/student/ansible-init
      
      # 開始編寫需要的 playbook
      $ vim loop_noloop.yml
      ---
        - name: do not use loop to show service
          hosts: webserver1
          tasks:
            - name: check httpd service status
              command:
                cmd: systemctl is-active httpd
              register: myoutput
            - name: show
              debug:
                var: myoutput.stdout
            - name: check sshd service status
              command:
                cmd: systemctl is-active sshd
              register: myoutput
            - name: show
              debug:
                var: myoutput.stdout
            - name: check tuned service status
              command:
                cmd: systemctl is-active tuned
              register: myoutput
            - name: show
              debug:
                var: myoutput.stdout
      
      # 立刻執行看看:
      $ ansible-playbook loop_noloop.yml
      
      PLAY [do not use loop to show service] ***********************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [check httpd service status] ****************************************************
      changed: [webserver1]
      
      TASK [show] **************************************************************************
      ok: [webserver1] => {
          "myoutput.stdout": "active"
      }
      
      TASK [check sshd service status] *****************************************************
      changed: [webserver1]
      
      TASK [show] **************************************************************************
      ok: [webserver1] => {
          "myoutput.stdout": "active"
      }
      
      TASK [check tuned service status] ****************************************************
      changed: [webserver1]
      
      TASK [show] **************************************************************************
      ok: [webserver1] => {
          "myoutput.stdout": "active"
      }
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=7 changed=3 unreachable=0 failed=0 skipped0    rescued=0    ignored=0
      
      一層一層執行下來,撰寫這些 playbook 覺得很煩!有沒有好一點的方案呢?
    2. 如果想要設計迴圈,可以透過『 loop: 』這個迴圈控制參數來處理。要注意喔,loop 不是模組,所以無法使用 ansible-doc 去查詢, 不過,基礎的 loop 語法可以這樣寫:
      $ vim loop_service1.yml
      ---
        - name: do not use loop to show service
          hosts: webserver1
          tasks:
            - name: check "{{item}}" service status
              command:
                cmd: systemctl is-active "{{item}}"
              loop:
                - httpd
                - sshd
                - tuned
      
      $ ansible-playbook loop_service1.yml
      
      PLAY [do not use loop to show service] ***********************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [check "{{item}}" service status] ***********************************************
      changed: [webserver1] => (item=httpd)
      changed: [webserver1] => (item=sshd)
      changed: [webserver1] => (item=tuned)
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=2  changed=1  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
      
      如上所示:
      • loop 只會帶給前一個模組有迴圈的效果
      • loop 並不是模組,也不是模組的參數
      • loop 必須與模組相同等級,所以 loop: 就會與 command 在同一個縮排處。
      • loop 內的項目,在前一個模組裡面,可以使用 "{{ item }}" 這個變數來處理。
      所以, command: 模組就會依據 loop 的設定,總共執行三次,第一次 item 變數內容為 httpd,第二次為 sshd,以此類推。
    3. 使用註冊 (register) 的變數,搭配 results 關鍵字的定義,就可以取得 loop 在各個階段所執行的訊息輸出。 以上面的範例為例,未加入 loop 以前,可以透過 register myoutput 搭配 .stdout 就可以得到輸出的資訊, 那如果透過 loop 之後,該如何處理?
      $ vim loop_service2.yml
      ---
        - name: do not use loop to show service
          hosts: webserver1
          tasks:
            - name: check "{{item}}" service status
              command:
                cmd: systemctl is-active "{{item}}"
              register: myoutput
              loop:
                - httpd
                - sshd
                - tuned
            - name: show "{{ item }}"
              debug:
                msg: output is "{{ item.stdout }}"
              loop: "{{ myoutput['results'] }}"
      
      $ ansible-playbook loop_service2.yml
      
      PLAY [do not use loop to show service] ***********************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [check {{item}} service status] *************************************************
      changed: [webserver1] => (item=httpd)
      changed: [webserver1] => (item=sshd)
      changed: [webserver1] => (item=tuned)
      
      TASK [show {{ item.item }}] **********************************************************
      ok: [webserver1] => (item={'cmd': ['systemctl', 'is-active', 'httpd'], 'stdout': 
       'active', 'stderr': '', 'rc': 0, 'start': '2020-11-11 08:29:05.922439', 'end': 
       '2020-11-11 08:29:05.931727', 'delta': '0:00:00.009288', 'changed': True, 
       'invocation': {'module_args': {'_raw_params': 'systemctl is-active "httpd"', 'warn': 
       True, '_uses_shell': False, 'stdin_add_newline': True, 'strip_empty_ends': True, 
       'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 
       'stdin': None}}, 'stdout_lines': ['active'], 'stderr_lines': [], 'failed': False, 
       'item': 'httpd', 'ansible_loop_var': 'item'}) => {
          "msg": "output is \"active\""
      }
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=3  changed=1  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
      
    4. 簡單的迴圈設計中,除了上面 loop 這個關鍵字放置的位置與用途之外,還需要注意:
      • loop 只與前一個模組有關,取用 loop 內的變數值,就得要使用 {{item}} 變數
      • 在 loop 內可以註冊輸出資料,定義某個變數名稱,例如上個案例的 myoutput
      • 搭配 debug 輸出資訊時,被註冊的變數需要使用陣列特性,使用 myoutput['results'] 取出變數內容。
      • 同樣的,要取出上述的實際資料時,就得要透過類似 item.stdout 或 item.content 之類的方式
      • 所以,要記得背誦 item 這個英文。
  2. 簡單的條件控制:
    1. 基本上,ansible 控制條件主要是透過 when 來設計的。when 的語法與 loop 有點類似,when 並不是模組,但是需要與模組同等級縮排。 而且,when 在使用變數時,似乎不需要加上 "{{ var }}" 的括號,直接使用 var 即可。when 也能調用 ansible_facts 的相關資料。 基本語法有點像這樣:
      when: ansible_facts['machine'] == "x86_64"    # 兩者相同才進行
      when: item == "httpd"                         # loop 的值為 httpd 才進行
      when: max_memory >= 1024                   # 記憶體大於 1024M 才進行
      when: min_memory is defined                   # 有 min_memory 這個變數時
      when: min_memory is not defined               # 沒有 min_memory 這個變數時
      when: some_var                                # 當 some_var 變數是 true 時
      when: not some_var                            # 當 some_var 變數是 false 時
      
    2. 簡單設計一下,當變數內容是 httpd 且 Linux 版本是 CentOS 時(注意大小寫),才重新啟動,可以這樣寫:
      $ vim condition_start_httpd.yml
      ---
        - name: restart httpd
          hosts: webserver1
          vars:
            myservice: httpd
          tasks:
            - name: restart httpd
              service:
                name: "{{ myservice }}"
                state: restarted
              when: ansible_facts['distribution'] == "CentOS" and myservice == "httpd"
      
      $ ansible-playbook condition_start_httpd.yml
      
      PLAY [restart httpd] *****************************************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [restart httpd] *****************************************************************
      changed: [webserver1]
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=2  changed=1  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
      
      $ 
      
    3. 沒有條件控制情境下,若失敗,後續動作會停止,也比較沒有效率:
      $ vim condition_start_services.yml
      ---
        - name: start service with condition
          hosts: webserver1
          vars:
            myservice:
              - httpd
              - named
              - squid
          tasks:
            - name: start services
              service:
                name: "{{ item }}"
                state: restarted
              loop: "{{ myservice }}"
      
      $ ansible-playbook condition_start_services.yml
      
      PLAY [start service with condition] **************************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [start services] ****************************************************************
      changed: [webserver1] => (item=httpd)
      failed: [webserver1] (item=named) => {"ansible_loop_var": "item", "changed": false, 
       "item": "named", "msg": "Could not find the requested service named: host"}
      failed: [webserver1] (item=squid) => {"ansible_loop_var": "item", "changed": false, 
       "item": "squid", "msg": "Could not find the requested service squid: host"}
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=1  changed=0  unreachable=0  failed=1  skipped=0  rescued=0  ignored=0
      
      如上所示,會有一個成功,兩個失敗!因為我們的 managed hosts 並沒有安裝 named 與 squid 這些服務。若能事先檢查有該指令否, 若有存在指令,才重新啟動的話~那可以透過底下的方式進行版本控制:
    4. 使用迴圈的回傳值進行下一個迴圈的條件判斷:在一邊情境下,只要前一個指令有嚴重的錯誤時,後面的任務就會停止運作。 如上面的案例來看,如果有下一個任務,則該任務就會停止。因此,若你需要依據前面的錯誤來進行後面的任務時, 就得要加上『 ignore_errors: true 』才行。如下所示:
      $ vim condition_start_services2.yml
      ---
        - name: start service with condition
          hosts: webserver1
          vars:
            myservice:
              - httpd
              - named
              - squid
          tasks:
            - name: check daemon program
              command: which "{{ item }}"
              ignore_errors: true
              register: myoutput
              loop: "{{ myservice }}"
      
            - name: start services
              service:
                name: "{{ item.item }}"
                state: restarted
              loop: "{{ myoutput['results'] }}"
              when: item.rc  == 0
      
      $ ansible-playbook condition_start_services2.yml
      
      PLAY [start service with condition] **************************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [check daemon program] **********************************************************
      changed: [webserver1] => (item=httpd)
      failed: [webserver1] (item=named) => {"ansible_loop_var": ... []}
      failed: [webserver1] (item=squid) => {"ansible_loop_var": ... []} 
      ...ignoring
      
      TASK [start services] ****************************************************************
      changed: [webserver1] => (item={'cmd': ['which', 'httpd'], 'stdout': ...}
      skipping: [webserver1] => (item={'msg': 'non-zero return code', 'cmd': .. }
      skipping: [webserver1] => (item={'msg': 'non-zero return code', 'cmd': .. }
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=3  changed=2  unreachable=0  failed=0  skipped=0  rescued=0  ignored=1
      
      如上所示,因為 command 這個模組的輸出資訊中,主要有 return_code (回傳值,在 ansible 當中為 rc 這個變數名稱), 因此,我們在 when 後面使用 item.rc 的資料,就可以依據指令成功 (回傳值為 0) 或失敗 (回傳值不為 0) 的狀況, 來設計是否要進行任務。而因為我們使用的 loop 資料是前一個指令的整體回傳訊息,因此,你可能需要先使用 debug 模組, 參考一下錯誤訊息後,找到相關的變數名稱來設計才好。
  3. 實做練習:處理群組、帳號等資訊,再以 id 指令檢測該帳號是否存在?
    1. 撰寫 loop_exam_user_create.yml 這個 playbook,主要針對 webserver1 這部主機
    2. 填寫變數,變數名稱 myusers 的內容有 exuser1, exuser2, exuser3, exuser4 等四個用戶
    3. 開始 tasks 的任務內容:
      • 第 1 個任務,先以 group 模組建立名為 exgroup 的群組名稱。
      • 第 2 個任務,針對 myusers 這個變數設計迴圈,然後以 user 模組建立起這四個用戶,且這四個用戶需要加入 exgroup 與 users 群組才行。
      • 第 3 個任務,使用 command 模組,透過 id 指令檢查四個用戶,同樣加上迴圈設計。另外,註冊一個名為 myoutput 的變數
      • 第 4 個任務,以 debug 模組,呼叫 myoutput 變數內容 (記得要用到 results 關鍵字),然後將 stdout 資料撈出來看看結果。
    4. 最後執行的結果會有點像這樣:
      
      PLAY [create and check users] ********************************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [1st task create group] *********************************************************
      changed: [webserver1]
      
      TASK [2nd task create users] *********************************************************
      changed: [webserver1] => (item=exuser1)
      changed: [webserver1] => (item=exuser2)
      changed: [webserver1] => (item=exuser3)
      changed: [webserver1] => (item=exuser4)
      
      TASK [3th task use id check users] ***************************************************
      changed: [webserver1] => (item=exuser1)
      changed: [webserver1] => (item=exuser2)
      changed: [webserver1] => (item=exuser3)
      changed: [webserver1] => (item=exuser4)
      
      TASK [4th task show output] **********************************************************
      ok: [webserver1] => (item={'cmd': ['id', ....} => {
          "msg": "show id output is => \"uid=3106(exuser1) gid=3107(exuser1) 
       groups=3107(exuser1),100(users),3106(exgroup)\""
      }
      ok: [webserver1] => (item={'cmd': ['id', ....} => {
          "msg": "show id output is => \"uid=3107(exuser2) gid=3108(exuser2) 
       groups=3108(exuser2),100(users),3106(exgroup)\""
      }
      ok: [webserver1] => (item={'cmd': ['id', ....} => {
          "msg": "show id output is => \"uid=3108(exuser3) gid=3109(exuser3) 
       groups=3109(exuser3),100(users),3106(exgroup)\""
      }
      ok: [webserver1] => (item={'cmd': ['id', ....} => {
          "msg": "show id output is => \"uid=3109(exuser4) gid=3110(exuser4) 
       groups=3110(exuser4),100(users),3106(exgroup)\""
      }
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=5  changed=3  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
      

使用 handler 進行例外呼叫 (類似函數 function)

  1. handler 指的是類似例外處理的程序,那為什麼需要 handler 這個功能?
    1. 基本上, ansible 主要是暮等的程序,許多的任務都會一項一項的去 managed hosts 執行。 每項任務可能會被執行,也可能不會有任何的更動。若同一個 playbook 執行多次後,某些流程就會顯示不會更動了!
    2. 有時,某些任務必需要在某個動作失敗後,才會主動被喚醒來操作的,例如,當發現到 httpd 服務沒有運作時,才給予重新啟動。 但是,一般來說,若前一個任務失敗,通常後續的任務就會直接被略過,除非跟前小節一樣,加上『 ignore_errors = true 』 這樣的設定,才會強迫任務繼續運作下去。
    3. 我想要讓某個任務物脫離正常的任務範圍之內,而是當某個任務失敗,才主動去呼叫該任務,這個任務就可以寫入處理程序 (handler) 裡面。 也就是說,當某個任務出現『狀態改變』時 (不是 OK 也不是 false 喔!是 changed 時),才去呼叫某個任務!這就是 handler 要做的事。
    4. 你可能會說,那不是透過 when 也能進行這件事情?基本上,handler 與 when 還是有點不一樣的。when 所在的任務, 還是在一般任務裡面,所以,when 總是會被執行,只是條件不同時就不予以實際執行。那麼 handler 則是在正常程序之外的額外的程序, 所以除非被呼叫,否則一般是不會被執行的。
  2. 一個簡單的 handler 範例:當設定檔改變過後,才去處理該服務的重新啟動。
    1. 撰寫名為 handler_restart_httpd.yml 檔案,主要針對 webserver1 主機進行任務,主要的任務有:
      • 安裝 httpd 服務
      • 將 check.conf 檔案傳輸到 /etc/httpd/conf.d/check.conf 檔案去
      • 若上述 check.conf 的內容有改變,就予以呼叫重新啟動 (restart) web 的服務。
      # 先來設計 check.conf 的檔案,這個檔案其實很簡單,只是有註解的項目而已
      $ vim check.conf
      # just a comment
      
      $ vim hander_restart_httpd.yml
      ---
        - name: restart httpd when configure changed
          hosts: webserver1
          tasks:
            # 安裝 httpd
            - name: install httpd
              yum:
                name: httpd
                state: present
            # 上傳設定檔
            - name: upload configure file
              copy:
                src: check.conf
                dest: /etc/httpd/conf.d/check.conf
              # 若有 changed 的狀態,就予以通知 (notify) handler 去執行 restart httpd 的任務
              notify:
                - restart httpd
      
          handlers:
            # 建立名為 restart httpd 的任務
            - name: restart httpd
              service:
                name: httpd
                state: restarted
      
    2. 上面這個腳本比較重要的地方,就在:
      • 在需要注意的項目,亦即是有沒有改變 check.conf 設定檔內容時,若狀態為 changed (該設定檔有被改變), 那就使用 notify 的語法,去呼叫名為 restart httpd 的任務。
      • 透過 handlers: 的宣告,與 tasks 在同一個層級,主要在撰寫『非例行的任務』!通常是例外處理!
      • notify 底下接的任務名稱,必須在 handlers: 裡面為相同的任務名稱才會被執行喔!
      根據上面的說明,我們來多次執行這個 playbook 看看:
      $ ansible-playbook --syntax-check hander_restart_httpd.yml
      
      # 第一次執行這個 playbook
      $ ansible-playbook hander_restart_httpd.yml
      
      PLAY [restart httpd when configure changed] ******************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [install httpd] *****************************************************************
      ok: [webserver1]
      
      TASK [upload configure file] *********************************************************
      changed: [webserver1]
      
      RUNNING HANDLER [restart httpd] ******************************************************
      changed: [webserver1]
      
      PLAY RECAP ***************************************************************************
      webserver1: ok=4  changed=2  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
      
      # 第二次執行這個 playbook
      $ ansible-playbook hander_restart_httpd.yml
      
      PLAY [restart httpd when configure changed] ******************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [install httpd] *****************************************************************
      ok: [webserver1]
      
      TASK [upload configure file] *********************************************************
      ok: [webserver1]
      
      PLAY RECAP ***************************************************************************
      webserver1: ok=3  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
      
      大家可以發現,因為第二個任務變成了 ok (我們沒有重新調整 check.conf),所以系統就沒有重新啟動 httpd 囉!甚至連『 HANDLER 』的字樣都沒有被顯示出來哩!
  3. 使用 handler 的好處 (跟 when 相比的話):
    1. handlers 任務的執行順序是固定的:
      handlers 區塊內定義的任務,也是依序被 handlers 所調用,因此,如果有 A, B, C 三個依序在 handler 內的任務, 這三個任務會依序被調用出來看看需要不需要被執行。這些任務不是隨機被其他任務的 notify 呼叫的, 也不會因為 notify 裡面出現的順序是 B, A, C 而改變其被調用的順序。
    2. handlers 任務總是最後執行的:
      當正常程序 (tasks) 的所有任務執行完畢之後,handlers 內的任務才會開始被依序執行。即使有 notify 到這些任務, 這些 handlers 的任務還是會接在最後一個 tasks 之後才執行。
    3. 相同任務名稱只有一個會執行:
      要執行 handlers 的任務,通常需要 notify 指定任務名稱。handlers 內的任務名稱如果因為某些緣故導致具有相同的任務名稱時, 只有其中一個會被執行。
    4. handlers 的任務也只會執行一次:
      當有多個正常程序的任務重複 notify 同一個 handler 的任務名稱時,該任務也只會執行一次!並不會被重複執行。這點與 shell script 的 function 差很多!完全不同。
    5. notify 只會通知在 changed 的狀態:
      handlers 內的任務,只有在正常任務是在改變 (changed) 的狀態下,才會通知到 handler 的相關任務。當正常程序狀態是 ok 或 false 時, 都不會呼叫到 handler 的任務喔!
    6. handler 任務正常狀態是不會被執行的:
      如果正常程序執行過程都是 ok ,並沒有 changed 時,那麼正常程序跑完後,整個 playbook 就停止了,handler 任務並不會被觸發! 再次強調,只有正常程序狀態是 changed 時,才會觸發相關的 handler 任務。
  4. 實做練習:同樣的,改變了 httpd 的設定檔時,才去重新啟動 mariadb 與 httpd 兩個服務
    1. 撰寫名為 hander_restart_httpd2.yml 的 playbook,主要由 hander_restart_httpd.yml 複製而來
    2. 修改 yum 模組的任務,改變成為 loop 的方式,安裝的軟體主要有 httpd 與 mariadb-server 兩個!注意,是使用 loop 喔!
    3. 改變 handlers 內的 restart httpd 任務,同樣加入 loop 的功能,只是需要啟動的服務分別有 mariadb 與 httpd 這兩個!
    4. 改變 check.conf 的內容,內文隨便再加一行註解文字即可。
    5. 最後執行 hander_restart_httpd2.yml 之後,會出現類似這樣的畫面:
      
      PLAY [restart httpd when configure changed] ******************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [install httpd] *****************************************************************
      ok: [webserver1] => (item=httpd)
      ok: [webserver1] => (item=mariadb-server)
      
      TASK [upload configure file] *********************************************************
      changed: [webserver1]
      
      RUNNING HANDLER [restart httpd] ******************************************************
      changed: [webserver1] => (item=mariadb)
      changed: [webserver1] => (item=httpd)
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=4  changed=2  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
      

任務失敗時的處置方案: force_handlers, ignore_errors, fail module, block+rescue+always..

設計腳本總有失敗的時候,因此,我們就得要預先『防呆』。透過上面的 handler 我們可以做一些簡易的防呆,不過,還是有很呆的情境啊! 那就是如果正常任務流程失敗,那就可能會導致 handler 無法執行的問題喔!如何解決?來看看先。

  1. 強制使用 handler 在正常任務失敗之後:
    1. 如上所述, handler 是在正常程序完成整體流程之後才會被啟動。但是如果正常程序裡面有失敗的任務呢? 如果又忘記加上 ignore_errors 的話,很可能就會讓 handler 的任務不被執行喔!如下的程序:
      $ cp hander_restart_httpd.yml hander_restart_httpd3.yml
      $ vim hander_restart_httpd3.yml
      ---
        - name: restart httpd when configure changed
          hosts: webserver1
          tasks:
            - name: install httpd
              yum:
                name: httpd
                state: present
            - name: upload configure file
              copy:
                src: check.conf
                dest: /etc/httpd/conf.d/check.conf
              notify:
                - restart httpd
              # 其實只有加上這一個任務,因為沒有任何一個軟體名稱是 hehehaha,所以打包票,
              # 這個任務一定會失敗!
            - name: failure install pkg
              yum:
                name: hehehaha
                state: present
      
          handlers:
            - name: restart httpd
              service:
                name: httpd
                state: restarted
      
      $ vim check.conf
      # just hahahah
      
      $ ansible-playbook hander_restart_httpd3.yml
      
      PLAY [restart httpd when configure changed] ******************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [install httpd] *****************************************************************
      ok: [webserver1]
      
      TASK [upload configure file] *********************************************************
      changed: [webserver1]
      
      TASK [failure install pkg] ***********************************************************
      fatal: [webserver1]: FAILED! => {"changed": false, ...}
      
      RUNNING HANDLER [restart httpd] ******************************************************
               <==沒有任何任務在這裡!
      PLAY RECAP ***************************************************************************
      webserver1 : ok=3  changed=1  unreachable=0  failed=1  skipped=0  rescued=0  ignored=0
      
      很簡單的就可以看到,在 HANDLER 的區塊內,沒有執行任何一個任務喔!
    2. 使用 force_handlers: true 強迫執行:
      現在,請在上面的 playbook 當中,在 hosts 底下增加一行『 force_handlers: true 』的項目,並且重新修改 check.conf 的內容, 然後重複執行一下:
      $ vim hander_restart_httpd3.yml
      ---
        - name: restart httpd when configure changed
          hosts: webserver1
          force_handlers: true
          tasks:
      ....
      
      $ vim check.conf
      # hehehe
      
      $ ansible-playbook hander_restart_httpd3.yml
      
      PLAY [restart httpd when configure changed] ******************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [install httpd] *****************************************************************
      ok: [webserver1]
      
      TASK [upload configure file] *********************************************************
      changed: [webserver1]
      
      TASK [failure install pkg] ***********************************************************
      fatal: [webserver1]: FAILED! => {"changed": false, ...}
      
      RUNNING HANDLER [restart httpd] ******************************************************
      changed: [webserver1]
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=4  changed=2  unreachable=0  failed=1  skipped=0  rescued=0  ignored=0
      
  2. 兩個可以強迫執行被 notify 的 handlers 任務的方法:
    1. force_handlers: true
      如上所示,如果有任何可能會失敗的任務,你還是希望 handler 一定可以被呼叫執行,那就使用 force_handlers 吧!
    2. ignore_errors: true
      如果你已經預期某個任務會失敗,在該任務底下加上 ignore_errors: true 也是可以讓任務繼續跑下去的!!
  3. 使用 failed_when 定義該模組的錯誤,或 fail 模組進行錯誤資料顯示:
    1. 使用者如果撰寫 shell script 具有良好的程序控制時,有時候反而會造成 ansible 不知如何是好喔! 舉例來說,我們檢查一下你的 webserver1 有沒有 joe 這個人?
      $ vim hander_check_fail1.yml
      ---
        - name: check user is OK or FAIL
          hosts: webserver1
          tasks:
            - name: use id to check joe man
              shell: "id joe && echo OK || echo FAIL"
              register: myoutput
      
            - name: show message
              debug:
                var: myoutput
      
      $ ansible-playbook hander_check_fail1.yml
      
      PLAY [check user is OK or FAIL] ******************************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [use id to check joe man] *******************************************************
      changed: [webserver1]
      
      TASK [show message] ******************************************************************
      ok: [webserver1] => {
          "myoutput": {
              "changed": true,
              "cmd": "id joe && echo OK || echo FAIL",
              "delta": "0:00:00.007586",
              "end": "2020-11-12 04:53:10.679374",
              "failed": false,
              "rc": 0,
              "start": "2020-11-12 04:53:10.671788",
              "stderr": "id: ‘joe’: no such user",
              "stderr_lines": [
                  "id: ‘joe’: no such user"
              ],
              "stdout": "FAIL",
              "stdout_lines": [
                  "FAIL"
              ]
          }
      }
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=3  changed=1  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
      
      明明就是沒有這個人!怎麼會顯示沒錯誤呢?這是因為 shell 裡面我們用了 echo 的指令,而 shell 只會記憶一個回傳值, 那就是最後一個指令的回傳值!因此就不會顯示錯誤了。
    2. 如果你要讓該程序變成錯誤的話,可以使用 failed_when 來處理!
      $ vim hander_check_fail1.yml
      ---
        - name: check user is OK or FAIL
          hosts: webserver1
          tasks:
            - name: use id to check joe man
              shell: "id joe && echo OK || echo FAIL"
              register: myoutput
              failed_when: "'FAIL' in myoutput.stdout"
      
            - name: show message
              debug:
                var: myoutput
      
      $ ansible-playbook hander_check_fail1.yml
      
      PLAY [check user is OK or FAIL] ******************************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [use id to check joe man] *******************************************************
      fatal: [webserver1]: FAILED! => {"changed": true, "cmd": 
       "id joe && echo OK || echo FAIL", "delta": "0:00:00.007584", "end": 
       "2020-11-12 04:58:08.917877", "failed_when_result": true, "rc": 0, "start": 
       "2020-11-12 04:58:08.910293", "stderr": "id: ‘joe’: no such user", "stderr_lines": 
       ["id: ‘joe’: no such user"], "stdout": "FAIL", "stdout_lines": ["FAIL"]}
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=1  changed=0  unreachable=0  failed=1  skipped=0  rescued=0  ignored=0
      
      這樣就會變成是一個錯誤的任務了!
    3. 使用回報為錯誤的 fail 模組來取代 failed_when 的應用:
      $ cp hander_check_fail1.yml hander_check_fail2.yml
      $ vim hander_check_fail2.yml
      ---
        - name: check user is OK or FAIL
          hosts: webserver1
          tasks:
            - name: use id to check joe man
              shell: "id joe && echo OK || echo FAIL"
              register: myoutput
              ignore_errors: true
      
            - name: define to be fail task
              fail:
                msg: The task is failed
              when: "'FAIL' in myoutput.stdout"
      
      $ ansible-playbook hander_check_fail2.yml
      
      PLAY [check user is OK or FAIL] ******************************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [use id to check joe man] *******************************************************
      changed: [webserver1]
      
      TASK [define to be fail task] ********************************************************
      fatal: [webserver1]: FAILED! => {"changed": false, "msg": "The task is failed"}
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=2  changed=1  unreachable=0  failed=1  skipped=0  rescued=0  ignored=0
      
      用 fail 模組取代 fail_when 的好處是,顯示的訊息比較乾淨啦!
  4. 使用 changed_when 搭配 handlers 處理例外狀況
    1. 使用 failed_when 搭配 fail 模組,主要是設計在偵測錯誤的狀況。但是,如何克服呢?基本上,還是可以透過 handlers 的! 只是,可能不能使用 failed_when,這是因為 failed_when 是回報為錯誤!但是 handlers 則是需要『 changed 』狀態啊! 所以,我們可以改用 changed_when 來搭配喔!
    2. 簡單範例,同樣使用上面的範本,當發現到沒有該帳號時,就主動建立該帳號的情境:
      $ cp hander_check_fail1.yml hander_check_change.yml
      $ vim hander_check_change.yml
      ---
        - name: check user is OK or FAIL
          hosts: webserver1
          tasks:
            - name: use id to check joe man
              shell: "id joe && echo OK || echo FAIL"
              register: myoutput
              changed_when: "'FAIL' in myoutput.stdout"
              notify:
                - create joe
      
            - name: show message
              debug:
                var: myoutput.stdout
      
          handlers:
            - name: create joe
              user:
                user: joe
                state: present
      
      $ ansible-playbook hander_check_change.yml
      
      PLAY [check user is OK or FAIL] ******************************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [use id to check joe man] *******************************************************
      changed: [webserver1]
      
      TASK [show message] ******************************************************************
      ok: [webserver1] => {
          "myoutput.stdout": "FAIL"
      }
      
      RUNNING HANDLER [create joe] *********************************************************
      changed: [webserver1]
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=4  changed=2  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
      
      你可以連續執行這個 playbook 幾次看看,你就會發現,除了第一次找不到 joe 所以會主動呼叫 handlers 一次之外, 其他時間就不會展示 HANDLER 的任務喔!
  5. 使用 block, rescue, always 定義任務群組
    1. 我們可以使用 block 來定義整個任務裡面的子任務!所謂的 block 是可以將某些子任務視為一個集合, 並且將該集合處理完畢這樣。只是,比較可惜的是,目前這個 block 只支援每個單一的任務 (子任務內有迴圈也還正常), 並不支援整體 block 子任務集合的迴圈!先來看看如何定義 block 呢?例如,分別檢查 httpd 是否啟動? 以及是否擁有 /etc/php.ini 這個檔案?
      $ vim block_restart_httpd.yml
      ---
        - name: block tasks to check httpd daemon
          hosts: webserver1
          tasks:
            - name: check httpd daemon state
              block:
                - name: check httpd is active
                  command: systemctl is-active httpd
                  ignore_errors: true
                - name: check php.ini exist
                  command: ls /etc/php.ini
                  ignore_errors: true
      
      $ ansible webserver1 -m command -a 'systemctl stop httpd'
      
      $ ansible-playbook block_restart_httpd.yml
      
      PLAY [block tasks to check httpd daemon] *********************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [check httpd is active] *********************************************************
      fatal: [webserver1]: FAILED! => {"changed": true, "cmd": ...}
      ...ignoring
      
      TASK [check php.ini exist] ***********************************************************
      changed: [webserver1]
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=3  changed=2  unreachable=0  failed=0  skipped=0  rescued=0  ignored=1
      
      這樣就把兩個任務綁在一起了!
    2. block 的子任務集合錯誤克服也是能搭配 handlers 來處理,也能搭配 when 來處置,不過,更好的作法,其實是搭配 rescue (救援) 來處理!也就字面上的意思, block 是正常程序,而 rescue 則是當 block 出現 fail 的時候才會進行的另一個子任務的集合!
      $ cp block_restart_httpd.yml block_restart_httpd2.yml
      $ vim block_restart_httpd2.yml
      ---
        - name: block tasks to check httpd daemon
          hosts: webserver1
          tasks:
            - name: check httpd daemon state
              block:
                - name: check httpd is active
                  command: systemctl is-active httpd
                - name: check php.ini exist
                  command: ls /etc/php.ini
              rescue:
                - name: restart httpd
                  command: systemctl restart httpd
      
      $ ansible-playbook block_restart_httpd2.yml
      
      PLAY [block tasks to check httpd daemon] *********************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [check httpd is active] *********************************************************
      fatal: [webserver1]: FAILED! => {"changed": true, "cmd":...}
      
      TASK [restart httpd] *****************************************************************
      changed: [webserver1]
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=2  changed=1  unreachable=0  failed=0  skipped=0  rescued=1  ignored=0
      
      你必需要注意的是, rescue 只有在發生錯誤 (而且不能 ignore_errors) 才會進行,因此,我們將原本 block 內的 ignore_errors 取消, 然後加上 rescue 的區塊,讓它去重新啟動 httpd !那就 OK 啦!如果你持續進行兩次以上這隻 playbook, 就可以發現 block 正常進行完畢,所以 rescue 就不會被觸發了!
    3. 以 always 設計總是要進行的任務:
      $ cp block_restart_httpd2.yml block_restart_httpd3.yml
      $ vim block_restart_httpd3.yml
      ---
        - name: block tasks to check httpd daemon
          hosts: webserver1
          tasks:
            - name: check httpd daemon state
              block:
                - name: check httpd is active
                  command: systemctl is-active httpd
                - name: check php.ini exist
                  command: ls /etc/php.ini
              rescue:
                - name: restart httpd
                  command: systemctl restart httpd
              always:
                - name: show httpd status
                  command: systemctl status httpd
                  register: myoutput
                - name: show output
                  debug:
                    var: myoutput.stdout
      
      $ ansible-playbook block_restart_httpd3.yml
              
      PLAY [block tasks to check httpd daemon] *********************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [check httpd is active] *********************************************************
      changed: [webserver1]
      
      TASK [check php.ini exist] ***********************************************************
      changed: [webserver1]
      
      TASK [show httpd status] *************************************************************
      changed: [webserver1]
      
      TASK [show output] *******************************************************************
      ok: [webserver1] => {
          "myoutput.stdout": "● httpd.service - The Apache HTTP Server\n ....
      }
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=5  changed=3  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
      
      透過 block, rescue, always 也能設計簡單的流程控制任務的執行!
  6. 實做練習:確定系統正確後,安裝相對的服務,上傳新的設定檔,重新啟動服務等任務
    1. 撰寫名為 block_handler.yum 的 playbook 檔案,主要針對 webserver1 進行任務
    2. 正常的任務列表如下:
      • 第一個任務,名稱使用『 check HW and OS system 』,使用 fail 群組,檢查當 (when) (1)記憶體 ( ansible_facts['memtotal_mb'] ) 小於 1024 時,或 (2)OS 版本不是 CentOS 時,就回報失敗。要注意,因為失敗,所以後續的所有任務都不會被執行喔!
      • 第二個任務,名稱使用『 Install web pksg 』,使用 yum 模組,開始安裝 httpd, mod_ssl, php 三個軟體, 無須使用 loop,請直接使用 yum 模組的功能即可。
      • 第三個任務,名稱使用『 start and enable httpd 』,透過 service 模組,將 httpd 設定為立刻啟動、開機啟動。
      • 第四個任務,名稱使用『 upload config file 』,內容透過 block 與 rescue 進行資料的處理:
        • 在 block 內僅有一個任務,複製本地端的 check.conf 到遠端的 /etc/httpd/conf.d/check.conf 去, 若有任何改變 (changed),則通知名為『 restart httpd 』的 handlers 任務。
        • 在 rescue 內僅有一個任務,若上述動作失敗,則使用 debug 模組,直接顯示『 upload file failure 』即可。
      • 第五個任務,名稱使用『 modify firewall 』,使用 firewalld 放行 http 與 https 兩個防火牆規則, 這個地方應該要使用 loop 方式來處理才行。
    3. handlers 任務,只有一個:
      • 名稱設定一定是『 restart httpd 』主要是提供第四個任務的 block 內部改變而做的。透過 service 模組,讓 httpd 狀態是 restarted 即可。
    4. 改變 check.conf 檔案,增加一個註解即可
    5. 最後,直接執行會有點像這樣:
      $ ansible-playbook  block_handler.yml
      
      PLAY [create web service] ************************************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [check HW and OS system] ********************************************************
      skipping: [webserver1]
      
      TASK [Install web pksg] **************************************************************
      ok: [webserver1]
      
      TASK [start and enable httpd] ********************************************************
      ok: [webserver1]
      
      TASK [upload check.conf] *************************************************************
      changed: [webserver1]
      
      TASK [modify firewall] ***************************************************************
      ok: [webserver1] => (item=http)
      ok: [webserver1] => (item=https)
      
      RUNNING HANDLER [restart httpd] ******************************************************
      changed: [webserver1]
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=6  changed=2  unreachable=0  failed=0  skipped=1  rescued=0  ignored=0
      

檔案傳輸複製行為與 Jinja2 樣板

有時候我們可能會複製一些檔案或者是更改一些檔案到 server 端的電腦上面,這時,透過 control node 去對 managed host 進行檔案內容的修改,可能就是一項很重要的業務!這個動作可能是修改檔案、完整的覆蓋檔案、分析 managed host 的資料後套用樣板資料來複寫檔案, 都是可以進行的喔!

  1. 與檔案傳輸、複製等有關行為的 ansible 模組,底下緊列出模組的主要用途,其他詳細用法,請務必參考 ansible-doc 的查詢!
    1. blockinfile:傳輸一整個區塊的文字數行
    2. copy:複製檔案,或者是將 ansible 內的資料傳輸到 managed host 內的檔案,亦能修改檔案權限
    3. fetch:與 copy 相似,但是方向相反,是由 managed host 複製資料過來 control node,並且可以 managed host 的目錄樹型態存檔
    4. file:設定權限屬性、建立實體連結、符號連結、建立或刪除檔案等等。
    5. lineinfile:確認某行資料是否在檔案內、或者是『修改』某檔案的內容(大概就那一行關鍵行)
    6. stat:嘗試取得檔案資料
    7. synchronize:透過 rsync 進行資料同步
  2. 利用 stat 模組:先嘗試取得 webserver1 的 /etc/issue 這個檔案的 (1)sha512 的指紋資料、(2)擁有者名稱、(3)群組名
    # 確認在 student 家目錄的正確 ansible 操作目錄中
    $ whoami; pwd
    student
    /home/student/ansible-init
    
    # 新增一個可以增加斷行功能的模組設定
    $ vim ansible.cfg
    [defaults]
    ....
    stdout_callback = debug
    
    # 開始設計 stat 模組的功能
    $ vim file_stat.yml
    ---
      - name: check file stat use stat module
        hosts: webserver1
        tasks:
          - name: check state for /etc/issue
            stat:
              path: /etc/issue
              checksum_algorithm: sha512
            register: results
          - name: show output
            debug:
              msg: |
                the sha512 is, {{ results.stat.checksum }}
                the owner is {{ results.stat.pw_name }}
                the group is {{ results.stat.gr_name }}
    
    $ ansible-playbook --syntax-check file_stat.yml
    playbook: file_stat.yml
    
    $ ansible-playbook  file_stat.yml
    PLAY [check file stat use stat module] ************************************************
    
    TASK [Gathering Facts] ****************************************************************
    ok: [webserver1]
    
    TASK [check state for /etc/issue] *****************************************************
    ok: [webserver1]
    
    TASK [show output] ********************************************************************
    ok: [webserver1] => {}
    
    MSG:
    
    the sha512 is, 88c9342e4c2a23e61af218b9b66808925de9d2b0bd6eeb341...c1d41d497a5b0668b472
    the owner is root
    the group is root
    
    
    PLAY RECAP ****************************************************************************
    webserver1  : ok=3  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
    
  3. 利用 file 模組,建立一個給 Samba 用的目錄,相關的設定會有點像這樣喔:
    • 目錄名稱: /srv/samba_dir/
    • 擁有者與群組: root:student
    • 權限: 2775
    • SELinux type: samba_share_t
    之後以 command 模組,將該目錄檔案的實際權限顯示出來 (ls -ld 檔名)
    $ vim file_file.yml
    ---
      - name: setup a SAMBA share directory
        hosts: webserver1
        tasks:
          - name: create directory and make permissions
            file:
              path: /srv/samba_dir
              state: directory
              owner: root
              group: student
              mode: '2775'
              setype: samba_share_t
          - name: show file output
            command:
              cmd: ls -ld /srv/samba_dir
            register: output
          - debug:
              msg: "{{ output.stdout }}"
    
    $ ansible-playbook --syntax-check file_file.yml
    playbook: file_file.yml
    
    $ ansible-playbook  file_file.yml
    PLAY [setup a SAMBA share directory] **************************************************
    
    TASK [Gathering Facts] ****************************************************************
    ok: [webserver1]
    
    TASK [create directory and make permissions] ******************************************
    changed: [webserver1]
    
    TASK [show file output] ***************************************************************
    changed: [webserver1]
    
    TASK [debug] **************************************************************************
    ok: [webserver1] => {}
    
    MSG:
    
    drwxrwsr-x. 2 root student 6 Nov 27 17:38 /srv/samba_dir
    
    PLAY RECAP ****************************************************************************
    webserver1 : ok=4  changed=2  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
    
  4. 透過 fetch 模組,將 managed host 的 /var/log/secure 抓下來 (目前不支援目錄下載)
    $ vim file_fetch.yml
    ---
      - name: copy files to control node
        hosts: webserver1
        tasks:
          - name: fetch file to here
            fetch:
              dest: file_backup
              src: /var/log/secure
    
    $ ansible-playbook file_fetch.yml
    PLAY [copy files to control node] *****************************************************
    
    TASK [Gathering Facts] ****************************************************************
    ok: [webserver1]
    
    TASK [fetch file to here] *************************************************************
    changed: [webserver1]
    
    PLAY RECAP ****************************************************************************
    webserver1 : ok=2  changed=1  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
    
    $ tree file_backup/
    file_backup/
    └── webserver1
        └── var
            └── log
                └── secure
    
    3 directories, 1 file
    
    預設的情境下,抓下來的檔名會以個別的主機名稱以及個別的檔案,放置在頂層目錄上。如果要抓下來成為單一檔名, 就得要加上『 flat: true 』在 fetch 模組中了!
  5. 透過 blockinfile 處理大量資料,假設我們要在 /etc/hosts 裡面新增資料,類似:
    172.16.100.1  pc1
    172.16.100.2  pc2
    172.16.100.3  pc3
    
    可以透過底下的方式來產生:
    $ vim file_blockinfile.yml
    ---
      - name: add a block area into file
        hosts: webserver1
        tasks:
          - name: use blockinfile module
            blockinfile:
              path: /etc/hosts
              block: |
                172.16.100.1  pc1
                172.16.100.2  pc2
                172.16.100.3  pc3
          - name: get file content
            command:
              cmd: cat /etc/hosts
            register: output
    
          - name: show output
            debug:
              msg: "{{ output.stdout }}"
    
    $ ansible-playbook file_blockinfile.yml
    PLAY [add a block area into file] ****************************************************
    
    TASK [Gathering Facts] ***************************************************************
    ok: [webserver1]
    
    TASK [use blockinfile module] ********************************************************
    changed: [webserver1]
    
    TASK [get file content] **************************************************************
    changed: [webserver1]
    
    TASK [show output] *******************************************************************
    ok: [webserver1] => {}
    
    MSG:
    
    127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
    ::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
    
    192.168.40.254  gateway
    192.168.40.200  gitserver200.ksu gitserver
    172.17.200.254  control ansible
    172.17.200.1    gitclient200.ksu gitclient client1 webserver1 dbserver1 devnode1 ..
    # BEGIN ANSIBLE MANAGED BLOCK
    172.16.100.1  pc1
    172.16.100.2  pc2
    172.16.100.3  pc3
    # END ANSIBLE MANAGED BLOCK
    
    PLAY RECAP ***************************************************************************
    webserver1 : ok=4  changed=2  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
    
    使用 blockinfile 模組,基本上,預設會在增加的資料上/下方,各加一行註解,就如同上面的輸出結果喔!可以查詢看看。
  6. 透過 Jinja2 這個樣板 (template) 來產生 managed host 的相關檔案內容
    1. 依據樣板資料產生檔案內容: Jinja2 樣板基本介紹:
      • 如上所示,雖然可以透過 copy, file, blockinfile, lineinfile 等模組來修改檔案,但是如果需要比較大量的內容資料時, 使用這些模組就顯的有點麻煩。因此,就有樣板模組的功能產生,其中, ansible 用的是名為 Jinja2 的樣板。
      • Jinja2 樣板可以使用許多符號方式 (delimiters) 來進行類似迴圈等行為,可以達成許多有效的內容產生。 常見的符號方式,類似透過『 {{ EXPR }} 』顯示變數資料,『 {{% EXPR %}} 』顯示邏輯運算,像剛剛提到的迴圈, 就可以透過這種百分比的符號來處理。至於,如果只是要讓編輯者知道該資料為何,就是所謂的註解,並不會產生檔案內容時, 可以使用『 {# 這是註解 #} 』來處理喔!
      • 例如 /etc/hosts 內,針對本機的 IP 與主機名稱對應的撰寫,可以是這樣的方式:
        {# this is /etc/hosts #}
        {{ ansible_facts['default_ipv4']['address']] }}  {{ ansible_facts['hostname'] }}
        
        直接透過 ansible 去分析的 managed hosts 資料的變數內容,直接拿來變更系統的設定項目就是了。
      • Jinja2 副檔名建議:一般來說, jinja2 樣板的資料,只要內容正確即可,檔名倒不是很重要的地方。 不過,如果有正確的副檔名,這樣在某些編輯軟體裡面,Jinja2 的樣板,應該會更好建置才對。 所以,一般建議副檔名取名為 .j2 這樣的情境較佳。
    2. Jinja2 支援的變數與參數資料 - 變數與迴圈
      • Jinja2 支援變數格式的使用,例如『 {{ varibale }} 』或者透過 『 {{ ansible_facts[xx] }} 』都可以。
      • Jinja2 支援迴圈功能,使用的迴圈格式如下:
        {% for user in users %}
        	{{ user }}
        {% endfor %}
        
        只要讓 users 成為陣列格式,就可以讓 user 一個一個列出來了。此外, Jinja2 也支援 if 的語法:
        {% for user in users if not user == "root" %}
        user number {{ loop.index }} - {{ user }}
        {% endfor %}
        
        當然也能直接使用陣列值
        {% for host in groups['hosts'] %}
        	{{ host }}
        {% endfor %}
        
      • Jinja2 支援的狀態參數 (contitions),例如:
        {% if finished %}
        {{ results }}
        {% endif %}
        
      • 輸出格式的處理,可以轉成 json 或 yaml 喔!當然,也能從 json 或 yaml 轉成一般文字:
        {{ output | to_json }}
        {{ output | to_yaml }}
        {{ output | from_json }}
        {{ output | from_yaml }}
        
    3. 使用 template 模組來支援 Jinja 格式檔案的方式,例如:
      - name: template sample
        template:
          src: files/filename.j2
          dest: /etc/to/file
      
    4. 我們登入系統之後,系統會顯示歡迎訊息的資訊,這些資訊其實是寫入到 /etc/motd 這個檔案內。 假設你想要客製化 /etc/motd 時,並且加入某些特定的資料,例如主機名稱、系統版本、系統管理員等等的資料, 可以先建立一個樣板檔案,然後再處理相關的資訊即可:
      # 先建立樣板檔案
      $ mkdir files
      $ vim files/motd.j2
      The hostname is {{ ansible_facts['fqdn'] }}
      My Linux distribution is {{ ansible_facts['distribution'] }}
      	and version is {{ ansible_facts['distribution_version'] }}
      if you have any question contact with {{ system_owner }}
      
      # 開始撰寫 playbook 的內容
      $ vim file_jinja.yml
      ---
        - name: setup a file content
          hosts: webserver1
          vars:
            system_owner: student@gitserver200.dic.ksu
          tasks:
            - name: setup /etc/motd file content
              template:
                src: files/motd.j2
                dest: /etc/motd
                owner: root
                group: root
                mode: 0644
            - name: get file content
              command:
                cmd: cat /etc/motd
              register: output
            - name: show output
              debug:
                msg: "{{output.stdout}}"
      
      $ ansible-playbook file_jinja.yml
      PLAY [setup a file content] **********************************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [setup /etc/motd file content] **************************************************
      changed: [webserver1]
      
      TASK [get file content] **************************************************************
      changed: [webserver1]
      
      TASK [show output] *******************************************************************
      ok: [webserver1] => {}
      
      MSG:
      
      The hostname is gitclient200.ksu
      My Linux distribution is CentOS
              and version is 8.1
      if you have any question contact with student@gitserver200.dic.ksu
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=4  changed=2  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
      
  7. 實做練習:先導知識,當使用者在本機端的文字終端機 (tty2, tty3..) 嘗試燈入時,會看到類似如下的畫面:
    CentOS Linux 8 (Core)
    Kernel 4.18.0-.... on an x86_64
    
    這個資料其實是 /etc/issue 提供的內容。而如果你想從網路登入時,那麼系統會提供 /etc/issue.net 的內容來顯示。 至於登入之後,就會以 /etc/motd 內容來顯示。現在,假設你登入之後,系統還會額外提供一些資訊的話,該如何處理? 例如,當你登入 managed host 之後,系統會提示如下的畫面:
    ############################################
    #
    # This server is managed by ansible 
    # please contect student@gitserver200.dic.ksu
    # System total memory: xxx MiB.
    # System prcessor count: N.
    #
    ############################################
    
    1. 先依據上面的指示,建立好 motd.j2 的樣板範本,特別注意,上表中那個特殊顏色的字體,務必使用變數或者是 ansible_facts 來設計,這樣才能在不同的系統處理好。
    2. 編寫 playbook ,內容包括需要的:
      • play 的名稱定義為: setup login show up
      • 針對 webserver1 主機設計
      • 建立 system_owner 變數
      • 建立第一個工作,使用 template,來源檔案 files/motd2.j2,目標檔案 /etc/motd,擁有者與群組都是 root, 權限 -rw-r--r--
    3. 檢查過語法之後,直接執行這個 play,大概會出現類似底下的畫面:
      
      PLAY [setup login show up] ***********************************************************
      
      TASK [Gathering Facts] ***************************************************************
      ok: [webserver1]
      
      TASK [setup /etc/motd file content] **************************************************
      changed: [webserver1]
      
      PLAY RECAP ***************************************************************************
      webserver1 : ok=2  changed=1  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
      
    4. 執行 ssh student@webserver1 ,並順利登入後,應該會有如底下的畫面才對:
      $ ssh student@webserver1
      student@webserver1's password:
      ############################################
      #
      # This server is managed by ansible
      # please contect student@gitserver200.dic.ksu
      # System total memory: 1829 MiB.
      # System prcessor count: 2.
      #
      ############################################
      Activate the web console with: systemctl enable --now cockpit.socket
      
      Last login: Sat Nov 28 05:34:28 2020 from 172.17.200.254