Linux伺服器 Linux伺服器

資工所專業課程上課教材

資工所專業課程 > 課程內容 > 專題 - 使用 Ansible 進行快速佈署 - Playbook 初探

專題 - 使用 Ansible 進行快速佈署 - Playbook 初探

上次更新日期 2020/11/07

Ansible 可以達成快速佈署,而且也能減輕 IT 人員的負擔。但是,如果依據前一章的設定,單純透過 ad hoc 單一指令來運作的話,那麼不就還得要重新撰寫 document?因為這些指令的內容還是得要記憶,對吧?這樣不好。 所以,就像 shell scripts 一樣,將指令寫成批次檔的概念,我們將 ad hoc 的內容寫成名為 playbook 的樣式, 讓 ansible 根據 playbook 去執行即可喔!未來要執行資料時,將 playbook 改一改,執行下去,全部受管控的 managed hosts 就全部更新了!方便快速得很!

Playbook 與 YAML 語法

  1. 什麼是 playbook 呢?
    1. 就像 shell script 簡單的說,可以將一系列的指令彙整成為類似批次檔一樣。 playbook 可以將前一章提到的 ansible ad hoc 指令, 彙整到一個文字檔,然後透過『 ansible-playbook filename.yml 』的方式,來將該動作執行完畢。而 playbook 又是純文字檔, 相當方便修改、彙整、紀錄等。
    2. 前一章有個 ansible ad hoc 內容是這樣:
      $ ansible webserver1 -m copy -a 'src=/etc/hosts dest=/etc/hosts'
      
      你可以將這一行改寫成為底下的模樣:
      ---
        - name: copy /etc/hosts to managed hosts
          hosts: webserver1
          tasks:
          - name: copy /etc/hosts to another hosts
            copy:
              src: /etc/hosts
              dest: /etc/hosts
      
      上面這個就是『 YAML 』格式的 playbook 內容!
  2. YAML 格式:
    1. 基本上, YAML 格式的要求是:『同一個階層的設定,需要使用相同的縮排』,至於縮排幾個空白字元,就沒有特別規定。 所以,簡單的說, YAML 對於縮排的需求有兩個基本的規則,一定要符合才行:
      • 每一個不同元素,只要其階層相同,那縮排必須要相同。如同上面的範例中,name, hosts, tasks 三個元素的階層相同, 所以縮排就一定要相同。
      • 子元件必須要有更多的縮排,其縮排空白必須要比父元件多。以上面的 copy 為例,他是 tasks 的子元件, 所以就一定要在 tasks 有更多的縮排才行!
    2. 為符合 yaml 的格式設計,如果使用 vim 這個程式編輯器,那你可以在 ~/.vimrc 裡面增加這一行,讓 vim 偵測到副檔名為 .yml 時, 就自動以 2 個字元替代 tab 按鍵的產生:
      $ whoami
      
      $ vim ~/.vimrc
      autocmd FileType yaml setlocal ai ts=2 sw=2 et
      
      如此一來,未來我們就可以直接使用 [tab] 來達成對齊之外,編輯的時候, .yml 格式也會自動縮排對齊!
  3. playbook 格式:
    1. 除了 Yaml 的格式之外,playbook 也有其特定的格式喔!剛剛看到的 playbook 就是一個基礎格式,我們可以分析一下上面的檔案, 你會發現到幾個基本的規則:
      • 整個 playbook 的運作開頭是由三個減號 (---) 開始的。
      • 之後第一個 play 接一個減號與一個空白 (- ),注意到是減號與空白喔!
      • 通常每個 play 會有三個指標 (keys),分別是 name, hosts, tasks。
      • 任務 (tasks) 底下可能會有好幾個連續的動作,每個動作同樣都以減號空白 (- ) 為開始, 然後接著模組名稱,在模組名稱底下則帶入該模組所需要的參數。
    2. name 的功能: (其實,基本功能就是標題, label 的功能!)
      • 通常每個 playbook 可能會有好幾個 play,每個 play 裡面可能又有好幾個任務 (tasks),每個任務裡面都有獨自的模組與參數這樣。
      • 那如果以 ansible ad hoc 的角度來看,我們就是反覆不斷的執行指令而已。
      • playbook 為了讓大家知道該指令的功能是什麼,或者是該任務 (tasks) 的任務是什麼,因此給了 name 的參數, 後面接的訊息,就是執行 playbook 時,會提供給 IT 人員查看該動作意義的訊息。
      • 在 playbook 中,name 雖然是非必要的,但是 ansible 建議一定要寫!這才未來執行,才知道那是啥東西!
    3. hosts 的功能:很簡單啊!就是 hosts/group 主機名稱列表的主機名稱群!
    4. tasks 的功能:就是許多任務
      • 每一個任務依舊使用 (- ) 開頭,接的第一個指標通常也就是 name,紀錄該任務的功能說明
      • 接下來就是模組名稱,然後在模組名稱底下,就是該模組的參數, 也就是透過 ansible-doc 查到的參數與參數值,中間都用冒號 (:) 隔開而已。
  4. 撰寫與執行第一支 playbook:
    1. 撰寫 playbook:
      • 注意,要將 playbook 放置到個別的工作目錄裡面比較好喔!
      • 注意,副檔名是 .yml 喔!
      • 開始撰寫 copy_hosts.yml 這個 playbook 內容看看:
        $ whoami; pwd
        student
        /home/student/ansible-init
        
        $ vim copy_hosts.yml
        ---
          - name: copy /etc/hosts to managed hosts
            hosts: webserver1
            tasks:
            - name: copy /etc/hosts to another hosts
              copy:
                src: /etc/hosts
                dest: /etc/hosts
        
    2. 檢查 playbook 是否有語法方面的問題:
      $ ansible-playbook --syntax-check copy_hosts.yml
      ERROR! We were unable to read either as JSON nor YAML, these are the errors we got from 
      each: JSON: Expecting value: line 1 column 1 (char 0)
      
      Syntax Error while loading YAML.
        mapping values are not allowed in this context
      
      The error appears to be in '/home/student/ansible-init/copy_hosts.yml': line 3, column 
      11, but maybe elsewhere in the file depending on the exact syntax problem.
      
      The offending line appears to be:
      
        - name: copy /etc/hosts to managed hosts
           hosts: webserver1
                ^ here
      
      如上所示,最後 3 行顯示錯誤的地方,原來是 hosts 的縮排多了一個空白字元的結果。改完之後重複執行一次, 系統就會提示沒問題了!
    3. 測試一下僅執行環境檢查的 dry run 結果,此動作僅檢查環境,不會實際運作喔!
      $ ansible-playbook -C copy_hosts.yml
      PLAY [copy /etc/hosts to managed hosts] *********************************************
      
      TASK [Gathering Facts] **************************************************************
      ok: [webserver1]
      
      TASK [copy /etc/hosts to another hosts] *********************************************
      ok: [webserver1]
      
      PLAY RECAP **************************************************************************
      webserver1 : ok=2  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
      
      你會發現到中括號 [] 裡面,大部分就是 name 設計的名稱!這樣真的比較好理解工作項目! 所以, name 要寫清楚一點喔!最後一行則是顯示執行的結果,大部分都是 ok 的!
    4. 實際執行此 playbook:
      $ ansible-playbook  copy_hosts.yml
      
      結果會跟上面一樣,只是,這次就真的是執行而不是僅檢查環境而已!
  5. 實做練習:
    1. 模仿上面的 playbook 設定,然後翻到前一章,找到新增用戶的指令如下:
      python3 -c "from passlib.hash import sha512_crypt; \
        import getpass; \
        print(sha512_crypt.using(rounds=5000).hash(getpass.getpass()))"
      Password:
      $6$I9E8lDkx9ThYK5y0$c7icc4zDLaLyBE3zacseBQeKBmkhUpNTUC.0RrK/6Od...
      
      ansible webserver1 -m user -a 'user=demouser uid=3000 password=$6$I9E8lDkx9T..'
      
    2. 現在,新增一個新的用戶,名稱為 myuser1,uid 為 3101,密碼為加密過後的 itismyuser。 請將該設定值寫成名為 user_add.yml 的 playbook
    3. 嘗試檢查語法,若沒問題,就直接執行這個 playbook 看看
    4. 使用 ansible ad hoc 的指令功能,使用 id 檢查一下這個帳號 myuser1 是否已經存在其系統中了?

一個 playbook 使用範例:啟動 httpd 服務

  1. 範例規劃:安裝 http 以及處理首頁檔案
    1. 管理員大概很常被要求要幫用戶處理 web server 的啟動與首頁的製作。大致上安裝、啟動、開機啟動、設置首頁檔案、防火牆的設定流程是不變的。 我們可以透過 playbook 來設定好這些資料!
    2. 安裝的流程應該是使用 yum 模組的,安裝的軟體為 httpd,使用底下的方式找到 yum 的參數:
      $ ansible-doc yum
      .....
      - name
              A package name or package specifier with version, like `name-1.0'.
      .....
      - state
              Whether to install (`present' or `installed', `latest'), or
              remove (`absent' or `removed') a package.
              `present' and `installed' will simply ensure that a desired
              package is installed.
              `latest' will update the specified package if it's not of the
              latest available version.
              `absent' and `removed' will remove the specified package.
              Default is `None', however in effect the default action is
              `present' unless the `autoremove' option is enabled for this
              module, then `absent' is inferred.
      .....
      
      通常會使用 latest 強制安裝到最新,至於 installd 與 present 則是要求要有安裝。 如果要移除,才使用 removed 。
    3. 啟動與開機啟動需要透過 service 模組的支援,所以檢查 service 的參數:
      $ ansible-doc service
      .....
      - enabled
              Whether the service should start on boot.
      .....
      = name
              Name of the service.
      .....
      - state
              `started'/`stopped' are idempotent actions that will not run
              commands unless necessary.
              `restarted' will always bounce the service.
              `reloaded' will always reload.
      
      是否要開機啟動,透過 enabled 為 true 或 false 來設計,至於 state 則是立刻啟動/關閉/重新啟動的設計。
    4. 修改網頁,如果是新的網頁,然後只具有一兩行的話,可以使用 copy 的 content 內容來處理即可。
  2. 開始撰寫 playbook :
    1. 假設檔名設定為 web_first.yml 好了:
      $ whoami; pwd
      student
      /home/student/ansible-init
      
      $ vim web_first.yml
      ---
        - name: setup web server and add first web page
          hosts: webserver1
          tasks:
            - name: install httpd packages
              yum:
                name: httpd
                state: latest
      
            - name: start and enable httpd service
              service:
                name: httpd
                enabled: true
                state: started
      
            - name: create first web page for index.html
              copy:
                content: 'name: vbird tsai\nID: g090axxx\n'
                dest: /var/www/html/index.html
      
    2. 開始檢查語法是否有問題:
      $ ansible-playbook --syntax-check web_first.yml
      playbook: web_first.yml
      
    3. 測試執行檢查環境:
      $ ansible-playbook -C web_first.yml
      PLAY [setup web server and add first web page] **************************************
      
      TASK [Gathering Facts] **************************************************************
      ok: [webserver1]
      
      TASK [install httpd packages] *******************************************************
      changed: [webserver1]
      
      TASK [start and enable httpd service] ***********************************************
      changed: [webserver1]
      
      TASK [create first web page for index.html] *****************************************
      changed: [webserver1]
      
      PLAY RECAP **************************************************************************
      webserver1  : ok=4  changed=3  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
      
      這代表指令執行應該會成功 (ok=4),而且可能會有改變任務的項目有三個 (chaged=3)。
  3. 執行、假查與測試:
    1. 開始執行 playbook 囉!
      $ ansible-playbook web_first.yml
      PLAY [setup web server and add first web page] **************************************
      
      TASK [Gathering Facts] **************************************************************
      ok: [webserver1]
      
      TASK [install httpd packages] *******************************************************
      changed: [webserver1]
      
      TASK [start and enable httpd service] ***********************************************
      changed: [webserver1]
      
      TASK [create first web page for index.html] *****************************************
      changed: [webserver1]
      
      PLAY RECAP **************************************************************************
      webserver1  : ok=4  changed=3  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
      
      $ ansible webserver1 -m command -a 'cat /var/www/html/index.html'
      webserver1 | CHANGED | rc=0 >>
      name: vbird tsai\nID: g090axxx\n
      
      這樣看起來,確實是有成功的執行了將首頁貼上去的功能!
    2. 使用 curl 檢查一下網頁囉!
      $ curl webserver1
      curl: (7) Failed to connect to webserver1 port 80: No route to host
      
      $ ansible webserver1 -m command -a 'systemctl status httpd'
      webserver1 | CHANGED | rc=0 >>
      ● httpd.service - The Apache HTTP Server
         Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled;vendor preset:disabled)
         Active: active (running) since Sun 2020-10-25 00:12:09 CST; 2min 53s ago
           Docs: man:httpd.service(8)
       Main PID: 28961 (httpd)
         Status: "Running, listening on: port 80"
          Tasks: 213 (limit: 11482)
         Memory: 39.1M
         CGroup: /system.slice/httpd.service
                 ├─28961 /usr/sbin/httpd -DFOREGROUND
                 ├─28962 /usr/sbin/httpd -DFOREGROUND
                 ├─28963 /usr/sbin/httpd -DFOREGROUND
                 ├─28964 /usr/sbin/httpd -DFOREGROUND
                 └─28965 /usr/sbin/httpd -DFOREGROUND
      
      $ ansible webserver1 -m command -a 'firewall-cmd --list-all'
      webserver1 | CHANGED | rc=0 >>
      public (active)
        target: default
        icmp-block-inversion: no
        interfaces: ens3
        sources:
        services: cockpit dhcpv6-client ssh
        ports:
        protocols:
        masquerade: no
        forward-ports:
        source-ports:
        icmp-blocks:
        rich rules:
      
      連不上 httpd 呢!原來是防火牆搞的鬼!我們都忘記啟動用戶端的防火牆 http 了!
  4. 增加任務與重複執行 playbook:
    1. 我們忘記加上 managed host 防火牆,所以現在要補強!防火牆使用 firewalld 模組:
      $ ansible-doc firewalld
      .....
      - immediate
              Should this configuration be applied immediately, if set as permanent.
              [Default: False]
              type: bool
      .....
      - permanent
              Should this configuration be in the running firewalld
              configuration or persist across reboots.
              [Default: (null)]
              type: bool
      .....
      - service
              Name of a service to add/remove to/from firewalld.
              The service must be listed in output of firewall-cmd --get-
              services.
              [Default: (null)]
              type: str
      .....
      = state
              Enable or disable a setting.
              For ports: Should this port accept (enabled) or reject
              (disabled) connections.
              The states `present' and `absent' can only be used in zone
              level operations (i.e. when no other parameters but zone and
              state are set).
              (Choices: absent, disabled, enabled, present)
              type: str
      .....
      
      我們目前只需要啟動 http 而已,所以可以改寫 web_first.yml 檔案了:
    2. 改寫 web_first.yml 檔案:
      $ vim web_first.yml
      .....
            - name: add http port to firewalld
              firewalld:
                immediate: true
                permanent: true
                service: http
                state: enabled
      
      $ ansible-playbook --syntax-check web_first.yml
      playbook: web_first.yml
      
      $ ansible-playbook web_first.yml
      PLAY [setup web server and add first web page] **************************************
      
      TASK [Gathering Facts] **************************************************************
      ok: [webserver1]
      
      TASK [install httpd packages] *******************************************************
      ok: [webserver1]
      
      TASK [start and enable httpd service] ***********************************************
      ok: [webserver1]
      
      TASK [create first web page for index.html] *****************************************
      ok: [webserver1]
      
      TASK [add http port to firewalld] ***************************************************
      changed: [webserver1]
      
      PLAY RECAP **************************************************************************
      webserver1  : ok=5  changed=1  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
      
      $ curl webserver1
      name: vbird tsai\nID: g090axxx\n
      
      這樣就搞定了!上面看起來有被更動的,也只有最後一個防火牆而已,因此就變成了 chaged 囉!
  5. 實做練習:想要在 /etc/hosts 裡面『增加』一行字。
    1. 因為是『增加』一行字,不是替代文字,因此,不能使用 copy 模組。此時,還有另外一個模組, 嘗試使用『 lineinfile 』模組,請自行查詢這個模組的可用參數。
    2. 建立名為 hosts_modify.yml 的檔案,這個 playbook 的重點是:將『 172.16.200.254 bigdata 』加入 /etc/hosts 內。
    3. 執行該 playbook 去影響 webserver1 主機
    4. 使用 ansible ad hoc 的功能,去看看 /etc/hosts 有沒有成功的加入了該行資料?
    5. 重複執行該 playbook,該行字會不會被持續加入呢?

Playbook 與 YAML 可用額外語法

  1. 執行 playbook 的額外資訊:
    1. ansible-playbook 如果要增加額外的訊息,可以加上 -v 這個參數喔:
      • -v :任務 (task) 的結果會顯示出來
      • -vv :設定資料與結果都會顯示出來
      • -vvv :包含連線到 managed hosts 之間的網路連線資訊也會顯示
      • -vvvv :更多額外的外掛資訊等,都會顯示喔!
    2. 測試執行 copy_hosts 的結果:
      $ ansible-playbook -vv copy_hosts.yml
      ansible-playbook 2.9.14
        config file = /home/student/ansible-init/ansible.cfg
        configured module search path = ['/home/student/.ansible/plugins/modules', 
           '/usr/share/ansible/plugins/modules']
        ansible python module location = /usr/lib/python3.6/site-packages/ansible
        executable location = /usr/bin/ansible-playbook
        python version = 3.6.8 (default, Apr 16 2020, 01:36:27) [GCC 8.3.1 20191121 
           (Red Hat 8.3.1-5)]
      Using /home/student/ansible-init/ansible.cfg as config file
      
      PLAYBOOK: copy_hosts.yml ************************************************************
      1 plays in copy_hosts.yml
      
      PLAY [copy /etc/hosts to managed hosts] *********************************************
      
      TASK [Gathering Facts] **************************************************************
      task path: /home/student/ansible-init/copy_hosts.yml:2
      ok: [webserver1]
      META: ran handlers
      
      TASK [copy /etc/hosts to another hosts] *********************************************
      task path: /home/student/ansible-init/copy_hosts.yml:5
      changed: [webserver1] => {"changed": true, "checksum": "423f985239e1e7eb5d52becf62da
         d1fac3e3b475", "dest": "/etc/hosts", "gid": 0, "group": "root", "md5sum": "677944b
         84b84bd729177b22a039b94c2", "mode": "0644", "owner": "root", "secontext": 
         "system_u:object_r:net_conf_t:s0", "size": 344, "src": "/home/sysadm/.ansible/tmp/
         ansible-tmp-1603562867.5132856-62179-209476908331227/source", "state": "file", "uid"
         : 0}
      META: ran handlers
      META: ran handlers
      
      PLAY RECAP **************************************************************************
      webserver1 : ok=2  changed=1  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
      
      比較重要的是 chaged 那一段,可以看到內容就有如 ansible ad hoc 執行的訊息顯示流程! 私訊非常完整。同時注意,該段訊息是以大括號 { } 將訊息包起來的喔!
  2. YAML 的額外語法說明
    1. 除了之前談到的縮排之外,如果 yuml 需要註解,可以在任何地方增加 # 即可在後面加入註解:
      # 這是註解
        -name: add file # 這也可以是註解
      
    2. YAML 的字串,通常不用設定啥,就是字串了!只是,如果你需要定義更清楚,可以使用單、雙引號處理:
      this is string words.
      'this is string words.'
      "this is string words."
      
      上面的語法都是被接受的!
    3. 如果有一行內容文字太長,你可能要分成好幾行,這樣比較好閱讀。這時,你可以使用兩個分隔字元來處理看看:
        - name: This playbook will treate your managed hosts for |
                its http service and
                php language and
                mysql SQL server.
          hosts: webserver1
      ...
      
      如果使用管線符號 (|) 來處理,則底下的文字會主動貼到上面這行來。 要注意的是,playbook 主要判斷內容的依據是冒號 (:),所以底下幾行都會黏到第一行去!第二種語法是:
        - name: This playbook will treate your managed hosts for >
                its http service and
                php language and
                mysql SQL server.
          hosts: webserver1
      ...
      
      兩者都可以達成這些目標喔!
    4. 使用字典格式 (dictionaries),亦即透過大括號 { } 處理:
      # 原始的格式是這樣:
          - name: copy /etc/hosts to another hosts
            copy:
              src: /etc/hosts
              dest: /etc/hosts
      
      # 可以改成這樣子:
          - name: copy /etc/hosts to another hosts
            copy:
              { src: /etc/hosts, dest: /etc/hosts }
      
    5. YAML 的列表功能,舉例來說,當你要安裝的軟體很多時,可以這樣處理:
      ---
        - name: setup web server and add first web page
          hosts:
            - webserver1
            - dbserver1
          tasks:
            - name: install httpd packages
              yum:
                name:
                  - httpd
                  - php
                  - mariadb
                state: latest
      
      使用 (- ) 作為開頭,同樣需要縮排喔!這樣就可以接上許多列表的資料了!
  3. 實做練習:加上 ftp 的服務功能
    1. 實施的主機群,請使用 website 這個主機群組!
    2. 設計一個 FTP 匿名登入伺服器的功能,需要的軟體主要是 vsftpd 這個服務,請查詢 yum 模組, 找到正確的參數,安裝好 vsftpd 模組
    3. 這個服務每次開機都要啟動,請找 service 模組,同樣啟動這個服務!
    4. 防火牆記得要放行 ftp 才行。
    5. 設計一個 readme.txt 檔案,放置到 myuser1 的家目錄當中, 內容就填寫『 This FTP daemon setted by Ansible 』即可。記得,使用者、群組要修改成為 myuser1 所有才對喔!
    6. 最後,使用 get_url 的模組,自我確認一下 ftp://webserver1/readmt.txt ,使用 myuser1 登入 (注意, 密碼為 itismyuser 喔!)是否可以將檔案儲存到 /dev/shm/checkftp.txt 當中?(因為使用 FTP 的關係, 你可能需要使用 ftp://username:password@hostname/dir/file 的格式來處理才行!)
    7. 上述 playbook 請寫入名為 ftp_first.yml 檔案中,並且測試與執行。