Ansible專欄文章之七:利用Role部署LNMP案例


回到:Ansible系列文章


各位讀者,請您:由於Ansible使用Jinja2模板,它的模板語法{% raw %} {{}} {% endraw %}和{% raw %} {%%} {% endraw %}和博客系統的模板使用的符號一樣,在渲染時會產生衝突,儘管我盡我努力地花了大量時間做了調整,但無法保證已經全部都調整。因此,如果各位閱讀時發現一些明顯的詭異的錯誤(比如像這樣的空的 行內代碼),請一定要回復我修正這些渲染錯誤。

7.更大的舞臺(2):利用Role部署LNMP案例

本文將介紹通過Role來搭建LNMP架構的案例,以便熟悉編寫Role的邏輯和過程,文章最後還提供了一種優化執行效率的方案。

爲了達到循序漸進的效果,最開始我會將本案例所有涉及到的功能寫在單個Role當中,後面會將大功能拆分並分離到多個小Role中,這也是一種比較規範的編寫Role的方式。但蘿蔔青菜各有所愛,有些人覺得是最佳實踐,有些人則持反對意見,無所謂,只要有自己的想法並認爲自己的好就行了。

文章內容較多,並不是想象中那樣簡簡單單安裝LNMP就完事,爲了編寫完美的Role,其中涉及很多重要的邏輯和知識點,甚至是一些理念,所以再次提醒,一定要做好筆記。

7.1 實驗環境說明

本文的測試環境如下:

節點說明 IP 系統版本 軟件版本
nginx節點 192.168.200.42 CentOS 7.2 nginx(rpm 1.16版)
PHP節點 192.168.200.43 CentOS 7.2 PHP 7.4
MySQL節點 192.168.200.44 CentOS 7.2 MySQL 5.7.22

建議在開始實驗之前先準備乾淨的虛擬機(已經配好ssh主機互信)並做好快照,在後面測試的時候,很可能需要多次恢復原始環境。

7.2 創建Role並提供inventory

從此處開始,使用https://github.com/malongshuai/ansible-column.git中的"7th/myroles"目錄。

我先將所有功能集中在單個Role中,後面會抽離、重整各類功能到不同的Role中。

創建Role的目錄結構:

$ mkdir roles
$ ansible-galaxy init lnmp --force --init-path roles/
$ tree
.
└── roles
    └── lnmp
        ├── defaults
        │   └── main.yml
        ├── files
        ├── handlers
        │   └── main.yml
        ├── meta
        │   └── main.yml
        ├── README.md
        ├── tasks
        │   └── main.yml
        ├── templates
        ├── tests
        │   ├── inventory
        │   └── test.yml
        └── vars
            └── main.yml

然後單獨爲LNMP這個功能配置一個inventory文件inventory_lnmp,它和roles目錄在同一個目錄下。初始時,這個inventory文件內容如下:

[nginx]
192.168.200.42

[php]
192.168.200.43

[mysql]
192.168.200.44

[dev:children]
nginx
php
mysql

7.3 配置yum鏡像源

在開始安裝並配置LNMP之前,第一個步驟就是配置各軟件包的yum鏡像源。在LNMP這個案例中,需要配置的包括:nginx官方源、php的remi和remisafe源、MySQL源。

配置yum源有多種方式,可以在Ansible控制端寫好repo文件,然後拷貝到目標節點上,也可以使用Ansible模塊yum_repository來配置,使用哪種方式可隨意,這裏我使用拷貝repo文件的方式。

爲圖簡單,我將所有軟件相關的yum鏡像源全寫在一個文件裏,並且會分發給所有目標節點,更佳方式是將nginx的yum源分發給nginx節點,mysql鏡像源分發給mysql節點。

步驟1:創建lnmp/files/lnmp.repo文件,內容如下:

[nginx]
name=nginx
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=0
enabled=1

[remi]
name=remirepo
baseurl=https://mirrors.tuna.tsinghua.edu.cn/remi/enterprise/$releasever/php74/$basearch/
enable=1
gpgcheck=0

[remisafe]
name=remisaferepo
baseurl=https://mirrors.tuna.tsinghua.edu.cn/remi/enterprise/$releasever/safe/$basearch/
enable=1
gpgcheck=0

[mysql57]
name=MySQL
baseurl=http://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el$releasever/
enabled=1
gpgcheck=0

上面repo文件中的baseurl指令使用了yum宏$releasever$basearch,所以這個repo文件可以在CentOS 6和CentOS 7版本通用。

步驟2:創建lnmp/tasks/add_repo.yml文件,寫一個任務去拷貝repo文件。其內容如下:

---
- name: add yum_repo for nginx, php, mysql
  copy: 
    src: lnmp.repo
    dest: /etc/yum.repos.d/lnmp.repo

步驟3:在lnmp/tasks/main.yml中引入add_repo.yml任務文件,內容如下:

---
- name: add_yum repo for nginx, php, mysql
  import_tasks: add_repo.yml

在此我使用的是import_tasks指令,使用include_tasks指令也沒有任何問題。但個人建議,在可以使用import_tasks的時候均不要使用include_tasksinclude_tasks指令限制比較多。

步驟4:提供一個入口playbook文件lnmp.yml來引入Role,它和roles目錄在同一個層次。內容如下:

---
- name: lnmp deploy
  hosts: dev
  gather_facts: false
  roles:
    - role: lnmp

然後進行測試:

$ ansible-playbook -i inventory_lnmp lnmp.yml

7.4 安裝並配置Nginx

然後安裝並配置nginx,相關的任務分佈在各個步驟中,在nginx配置完成後,我會將所有任務進行簡單的重構並整合在一起。

步驟1:創建lnmp/tasks/nginx_install_config.yml文件,在其中編寫安裝和配置nginx的任務。安裝任務如下:

---
- name: install nginx
  yum: 
    name: nginx
    state: present
  when: "'nginx' in group_names"

這裏加了when條件判斷,只有nginx組中的節點才執行任務,如果不加判斷,由於play的hosts指令指定的是dev主機組,這會使得所有節點都安裝nginx。

再介紹此處涉及到的變量group_names,它是Ansible的一個預定義特殊變量,之前也曾接觸過幾個預定義的特殊變量:inventory_hostname, play_hosts, hostvars。對於group_names變量,它保存了當前節點所在的主機組列表。例如,當192.168.200.42節點執行任務時,由於它屬於nginx主機組和dev主機組,所以它獲取到的group_names的值爲['nginx','dev'],而192.168.200.43執行任務時,獲取到的變量值爲['php','dev']

再來看when: "'nginx' in group_names"指令的條件,我想無需再解釋了。

nginx安裝完成後還需配置nginx,配置nginx的過程包括:

  • (1).在本地端寫好配置文件,包括nginx服務進程的配置和虛擬主機(即站點)的配置
  • (2).將配置文件發送給目標節點
  • (3).如果可以的話,在目標節點上檢查配置文件的語法
  • (4).啓動nginx服務
  • (5).提供測試頁面,並進行測試

也可以直接修改遠程的配置文件,但如果要修改的配置項較多,使用Ansible來做這樣的操作就很不方便。所以,更多的是將本地端寫好的配置文件拷貝到目標節點上。

步驟2:創建lnmp/templates/nginx.conf.j2文件作爲稍後要拷貝到目標節點的nginx配置文件,文件內容如下(每個配置項的含義我不做解釋,這是nginx的內容,而非Ansible的內容):

user nginx;
worker_processes {{ worker_processes }};
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

worker_rlimit_nofile {{worker_rlimit_nofile}};

events {
  worker_connections {{ worker_connections }};
  multi_accept {{ multi_accept }};
}

http {
  sendfile {{ send_file }};
  tcp_nopush {{ tcp_nopush }};
  tcp_nodelay {{ tcp_nodelay }};

  keepalive_timeout {{ keepalive_timeout }};
  server_tokens {{ server_tokens }};
  include /etc/nginx/mime.types;

  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log warn;

  gzip {{ gzip }};
  gzip_types {{ gzip_types | join(' ') if gzip_types is defined and gzip_types and (gzip_types | length > 0) else 'text/plain' }};
  gzip_min_length {{gzip_min_length}};
  gzip_disable "msie6";

  include /etc/nginx/conf.d/*.conf;
}

關於這個配置文件,有幾點需要說明:

  • (1).配置文件中並沒有將所有nginx指令的值直接配置好,而是使用了大量的變量進行佔位;
  • (2).因爲使用了變量佔位,所以需要提供這些變量的值,可將其定義在變量文件lnmp/vars/main.yml中,文件具體的內容見下文;
  • (3).因爲使用了變量佔位,所以這個配置文件並不是直接發送給目標節點的,而是先將佔位變量進行變量替換,替換過程稱爲渲染(render),然後將渲染完成後的數據發送給目標節點。所以,這不是一個普通的文件,它應當稱之爲一個模板文件,應該使用template模塊去處理這個文件併發送給目標節點,也因此將文件創建在lnmp/templates/nginx.conf.j2,以.j2後綴結尾表示這是Jinja2模板文件;
  • (4).在這個模板文件中,除了使用了變量佔位,還在gzip_types指令行中使用了Jinja2表達式,Jinja2在Ansible的應用中扮演了一個分量很重的角色,在後面我會專門使用一篇文章來詳細介紹它的功能和用法。稍後我會簡單介紹gzip_types這一行中表達式的含義,讓各位混個眼熟;
  • (5).Role中使用變量佔位是非常常見的方式,這種方式非常有助於後期的維護,也降低了配置和使用的難度,並且增強了Role的可移植性和通用性。比如,將這個Role發送給別人,別人只需要在變量文件nginx_config.yml中更改變量的值即可完成nginx的配置。

再來解釋下模板文件中gzip_types指令中使用的Jinja2表達式是什麼意思,目前各位只需看懂我的解釋即可,不用追求會寫,後面我會詳細介紹。

gzip_types {{gzip_types|join(' ') if gzip_types is defined and gzip_types and (gzip_types|length > 0) else 'text/plain'}};

它的語法模式爲:

<expr1> if <condition> else <expr2>

如果condition條件返回真,則執行expr1部分的代碼並返回,如果condition條件返回假,則執行expr2部分的代碼並返回。

對於此處的示例,gzip_types is defined and gzip_types and (gzip_types | length > 0)包含了三個條件判斷:

  • (1).gzip_types is defined用於判斷gzip_types變量是否已經定義;
  • (2).and後面的gzip_types等價於gzip_types is not none,用於判斷變量gzip_types定義後是否有賦值或是否被賦值爲null;
    • 在變量文件中將變量編寫爲var1: var1: null,表示未賦值或賦值爲null,它們等價;
  • (3).(gzip_types|length > 0)判斷gzip_types的元素個數是否大於0,也即表示gzip_types爲空列表時,將返回假。
    • 在變量文件中var1: []表示賦值爲空列表。

所以,如果gzip_types未定義,或定義了但未賦值、賦值爲null,或賦值爲空列表時,則if條件判斷均爲假,於是執行else分支,即返回text/plain,template模塊渲染時會將最終返回值替換到Jinja2代碼位置處。所以,該行最終渲染後得到的是gzip_types text/plain;

如果gzip_types已定義且賦值爲非null、非空列表時,則if條件判斷爲真,於是執行gzip_types|join(' ')。在這裏做了一個假設:gzip_types變量的值是一個列表,因爲這個變量用於指定nginx壓縮的文件類型,可能要指定多個要壓縮的類型,所以從邏輯上它應當是一個列表。

此外,這裏還使用了篩選器(filter),即那根豎線|,它表示將豎線左邊的返回值作爲參數傳遞給豎線右邊的篩選器函數進行處理,並最終返回。比如此處,將gzip_types變量的返回值也即變量自身的值當作參數傳遞給篩選器join(),join()篩選器的作用是將列表中的各個元素按照所指定的空格連接符連接起來,比如[1,2,3,4]|join('_')得到的是1_2_3_4

各位可能會覺得有些晦澀難懂,畢竟是第一次接觸到Jinja2裏的篩選器,沒關係,混個眼熟即可,後面還會詳細介紹,現在將其當作管道來理解即可。

步驟3:然後提供模板中涉及到的變量,將它們定義在變量文件lnmp/vars/main.yml中,內容如下:

worker_processes: 1
worker_rlimit_nofile: 65535
worker_connections: 10240
multi_accept: "on"
send_file: "on"
tcp_nopush: "on"
tcp_nodelay: "on"
keepalive_timeout: 65
server_tokens: "off"
gzip: "on"
gzip_min_length: 1024
# 1. You should assign a list to gzip_types,if you don't
#    want to use this variable, set it to an empty list,
#    such as "gzip_types: []".
# 2. There is no need to add "text/html" type for gzip_types,
#    gzip will always include "text/html".
gzip_types: 
  - text/plain
  - text/css
  - text/javascript
  - application/x-javascript
  - application/xml
  - image/jpeg
  - image/jpg
  - image/gif
  - image/png

注意其中的gzip_types是一個列表。

步驟4:提供默認變量,將它們保存在lnmp/defaults/main.yml中,內容如下:

worker_processes: 1
worker_rlimit_nofile: 1024
worker_connections: 1024
multi_accept: "on"
send_file: "on"
tcp_nopush: "off"
tcp_nodelay: "on"
keepalive_timeout: 65
server_tokens: "on"
gzip: "off"
gzip_min_length: 1024
gzip_types: ['text/plain']

通常來說,對於配置文件裏的變量都應當提供默認值,以保證即使在主變量文件中沒有配置對應變量時,模板文件也能正確渲染。

比如上面變量文件中的gzip_types本應是一個列表,但因爲用戶不想要壓縮功能或想要使用Nginx的默認值,於是將gzip_types變量設置爲空甚至直接刪除了這個變量,但因爲在模板配置文件中已經明確使用了nginx的gzip_types指令,模板引擎必須得爲其渲染出一個值出來,否則這個nginx配置文件就是語法錯誤。

所以,我在上面示例模板文件中加入了處理這類異常情況的if判斷。更簡單、更方面、更具可讀性的方式,是直接爲這些變量提供默認值,並在主變量文件中爲需要注意的變量加上註釋(正如我上面提供的lnmp/vars/main.yml文件加的註釋一樣)。

無論在哪門語言中,註釋都顯得無比重要,沒想到Ansible中也是如此吧?所以,也儘量爲你自己的playbook加上註釋。另外,要爲play和task設置好名稱。

有了這些默認變量,且有了註釋的保證,前面模板文件中的if判斷語句就可以簡化爲:

{{ gzip_types | join(' ') if gzip_types|length > 0 else 'text/plain'}}

爲了節省篇幅,我就不去再做修改,各位理解便可。

步驟5:既然nginx主配置文件相關內容都處理好了,接下來要編寫一個使用template模塊的任務,由該模板負責模板文件的渲染並將渲染好的內容拷貝到目標節點上。任務仍然寫在lnmp/tasks/nginx_install_config.yml文件中,追加的內容如下:

- name: render and copy nginx config
  template: 
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    backup: true
    validate: "/usr/sbin/nginx -t -c %s"
  when: "'nginx' in group_names"
  notify: "reload nginx"

template模塊的backup參數,表示在拷貝文件之前先備份目標節點上已經存在的文件。

validate參數在不少模塊中都存在,可用於數據校驗,這是非常實用的功能。比如此例中的template模塊,它表示文件拷貝到目標節點後但放置在目標位置之前(也就是說,拷貝的文件是先以臨時文件方式存在的,其實Ansible大多涉及到目標文件的操作都是先保存在目標節點的臨時文件中,然後再執行相關操作),先執行validate參數指定的命令nginx -t -c %s,該命令的作用是檢查拷貝過去的配置文件語法,%s表示拷貝後、替換前的目標文件名。如果validate校驗成功,則將臨時文件重命名爲最終目標/etc/nginx/nginx.conf,如果校驗失敗,則Ansible端報錯。過程如下圖:

另外,修改了nginx配置文件後需要重啓nginx(這裏是第一次配置nginx,只需啓動操作即可),所以這裏使用notify來觸發一個重啓nginx的handler,因此還需要編寫這個handler。

步驟6:在lnmp/handlers/main.yml中編寫reload nginx的handler,內容如下:

- name: reload nginx
  service: 
    name: nginx
    state: reloaded
    enabled: true

service模塊可以管理sysV和systemd的服務,換句話說,在CentOS 6和CentOS 7上都有效。

這裏各位可能會想,第一次將nginx配置文件拷貝過去時,應當是啓動或重啓操作,而不是reload,以後再拷貝配置文件過去,才應該是reload。

邏輯確實應該如此,但各位可以去ansible-doc -s service中看看手冊,state: reloaded狀態的功能有二:

  • (1)如果服務已啓動,則reload;
  • (2)如果服務未啓動,則start。

這完善的功能非常人性化。想象一下,如果service模塊沒有提供這個功能,就需要我們自己去判斷Nginx節點上的nginx服務是否已啓動。但要知道,如果想要遠程判斷的某個狀態,在Ansible中沒有原生提供相關功能,我們就只能自己使用命令的邏輯去判斷(比如遠程執行ps命令判斷進程狀態),但這種判斷方式比較麻煩。換句話說,Ansible在這一點上並不友好。

因爲遠程狀態判斷是一個非常常見的需求,所以這裏臨時轉移一下話題,我將判斷的方式提供給各位,以後肯定會有用武之地。如果暫時看不懂也沒關係,因爲經過下一篇文章之後肯定能看懂。

以獲取進程狀態爲例:

- name: get pids for sshd
  shell: |
    ps -ef | awk '/ssh[d]/{print $2}'
  register: pids
  failed_when: false
  changed_when: false

- name: print pids
  debug: 
    msg: "{{pids.stdout_lines | join(' ')}}"
  when: (pids.stdout_lines | length) > 0

另外,在Ansible 2.8中提供了pids模塊,可以遠程獲取給定進程名的pid列表,但和這裏的shell命令並沒有什麼區別,如有興趣各位可ansible-doc pids

回到正題,現在nginx主配置文件相關的內容都處理好了,接下來再提供nginx的站點配置文件,即使用Ansible配置nginx虛擬主機。

步驟7:移除nginx提供的默認虛擬主機文件/etc/nginx/conf.d/default.conf並提供我們自己編寫的虛擬主機配置。相關任務稍後編寫,現在先編寫虛擬主機的模板配置文件lnmp/templates/vhost.conf.j2

如果只需要提供一個虛擬主機,那麼直接寫虛擬主機文件即可,比如下面的:

server {
  listen       {{port}};
  server_name  {{server_name}};
  index index.html index.htm;
  location / {
    root {{document_root}};
  }
}

但我想更多場景下應該不止一個虛擬主機。所以,使用循環來生成多個虛擬主機的方式或許更佳。

所以,在lnmp/vars/main.yml變量文件中追加如下變量內容,表示要配置兩個nginx的虛擬主機:server1和server2。

vhosts:
  server1:
    server_name: www.abc.com
    listen: 80
    fastcgi_pass: "{{groups.php[0]}}:9000"
  server2: 
    server_name: www.def.com
    listen: 80
    fastcgi_pass: "{{groups.php[0]}}:9000"

這裏使用了另一個Ansible預定義特殊變量groups,它保存了inventory中所有主機組以及每個組中的所有節點。它是一個字典結構,key爲主機組名稱,value爲主機組中包含的節點列表。所以group.php表示找出php主機組中的節點列表,group.php[0]表示php組中第一個節點(示例中的php組只有一個節點)。

題外話:
發現不美好的地方了嗎?按照Role規範,所有的變量只能寫在固定的vars/main.yml裏,但可能我們想要將各類變量分開存放,比如爲Nginx主配置文件模板提供一個變量文件,爲虛擬主機配置文件模板提供一個變量文件。
後文我重整這個案例的時候,會介紹如何去調整變量的位置。

然後再提供虛擬主機模板配置文件lnmp/templates/vhost.conf.j2,其完整的內容如下:

server {
    listen       {{item.value.listen}};
    server_name  {{item.value.server_name}};

    location / {
        root   /usr/share/nginx/html/{{item.key}};
        index  index.html index.htm;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    location ~ \.php$ {
        fastcgi_pass   {{item.value.fastcgi_pass}};
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME /usr/share/www/{{item.key}}/php$fastcgi_script_name;
        include        fastcgi_params;
    }
}

關於其中的{{item.key}}{{item.value}}是什麼意思,稍後寫循環任務時會解釋。

另外注意其中的幾個root指令指定的目錄,稍後需要寫任務去創建它們。

其實,這個模板文件很不完善,大多數配置項都寫死了,這非常不利於維護。但因爲還沒有深入介紹Jinja2,所以目前只能達到這種程度,後面介紹Jinja2語法的時候,我將會爲各位完善這個模板,實現可以隨意添加虛擬主機、隨意添加配置項、隨意添加一項或多項location的功能,敬請各位的期待。

步驟8:編寫和虛擬主機相關的任務,這些任務仍然編寫在lnmp/tasks/nginx_install_config.yml文件中。包括的任務有:

  • (1).渲染虛擬主機模板文件並拷貝到目標節點
    • server1的目標文件名爲:/etc/nginx/conf.d/server1.conf
    • server2的目標文件名爲:/etc/nginx/conf.d/server2.conf
  • (2).移除nginx默認虛擬主機的文件/etc/nginx/conf.d/default.conf;
  • (3).創建每個虛擬主機涉及到的目錄
    • location /段涉及的目錄爲:/usr/share/nginx/html/server
    • location ~ \*.php$段涉及的目錄爲/usr/share/www/server{1,2}/php/,且在php節點上創建

首先寫移除Nginx默認虛擬主機文件的任務:

- name: remove nginx default vhost file
  file: 
    name: /etc/nginx/conf.d/default.conf
    state: absent
  when: "'nginx' in group_names"

然後寫渲染虛擬主機模板文件的任務:

- name: render and copy vhosts config
  template: 
    src: vhost.conf.j2
    dest: "/etc/nginx/conf.d/{{item.key}}.conf"
    # validate: "/usr/sbin/nginx -t -c %s"
  loop: "{{vhosts|dict2items}}"
  when: "'nginx' in group_names"
  notify: "reload nginx"

這裏有幾項需要說明:

  1. template模塊不要加上validate參數去檢查語法,因爲虛擬主機的配置文件是nginx配置文件的一部分,而且%s表示的是臨時文件名,不算是完整的Nginx配置文件,只檢查這部分文件的語法會報錯。
  2. notify會觸發reload nginx這個handler,它和之前拷貝nginx主配置文件時觸發的是同一個handler,但並沒有影響,因爲handler是在當前階段的任務執行完成後統一、且不重複執行的。
  3. 這裏loop指令迭代的目標是{% raw %} vhosts|dict2items {% endraw %},這裏又用到了篩選器filter,這裏簡單解釋下dict2items的作用。

在前面定義的vhosts變量是一個dict結構,在以前版本的Ansible中迭代字典的方式是使用with_dict指令,但現在都建議將with_xxx指令轉換成loop指令迭代,而loop指令只能迭代列表,所以這裏使用了dict2items篩選器將dict轉換成列表形式。

例如這裏執行{% raw %} vhosts|dict2items {% endraw %}之後,與之等價的結果是:

- key: server1
  value: 
    server_name: www.abc.com
    listen: 80
    fastcgi_pass: "{{groups.php[0]}}:9000"
- key: server2
  value: 
    server_name: www.def.com
    listen: 80
    fastcgi_pass: "{{groups.php[0]}}:9000"

於是,with_dict: "{{vhosts}}"和{% raw %} loop: "{{vhosts|dict2items}}" {% endraw %}是等價的,迭代之後,可以通過{{item.key}}分別獲取server1和server2,通過{{item.value}}分別獲取對應虛擬主機的配置項。

然後再編寫虛擬主機涉及到的目錄的任務。這裏有幾個要求:

  • (1)./usr/share/nginx/html/server{1,2}目錄在nginx節點上創建
  • (2)./usr/share/www/server{1,2}/php/目錄在php節點上創建

該任務可以寫成如下不太友好的方式:

- name: create dir on nginx
  file: 
    name: "/usr/share/nginx/html/{{item.key}}"
    state: directory
  loop: "{{vhosts|dict2items}}"
  when: "'nginx' in group_names"

- name: create dir on php
  file: 
    name: "/usr/share/www/{{item.key}}/php"
    state: directory
  loop: "{{vhosts|dict2items}}"
  when: "'php' in group_names"

之所以不友好是因爲創建php相關目錄本該是nginx節點上觸發的任務,但此處卻是在php節點執行任務時創建的,假如play的hosts指令中沒有選中php主機組呢?換句話說,nginx節點觸發的任務卻脫離了nginx。

更好的方式是使用之前曾提到過的delegate_to指令進行任務委託,在nginx節點執行任務到此任務時,臨時委託給php節點去創建php目錄。

所以,改寫成如下方式:

- name: create dir on nginx
  file: 
    name: "/usr/share/nginx/html/{{item.key}}"
    state: directory
  loop: "{{vhosts|dict2items}}"
  when: "'nginx' in group_names"

- name: create dir on php
  file: 
    name: "/usr/share/www/{{item.key}}/php"
    state: directory
  loop: "{{vhosts|dict2items}}"
  when: "'nginx' in group_names"
  delegate_to: "{{groups.php[0]}}"

這裏還能繼續進行優化,我也想繼續給各位完善下去,以便讓各位能寫出盡善盡美的Role,但實在篇幅受限,所以還是繼續下面的步驟。

步驟9:提供虛擬主機的測試頁面,這裏分別爲兩個虛擬主機提供兩個頁面:index.html和index.php,它們放在lnmp/files目錄下。

文件lnmp/files/index.html的內容如下:

<h1>hello world from index.html</h1>

文件lnmp/files/index.php的內容如下:

<?php echo "hello world from index.php"; ?>

步驟10:將測試頁面拷貝到nginx節點的/usr/share/nginx/html/server{1,2}和php節點的/usr/share/www/server{1,2}/php/。在lnmp/tasks/nginx_install_config.yml文件中繼續追加如下內容:

- name: copy index.html
  copy: 
    src: "index.html"
    dest: "/usr/share/nginx/html/{{item.key}}"
  loop: "{{vhosts|dict2items}}"
  when: "'nginx' in group_names"

- name: copy index.php
  copy:
    src: "index.php"
    dest: "/usr/share/www/{{item.key}}/php"
  loop: "{{vhosts|dict2items}}"
  when: "'php' in group_names"

爲什麼這裏又不使用委託的方式呢?使用任務委託也沒問題,只不過這是測試用的任務,沒必要太過講究,而且從更佳實踐的角度上考察,測試任務應當單獨編寫而不是和主任務放在一起,只不過在這個示例中我把所有任務都集中在單個Role中。

步驟11:最後做頁面測試。因爲php節點尚未部署,所以暫時只測試index.html。

- name: flush handler
  meta: flush_handlers

- name: test web page
  uri:
    url: "http://{{item.value.server_name}}:{{item.value.listen}}"
  loop: "{{vhosts|dict2items}}"
  run_once: true
  delegate_to: localhost

在這裏使用了uri模塊,它可以用來測試http/https頁面。如果只給定url參數,則默認GET該頁面,並在響應狀態碼爲200時表示測試成功。它還有更多、更靈活的用法,比如判斷頁面中是否包含了某個關鍵字,可在需要頁面測試時去參考官方手冊。

另外,測試任務是在Ansible端進行的,所以將這個任務委託給localhost執行。也因此注意,在測試時需要先在Ansible端/etc/hosts中添加DNS相關記錄:

192.168.200.42 www.abc.com www.def.com

另一方面,測試只需進行一次就可以了,沒必要重複測試,所以上面使用了run_once指令。這個指令的作用是隻在第一個選中的目標節點上執行一次(嚴格地說是在第一批的第一個節點),後續節點不再執行這個任務,但後續節點卻能獲取到第一個節點執行完成後的成果,比如在該任務中聲明瞭一個變量,雖然後續節點未執行該任務,但後續節點也能獲取到這個變量的值。

正如示例中,理論上它第一個選中的應該是nginx節點,在nginx節點執行到該任務時會委託給localhost執行一次,之後php節點和mysql節點雖然也選中了,但不會再執行到該任務,也就不會再委託。

run_once某些時候也有其他實現方式,比如:

when: inventory_hostname == play_hosts[0]

但並非總是能等價,run_once自有其優點,比如這個示例中已經有了run_once,那麼就可以繼續使用when做其它判斷。

現在測試頁面已經寫好了,似乎已經完事了,不過再想一想,在執行測試任務之前,nginx服務啓動了嗎?沒有,因爲這是第一次配置nginx,啓動nginx的操作留給了handler來完成,但是handler是在當前階段的所有任務執行完後纔開始執行的。

所以,在測試之前應當先啓動nginx。這裏我使用了meta: flush_handlers任務,當執行到該任務時,它會立即去執行當前階段已經觸發的所有handler任務。注意,這是改變Ansible執行流程的一種方式,必須掌握。

關於meta,它是Ansible中一個非常神奇的模塊,它的功能都直接作用在Ansible本身,雖然提供的功能不算多,但卻能解決一些比較棘手的需求,比如示例中所用的flush_handlers可以立即去執行已經觸發的handler,再比如meta: end_play可以立即終止當前play並進入下一個play,也就是說,當前play中剩下的任務不執行了,這也改變了任務的執行流程。此外還有幾個功能,各位可參考官方手冊:https://docs.ansible.com/ansible/latest/modules/meta_module.html去了解了解,以備不時之需。

最後,還有一個注意事項需要提醒一下,上面的測試任務是在nginx啓動後立即執行的,對於這裏的nginx reload來說這沒什麼問題,但對於某些啓動較慢的服務,在啓動任務後加上一個延遲行爲,這樣的效果是否會更佳?Ansible提供了wait_for模塊和pause模塊,前者用於等待指定的條件發生後才繼續執行後續任務,後者用於等待指定的超時時間,它們用法都非常簡單,各位可去看看官方手冊,後續的文章中可能也有用上它們的機會。

比如,上面的測試任務可以如下方式修改:

- name: flush handler
  meta: flush_handlers

# 等待80端口開啓才繼續執行後面的任務
# - wait_for:
#     port: 80
#     state: started

# 睡眠1秒後才繼續執行後面的任務
# - pause: 
#     seconds: 1

- name: test web page
  uri:
    ...

步驟12:將nginx_install_config.yml引入到lnmp/tasks/main.yml文件中。

lnmp/tasks/main.yml文件中的內容爲:

---
# 添加yum源
- name: add_yum repo for nginx, php, mysql
  import_tasks: add_repo.yml

# 安裝nginx、配置nginx、配置nginx虛擬主機、測試虛擬主機
- name: nginx config
  import_tasks: nginx_install_config.yml

步驟13:執行Role進行測試。

因爲當前已經編寫的所有任務都具有冪等性,所以現在執行測試且多次執行也沒有任何影響。

$ ansible-playbook -i inventory_lnmp lnmp.yml

如果測試一切OK,那麼進行最後一步,整理衆多的task。

7.5 整理nginx的衆多任務

目前爲止,絕大多數任務都寫在lnmp/tasks/nginx_install_config.yml中,但這裏面的任務有不少指令重複了,比如幾乎所有任務中都帶上了when判斷只在nginx節點上執行,比如和虛擬主機配置相關的任務中都有{% raw %} loop: "{{vhosts|dict2items}}" {% endraw %}迭代指令。對於重複的指令,如果條件允許,都建議將其抽取出來放在更高層次以避免重複書寫,比如寫在引入任務文件的指令層次上。再者,前面曾提到過測試任務和主任務寫在一起並不合理,所以應當將它們分開。

在這個步驟中我會在nginx Role層次對這三方面問題進行整理。

第一步,將nginx_install_config.yml中重複的when指令移除,並將when添加在lnmp/tasks/main.yml引入該任務文件的指令處。

修改後的lnmp/tasks/main.yml文件內容爲:

---
# 添加yum源
- name: add_yum repo for nginx, php, mysql
  import_tasks: add_repo.yml

# 安裝nginx、配置nginx、配置nginx虛擬主機、測試虛擬主機
- name: nginx config
  import_tasks: nginx_install_config.yml
  when: "'nginx' in group_names"

因爲使用的是import_tasks引入方式,所以when指令會拷貝到所有子任務上。

第二步,將nginx_install_config.yml中重複的{% raw %} loop: "{{vhosts|dict2items}}" {% endraw %}任務抽取出來,寫在另一個任務文件nginx_vhost_config.yml中,然後include_tasks引入該文件,並加上loop指令。

所以,整理後的nginx_install_config.yml文件內容如下:(因爲測試任務要單獨編寫,所以這裏也將測試相關任務移除了)

---
- name: install nginx
  yum: 
    name: nginx
    state: present

- name: render and copy nginx config
  template: 
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    backup: true
    validate: "/usr/sbin/nginx -t -c %s"
  notify: "reload nginx"

- name: nginx vhost config
  include_tasks: nginx_vhost_config.yml
  loop: "{{vhosts|dict2items}}"
  loop_control: 
    extended: yes

注意其中的loop_control,稍後我會解釋爲什麼要加上它。

其中include_tasks所引入的nginx_vhost_config.yml文件的內容如下:

# 注意這個任務的改變
- name: remove nginx default vhost file
  file: 
    name: /etc/nginx/conf.d/default.conf
    state: absent
  when: ansible_loop.first

# 下面這些任務除了移除了when指令和loop指令,沒有任何變化
- name: render and copy vhosts config
  template: 
    src: vhost.conf.j2
    dest: "/etc/nginx/conf.d/{{item.key}}.conf"
  notify: "reload nginx"

- name: create dir on nginx
  file: 
    name: "/usr/share/nginx/html/{{item.key}}"
    state: directory

- name: copy index.html
  copy: 
    src: "index.html"
    dest: "/usr/share/nginx/html/{{item.key}}"

- name: create dir on php
  file: 
    name: "/usr/share/www/{{item.key}}/php"
    state: directory
  delegate_to: "{{groups.php[0]}}"

- name: copy index.php
  copy:
    src: "index.php"
    dest: "/usr/share/www/{{item.key}}/php"
  delegate_to: "{{groups.php[0]}}"

這個文件裏唯一需要注意的是第一個任務:移除nginx默認虛擬主機文件。這個任務原本是沒有帶{% raw %} loop: "{{vhosts|dict2items}}" {% endraw %}指令的,但它也屬於nginx虛擬主機配置相關的任務,所以也抽取到這個文件中。爲了讓這個原本不在循環內的任務只執行一次,所以加了循環判斷when: ansible_loop.first,這表示只在第一輪loop循環時執行,之後每輪loop循環都不再執行。由於使用了ansible_loop.first這個循環過程中的信息,所以在前面loop指令中還需加上loop_control的extended參數。仍然是那句話,如果這裏暫時看不懂也沒關係,後面在進階深入Ansible的章節中會系統性介紹。

第三步,單獨編寫測試任務,將它與主任務分離。

編寫測試任務的邏輯有很多種,比如在入口playbook文件中編寫,或者單獨寫一個測試任務文件test.yml,或者將測試任務寫在Role任務執行流程的尾部,等等。但我想,將測試Role的任務規範化應該更佳,比如將所有的測試任務放在Role的test目錄中。

不知各位是否曾注意到,在ansible-galaxy init創建Role結構的時候,同時也會創建tests目錄,並且在此目錄下有inventory文件和test.yml文件:

$ tree roles/lnmp/tests/
roles/lnmp/tests/
├── inventory
└── test.yml

在inventory文件中可以提供測試所用的inventory,在test.yml中可以編寫測試所用的playbook。

其實這兩個文件中初始時已經有一些內容,比如對於lnmp這個Role,它的test.yml初始內容如下:

$ cat roles/lnmp/tests/test.yml 
---
- hosts: localhost
  remote_user: root
  roles:
    - lnmp

對於手動的Ansible測試,上面的文件測試時會報錯,因爲在tests目錄下找不到roles/lnmp這個Role。如果藉助測試工具或者自己寫測試腳本則可以改變測試邏輯。

不過對於這裏手動測試,對這個測試文件稍作修改即可。下面是我修改後的test.yml文件內容:

---
- hosts: dev
  gather_facts: false
  roles:
    - ../../lnmp

- name: test nginx
  hosts: dev
  gather_facts: false
  vars_files:
    - ../../lnmp/vars/main.yml
  tasks:
    - name: test web pages
      uri:
        url: "http://{{item.value.server_name}}:{{item.value.listen}}"
      loop: "{{vhosts|dict2items}}"
      run_once: true
      delegate_to: localhost
      tags: test

然後執行test.yml來測試:

$ ansible-playbook roles/lnmp/tests/test.yml -i inventory_lnmp --tags test

至此,nginx配置部署算是完成了,真正需要編寫的內容並不多,但這其中花了很大的篇幅來描述這個部署過程,希望各位能從繁瑣的知識點中整理好自己的筆記並將整個過程在自己的腦海裏化繁爲簡。

在經歷了部署nginx這個複雜的塊頭之後,接下來再部署的PHP和MySQL節點,相對來說要簡單許多。

7.6 安裝並配置PHP

安裝、配置PHP的過程出奇的簡單,因爲做實驗只需修改下php-fpm的監聽地址和端口即可,其它沒什麼可配置的_。如果各位確實想要修改配置,使用sed命令或lineinfile模塊去修改,或者按照配置nginx時的邏輯,將配置項定義成變量佔位方式並提供變量文件即可。

因爲任務全寫在單個Role中,所以創建lnmp/tasks/php_install_config.yml,內容如下:

---
- name: install php and php-fpm
  yum: 
    name: "{{item}}"
    state: installed
  loop:
    - php
    - php-fpm

- name: change php-fpm listen address and port
  shell: |
    sed -ri 's/^(listen *= *)127.0.0.1.*$/\1{{phpfpm_addr}}:{{phpfpm_port}}/' /etc/php-fpm.d/www.conf
    sed -ri 's/^(listen.allowed_clients.*)$/;\1/' /etc/php-fpm.d/www.conf

- name: restart php-fpm
  service:
    name: php-fpm
    state: reloaded

因爲修改了php-fpm的監聽地址,並且使用的是變量佔位方式,所以在lnmp/vars/main.yml中提供相關變量,追加內容如下:

phpfpm_addr: 0.0.0.0
phpfpm_port: 9000

然後在lnmp/tasks/main.yml中引入該任務文件,追加的內容如下:

- name: php config
  import_tasks: php_install_config.yml
  when: "'php' in group_names"

注意php配置任務只在php節點執行,所以引入時加上when條件判斷。

然後執行測試:

$ ansible-playbook -i inventory_lnmp lnmp.yml

順帶着,可以測試一下index.php頁面。在lnmp/tests/main.yml中添加:

- name: test web pages
  uri:
    url: "http://{{item.value.server_name}}:{{item.value.listen}}/index.php"
  loop: "{{vhosts|dict2items}}"
  run_once: true
  delegate_to: localhost
  tags: test

然後執行:

$ ansible-playbook roles/lnmp/tests/test.yml -i inventory_lnmp --tags test

上面測試index.php頁面的任務和之前測試index.html頁面的任務幾乎是重複定義的,唯一不同的是在URL尾部加上了"/index.php"後綴,其實應該用循環的方式來迭代測試index.html和index.php,這樣可以將兩個任務合併。

只是前面已經存在了{% raw %} loop: "{{vhosts|dict2items}}" {% endraw %}循環,再加入一個循環迭代,就得將嵌套循環派上用場。

在以前版本中,嵌套循環使用with_nested指令來實現,現在可以使用loop指令加product篩選器實現。下面兩種方式是等價的:(混個眼熟吧)

- name: test web pages
  uri:
    url: "http://{{item[0].value.server_name}}:{{item[0].value.listen}}/{{item[1]}}"
  with_nested:
    - "{{vhosts|dict2items}}"
    - ['index.html', 'index.php']
  run_once: true
  delegate_to: localhost
  tags: test

- name: test web pages
  uri:
    url: "http://{{item[0].value.server_name}}:{{item[0].value.listen}}/{{item[1]}}"
  loop: "{{vhosts|dict2items|product(['index.html','index.php'])|list }}"
  run_once: true
  delegate_to: localhost
  tags: test

7.7 安裝並配置MySQL

最後部署MySQL。部署MySQL其實......算了,不多說廢話了,直接開始吧。

步驟1:創建一個任務文件lnmp/tasks/mysql_install_config.yml,其中編寫安裝mysql server、mysql client、python2-PyMySQL、MySQL-python相關的任務。

其中python2-PyMySQL包和MySQL-python包是使用Ansible MySQL相關模塊時需要的,前者安裝時指定爲python-PyMySQL即可,它在epel鏡像源中,所以需要先設置好epel鏡像源。

任務文件lnmp/tasks/mysql_install_config.yml的內容如下:

---
- name: install mysql
  yum: 
    name: "{{item}}"
    state: installed
  loop: 
    - mysql-community-server
    - mysql-community-client
    - python-PyMySQL
    - MySQL-python

然後在lnmp/tasks/main.yml中引入該任務文件,所以在此文件中追加如下內容:

- name: mysql config
  import_tasks: mysql_install_config.yml
  when: "'mysql' in group_names"

步驟2:提供MySQL的模板配置文件lnmp/templates/mysql.cnf.j2

模板文件mysql.cnf.j2內容如下:

[client]
socket = {{mysql.client.socket}}

[mysqldump]
max_allowed_packet = {{mysql.mysqldump.max_allowed_packet}}

[mysqld]
port = {{mysql.mysqld.port}}
datadir = {{mysql.mysqld.datadir}}
socket = {{mysql.mysqld.socket}}
server_id = {{mysql.mysqld.server_id}}
log-bin = {{mysql.mysqld.log_bin}}
sync_binlog = {{mysql.mysqld.sync_binlog}}
binlog_format = {{mysql.mysqld.binlog_format}}
character-set-server = {{mysql.mysqld.character_set_server}}
skip_name_resolve = {{mysql.mysqld.skip_name_resolve}}
pid-file = {{mysql.mysqld.pid_file}}
log-error = {{mysql.mysqld.log_error}}

注意,變量名中一定不能出現-,因爲YAML中的-是保留符號,它表示定義一個列表,因此在key中不能出現短橫線。

步驟3:在lnmp/vars/main.yml中提供MySQL配置文件中涉及的變量。

在變量文件lnmp/vars/main.yml中追加如下內容:

mysql:
  client: 
    socket: "/data/mysql.sock"
  mysqldump: 
    max_allowed_packet: "32M"
  mysqld: 
    port: 3306
    datadir: "/data"
    socket: "/data/mysql.sock"
    server_id: 100
    log_bin: "mysql-bin"
    sync_binlog: 1
    binlog_format: "row"
    character_set_server: "utf8mb4"
    skip_name_resolve: 1
    pid_file: "/data/mysql.pid"
    log_error: "/data/error.log"

在這個變量中/data重複出現了多次,或許有人會想,將重複部分以變量的方式抽取出來多好,但很遺憾,YAML自身並不支持在同一個結構當中引用兄弟元素的值。

換句話說,下面這個結構中,沒有任何辦法將/home/junmajinlong這個重複的部分抽取出來並複用到rubyhome上(我翻遍網上資料也沒找到方案,如果你知道請一定告訴我)。

- path:
    home: /home/junmajinlong
    rubyhome: /home/junmajinlong/ruby

但不同結構中是可以引用的,比如下面的示例:

- home: /home/junmajinlong
- rubyhome: "{{home}}/ruby"

此外,如果只是簡簡單單的引用目標元素,而不連接其它字符串時,還可以使用YAML的錨定和引用(也稱別名alias)功能,即使是引用兄弟元素也能實現。關於YAML的錨定和別名用法,在編程時用的比較多,在Ansible中用的很少很少,所以這裏我只給幾個示例稍作介紹,各位如有興趣可自行搜索相關資料。

例如,下面的YAML語法是正確的:

- path: 
    p1: 
      home: &refme /home/junmajinlong
      rubyhome: *refme
    p2: 
      home: *refme
      pythonhome: *refme

這個結構等價於:

- path: 
    p1: 
      home: /home/junmajinlong
      rubyhome: /home/junmajinlong
    p2: 
      home: /home/junmajinlong
      pythonhome: /home/junmajinlong

其中&符號表示創建一個錨定,*表示引用指定的錨定。

注意,引用錨時不能額外連接字符串,只能單獨引用。例如*refme/ruby*refme"/ruby"都是錯誤的語法,YAML自身也沒有任何方法實現這樣的功能。

步驟4:在mysql_install_config.yml中編寫任務,渲染配置文件,並在MySQL節點創建配置文件中涉及到的datadir目錄,並設置該目錄的所有者和所屬組爲mysql。

追加的內容如下:

- name: render mysql config
  template:
    src: mysql.cnf.j2
    dest: /etc/my.cnf
  notify: restart mysql

- name: create mysql datadir
  file: 
    name: "{{mysql.mysqld.datadir}}"
    state: directory
    owner: mysql
    group: mysql

步驟5:在lnmp/handlers/main.yml中編寫restart mysql這個handler任務。

追加的內容如下:

- name: restart mysql
  service:
    name: mysqld
    state: restarted

注意,mysqld服務不支持reload操作。

步驟6:從MySQL 5.7開始,初始化mysql(或第一次啓動)時會創建臨時密碼並保存在MySQL的error log中,之後要連接到MySQL進行操作,必須先使用ALTER USER語句修改root@localhost的密碼。既然要使用Ansible來完成MySQL的初始化配置,它應當要去完成這個任務。

所以,先從遠程MySQL節點獲取到這個密碼,然後使用mysql的ALTER USER語句更新密碼。於是,在mysql_install_config.yml中繼續編寫任務:

- block:
    - name: get initialize temp password
      shell: |
        sed -rn '/temp.*pass/s/^.*root@localhost: (.*)$/\1/p' {{mysql.mysqld.log_error}}
      register: tmp_passwd

    - name: modify root@localhost password before any op
      shell: |
        mysql -uroot -p'{{tmp_passwd.stdout}}' \
        --connect-expired-password \
        -NBe \
        'ALTER USER "root"@"localhost" identified by "{{mysql.mysql_passwd}}";'
  tags: 
    - never

上面第二個任務中使用了變量mysql.mysql_passwd來指定修改後的密碼,所以在lnmp/vars/main.yml中加入這個變量:

mysql:
  mysql_passwd: "P@ssword1!"
  client: 
    socket: "/data/mysql.sock"
  mysqldump: 
    max_allowed_packet: "32M"
  mysqld: 
    port: 3306
    ......

注意,此處爲了簡單,MySQL密碼直接以明文方式保存在變量文件中,後面會有一篇文章介紹如何使用Ansible Vault來加密敏感數據,以保證安全性。

然後解釋下block中的這兩個任務。

第一個任務使用了shell模塊執行sed命令來篩選遠程mysql節點上創建的臨時密碼,這個命令的執行結果(即密碼)是稍後要在Ansible中繼續使用的,所以應當將其保存下來。

使用register指令可以將模塊執行後的返回值註冊爲一個變量,比如上面示例中是將shell模塊的返回值保存在tmp_passwd變量中,之後的任務比如修改密碼的操作就可以使用tmp_passwd這個變量。

每個模塊的返回值類型都不相同,具體可參考對應模塊的手冊。對於shell模塊執行的遠程命令來說,經常會將其結果使用register註冊成變量,所以有必要了解它的返回結果中包含了哪些內容。對於此,測試便知。

---
- hosts: localhost
  gather_facts: false
  tasks: 
    - shell: echo hahaha
      register: result
    - debug: 
        var: result

執行結果:

$ ansible-playbook a.yml
......
TASK [debug] **************************
ok: [localhost] => {
    "result": {
        "changed": true,
        "cmd": "echo hahaha",
        "delta": "0:00:00.001855",
        "end": "2019-12-29 02:02:40.411330",
        "failed": false,
        "rc": 0,
        "start": "2019-12-29 02:02:40.409475",
        "stderr": "",
        "stderr_lines": [],
        "stdout": "hahaha",
        "stdout_lines": [
            "hahaha"
        ]
    }
}
......

從結果中不難發現,shell模塊的返回值中包含了多項,它們都保存在一個字典結構中,包括:

  • (1).所執行的命令cmd
  • (2).開始執行的時間start、執行結束的時間end以及執行命令花掉的時間delta
  • (3).是否失敗failed
  • (4).命令的退出狀態碼rc,Ansible默認只認0退出狀態碼爲正確,其它狀態碼均報錯處理
  • (5).標準錯誤stderr以及列表方式保存的標準錯誤行stderr_lines
  • (6).標準輸出stdout以及列表方式保存的標準輸出行stdout_lines
  • (7).shell模塊的changed狀態(對於shell模塊,只要命令執行了,changed狀態一定爲true)

所以,要獲取命令的輸出結果,只需使用result.stdout即可:

- debug:
    var: result.stdout

執行結果:

$ ansible-playbook a.yml
......
TASK [debug] *********************
ok: [localhost] => {
    "result.stdout": "hahaha"
}
......

再回到block這個指令。它組織了兩個比較特殊的任務,特殊之處在於修改臨時密碼是隻在第一次連接MySQL時執行的,其它時候均不會執行,所以在block上加了一個名爲never的特殊標籤,這個特殊標籤的作用是:只有在ansible-playbook命令行中顯式指定了--tags never時纔會執行帶有這個標籤的任務,其它時候均不執行這些任務。

對於本例來說,修改MySQL臨時密碼的操作只應在第一次執行,所以第一次執行playbook時需以如下方式執行:

$ ansible-playbook -i inventory_lnmp --tags "all,never" lnmp.yml

以後再執行則不用加never標籤,直接執行即可,它會自動跳過所有帶never標籤的任務,:

$ ansible-playbook -i inventory_lnmp lnmp.yml

還有幾個其它的特殊標籤:all、never、tagged、untagged、always,它們的含義參考官方手冊:特殊tag

另一種實現任務只執行一次的方式是在變量文件中提供一個狀態變量,比如temp_passwd_updated,初始時該變量值爲0,當修改密碼後通過lineinfile模塊來修改這個變量的值爲1(該任務委託給Ansible端即可),然後在修改臨時密碼的block層次上加一個when指令,判斷該變量的值。在後文重整Role的時候將採用這種方式。

步驟7:對於剛安裝的MySQL,除了修改密碼外,還應移除不安全用戶,創建某些用戶,創建、刪除某些數據庫,等等。(注:MySQL 5.7初始化後就已經移除了test數據庫和不安全的用戶)

這些操作可以使用shell模塊執行mysql命令或mysqladmin命令來完成。這裏需要提醒各位,在Ansible的shell模塊中使用mysql命令遠程操作時,可以考慮加上-N -B這兩個選項:-N選項表示不輸出查詢的字段名,-B表示不輸出邊框。

對比下就知道這兩個選項的作用:

$ mysql -uroot -pP@ssword1! -e'select user,host from mysql.user;'
+---------------+-----------+
| user          | host      |
+---------------+-----------+
| mysql.session | localhost |
| mysql.sys     | localhost |
| root          | localhost |
+---------------+-----------+

$ mysql -uroot -pP@ssword1! -Ne'select user,host from mysql.user;'
+---------------+-----------+
| mysql.session | localhost |
|     mysql.sys | localhost |
|          root | localhost |
+---------------+-----------+

$ mysql -uroot -pP@ssword1! -Be'select user,host from mysql.user;'
user    host
mysql.session   localhost
mysql.sys       localhost
root    localhost

$ mysql -uroot -pP@ssword1! -NBe'select user,host from mysql.user;'
mysql.session   localhost
mysql.sys       localhost
root    localhost

除了使用shell模塊遠程執行mysql命令,也可以使用Ansible爲MySQL提供的幾個模塊:

  • mysql_db:用於創建、刪除MySQL數據庫
  • mysql_info:用於收集MySQL服務的信息
  • mysql_user:用於創建、刪除MySQL上的用戶以及權限管理
  • mysql_variables:用於管理MySQL的全局變量
  • mysql_replication:用於管理MySQL replication相關功能,比如獲取slave、更改master、啓停slave等等

通過這些模塊,可以在遠程通過python mysql相關的庫連接到MySQL上並執行操作。注意,不是Ansible端直接連接遠端MySQL,而是將連接的任務分發到MySQL節點上,並在MySQL節點上進行本地連接。

在使用這些模塊時,有兩個注意事項:

  • (1).要求目標節點上已經安裝了python-PyMySQL、MySQL-python
  • (2).既然要連接MySQL,就需要提供用戶名和密碼,可以使用這些模塊的login_userlogin_password參數來指定,如果不指定,則首先從MySQL本地的~/.my.cnf中讀取,如果MySQL節點沒有該文件或沒有想讀取的內容,則默認以root用戶、空密碼進行連接

如果使用~/.my.cnf文件提供連接時的用戶名和密碼,則至少需要提供如下內容:

[client]
user = ...
password = ...

所以,爲了以後能使用Ansible操作MySQL,寫一個任務來創建這個遠程文件,於是在lnmp/tasks/mysql_install_config.yml中追加如下任務:

- name: set mysql connection info for ansible
  template:
    src: .my.cnf.j2
    dest: /root/.my.cnf

並創建lnmp/templates/.my.cnf.j2文件,內容如下:

[client]
user = root
password = {{mysql.mysql_passwd}}
socket = {{mysql.mysqld.socket}}

然後就可以使用這些MySQL模塊來執行操作。比如,管理MySQL用戶和權限、創建和刪除數據庫、備份和恢復數據庫,等等。

下面是一些示例,更多用法請參見這些模塊的官方手冊。

# 移除localhost的匿名用戶
- name: remove anonymous user for localhost
  mysql_user:
    name: ""
    host: localhost
    state: absent

# 移除所有匿名用戶
- name: remove all anonymous user
  mysql_user:
    name: ""
    host_all: true
    state: absent

# 手動指定連接MySQL的用戶名和密碼,然後創建新用戶並指定權限
- name: create user and grant privileges
  mysql_user: 
    name: "junmajinlong"
    host: "192.168.200.%"
    password: 'P@ssword2!'
    priv: '*.*:ALL'
    state: present
    login_user: root
    login_password: 'P@ssword1!'

# 創建數據庫
- name: Create new databases with names 'foo' and 'bar'
  mysql_db:
    name: test
    state: present

# 創建多個數據庫
- name: Create new databases with names 'foo' and 'bar'
  mysql_db:
    name:
      - foo
      - bar
    state: present

# 刪除test數據庫
- name: drop test database
  mysql_db: 
    name: test
    state: absent

# 刪除多個數據庫
- name: drop databases with names 'foo' and 'bar'
  mysql_db:
    name:
      - foo
      - bar
    state: absent

# 備份多個數據庫
- name: Dump multiple databases
  mysql_db:
    state: dump
    name: foo,bar
    target: /tmp/dump.sql

# 備份數據庫並壓縮爲.bz2、.xz或.gz格式
- name: Dump multiple databases
  mysql_db:
    state: dump
    name: foo,bar
    target: /tmp/dump.sql.bz2

# 恢復數據庫,需要先將文件拷貝到MySQL節點
- name: Copy database dump file
  copy:
    src: /tmp/dump.sql.bz2
    dest: /tmp

- name: Restore database
  mysql_db:
    name: my_db
    state: import
    target: /tmp/dump.sql.bz2

至此,整個LNMP結構就部署完成了,這漫長的過程並不像想象中那樣簡單,其中涉及了很多邏輯和知識點,好在現在終於完成了所有任務。但是真的結束了嗎?並沒有。在前面我將所有的任務全都集中在單個Role中,這些任務數量多了,雖然已經分別保存在不同的文件當中,但仍顯得混亂。另一方面,前面所有的變量都定義在單個變量文件vars/main.yml中,這些變量也很混亂。

所以,更佳方式是將各類任務分別定義成Role來自治。

7.8 任務分離到多個Role中

從此處開始,使用https://github.com/malongshuai/ansible-column.git中的"7th/lnmp"目錄。

既然要分離成多個Role,這裏我準備將安裝配置nginx、PHP、MySQL的任務分別分離爲三個Role。

另外,有些任務是所有節點上都執行的,比如配置SSH主機互信、配置時間同步、配置防火牆等等,這類通用性任務通常會分離成另一個稱爲common的Role(當然,名稱是隨意的,common比較見名知意)。這裏我也劃分一個common的Role,用來保存配置yum源的任務。

所以,重新組織的目錄結構如下:

$ tree -L 2 -F lnmp/ 
lnmp/
├── inventory_lnmp
├── lnmp.yml
└── roles/
    ├── common/
    ├── mysql/
    ├── nginx/
    └── php/

先編寫入口playbook文件lnmp/lnmp.yml,內容如下:

---
- name: common config
  hosts: all
  gather_facts: false
  roles: 
    - common
  tags: 
    - common

- name: install and config nginx
  hosts: nginx
  gather_facts: false
  roles: 
    - nginx
  tags: 
    - nginx

- name: install and config PHP
  hosts: php
  gather_facts: false
  roles: 
    - php
  tags: 
    - php

- name: install and config mysql
  hosts: mysql
  gather_facts: false
  roles: 
    - mysql
  tags: 
    - mysql

然後再整理各個Role。各個Role中大多數任務的內容基本沒有改變,只是換了存放位置,所以可快速瀏覽下面的內容。

7.8.1 common Role

common Role的內容很簡單,僅僅只是添加yum源。

所以,只需一個common/tasks/main.yml任務文件即可,其它全刪掉。

$ rm -rf lnmp/roles/common/{defaults,vars,files,templates,handlers,tests,meta,README.md}

文件common/tasks/main.yml的內容:

---
# add yum repos
- name: backup origin yum repos
  shell: 
    cmd: "mkdir bak; mv *.repo bak"
    chdir: /etc/yum.repos.d
    creates: /etc/yum.repos.d/bak

- name: add os,epel,nginx,remi,remi-save,mysql repos
  yum_repository: 
    name: "{{item.name}}"
    description: "{{item.name}} repo"
    baseurl: "{{item.baseurl}}"
    file: "{{item.name}}"
    enabled: 1
    gpgcheck: 0
    reposdir: /etc/yum.repos.d
  loop:
    - name: os
      baseurl: "https://mirrors.tuna.tsinghua.edu.cn/centos/$releasever/os/$basearch"
    - name: epel
      baseurl: "https://mirrors.tuna.tsinghua.edu.cn/epel/$releasever/$basearch"
    - name: nginx
      baseurl: "http://nginx.org/packages/centos/$releasever/$basearch/"
    - name: remi
      baseurl: "https://mirrors.tuna.tsinghua.edu.cn/remi/enterprise/$releasever/php74/$basearch/"
    - name: remisafe
      baseurl: "https://mirrors.tuna.tsinghua.edu.cn/remi/enterprise/$releasever/safe/$basearch/"
    - name: mysql57
      baseurl: "http://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el$releasever/"

7.8.2 Nginx Role

nginx Role只有安裝、提供主配置文件、虛擬主機配置文件的任務,提供index.html測試頁面並測試的任務並沒有包含在此。如果需要,可在測試時提供。

把用不上的目錄刪掉:

$ rm -rf lnmp/roles/nginx/{defaults,tests,files,meta,README.md}

文件lnmp/roles/nginx/tasks/main.yml內容:

---
- name: install and config nginx
  import_tasks: nginx_install_config.yml

文件lnmp/roles/nginx/tasks/nginx_install_config.yml內容:

---
- name: install nginx
  yum: 
    name: nginx
    state: present

- name: render and copy nginx config
  template: 
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    backup: true
    validate: "/usr/sbin/nginx -t -c %s"
  notify: "reload nginx"

- name: nginx vhost config
  include_tasks: nginx_vhost_config.yml
  loop: "{{vhosts|dict2items}}"
  loop_control: 
    extended: yes

文件lnmp/roles/nginx/tasks/nginx_vhost_config.yml內容:

---
- name: remove nginx default vhost file
  file: 
    name: /etc/nginx/conf.d/default.conf
    state: absent
  when: ansible_loop.first

- name: render and copy vhosts config
  template: 
    src: vhost.conf.j2
    dest: "/etc/nginx/conf.d/{{item.key}}.conf"
  notify: "reload nginx"

- name: create dir on nginx
  file: 
    name: "/usr/share/nginx/html/{{item.key}}"
    state: directory

- name: create dir on php
  file: 
    name: "/usr/share/www/{{item.key}}/php"
    state: directory
  delegate_to: "{{groups.php[0]}}"

lnmp/roles/nginx/handlers/main.yml文件內容:

---
- name: reload nginx
  service: 
    name: nginx
    state: reloaded
    enabled: true

lnmp/roles/nginx/templates/nginx.conf.j2文件內容:

user nginx;
worker_processes {{ worker_processes }};
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

worker_rlimit_nofile {{worker_rlimit_nofile}};

events {
  worker_connections {{ worker_connections }};
  multi_accept {{ multi_accept }};
}

http {
  sendfile {{ send_file }};
  tcp_nopush {{ tcp_nopush }};
  tcp_nodelay {{ tcp_nodelay }};

  keepalive_timeout {{ keepalive_timeout }};
  server_tokens {{ server_tokens }};
  include /etc/nginx/mime.types;

  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log warn;

  gzip {{ gzip }};
  gzip_types {{ gzip_types | join(' ') if gzip_types | length > 0 else 'text/plain'}};
  gzip_min_length {{gzip_min_length}};
  gzip_disable "msie6";

  include /etc/nginx/conf.d/*.conf;
}

lnmp/roles/nginx/templates/vhost.conf.j2文件內容:

server {
    listen       {{item.value.listen}};
    server_name  {{item.value.server_name}};

    location / {
        root   /usr/share/nginx/html/{{item.key}};
        index  index.html index.htm;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    location ~ \.php$ {
        fastcgi_pass   {{item.value.fastcgi_pass}};
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME /usr/share/www/{{item.key}}/php$fastcgi_script_name;
        include        fastcgi_params;
    }
}

lnmp/roles/nginx/vars/main.yml文件內容:

---
worker_processes: 1
worker_rlimit_nofile: 65535
worker_connections: 10240
multi_accept: "on"
send_file: "on"
tcp_nopush: "on"
tcp_nodelay: "on"
keepalive_timeout: 65
server_tokens: "off"
gzip: "on"
gzip_min_length: 1024
# 1. You should assign a list to gzip_types,if you don't
#    want to use this variable, set it to an empty list,
#    such as "gzip_types: []".
# 2. There is no need to add "text/html" type for gzip_types,
#    gzip will alway include "text/html".
gzip_types:
  - text/plain
  - text/css
  - text/javascript
  - application/x-javascript
  - application/xml
  - image/jpeg
  - image/jpg
  - image/gif
  - image/png

vhosts:
  server1:
    server_name: www.abc.com
    listen: 80
    fastcgi_pass: "{{groups.php[0]}}:9000"
  server2: 
    server_name: www.def.com
    listen: 80
    fastcgi_pass: "{{groups.php[0]}}:9000"

有一點需要說明,上面變量文件中的9000端口本是PHP Role中定義的變量,這裏將端口號寫死了並不友好。

像這種跨Role引用的變量,也即多個Role共用的變量,可以將它們定義在inventory文件的all主機組中,也可也定義在playbook同目錄層次的group_vars目錄下的all.yml文件中。此處簡單做個演示,在後面介紹變量的文章裏會再做解釋。

例如:

$ tree -L 2 -F lnmp
lnmp
├── group_vars/
│   └── all.yml
├── inventory_lnmp
├── lnmp.yml
└── roles/
    ├── common/
    ├── mysql/
    ├── nginx/
    └── php/

$ cat lnmp/group_vars/all.yml
phpfpm_port: 9000

於是上面nginx變量文件中就可以引用這個共用變量的值,例如:

vhosts:
  server1:
    server_name: www.abc.com
    listen: 80
    fastcgi_pass: "{{groups.php[0]}}:{{phpfpm_port}}"
  server2: 
    server_name: www.def.com
    listen: 80
    fastcgi_pass: "{{groups.php[0]}}:{{phpfpm_port}}"

7.8.3 PHP Role

PHP Role比較簡單,只需一個任務文件加一個變量文件即可。

$ rm -rf lnmp/roles/php/{defaults,files,templates,meta,handlers,tests,README.md}

lnmp/roles/php/tasks/main.yml文件內容:

---
- name: install php and php-fpm
  yum: 
    name: "{{item}}"
    state: installed
  loop:
    - php
    - php-fpm

- name: change php-fpm listen address and port
  shell: |
    sed -ri 's/^(listen *= *)127.0.0.1.*$/\1{{phpfpm_addr}}:{{phpfpm_port}}/' /etc/php-fpm.d/www.conf
    sed -ri 's/^(listen.allowed_clients.*)$/;\1/' /etc/php-fpm.d/www.conf

- name: restart php-fpm
  service:
    name: php-fpm
    state: reloaded

lnmp/roles/php/vars/main.yml文件內容:

phpfpm_addr: 0.0.0.0
phpfpm_port: 9000
# phpfpm_port: "{{phpfpm_port}}" # 這是錯誤的

注意,這裏的phpfpm_port變量不可以引用共用變量的值,因爲變量重名了。要想引用共用變量,要麼這裏的變量改名,要麼加一個變量層次,要麼使用inventory變量訪問方式訪問group_vars中的變量。所以下面三種方式都可以:

phpfpm_addr: 0.0.0.0
phpfpm_port1: "{{phpfpm_port}}"

phpfpm_addr: 0.0.0.0
phpfpm_port: "{{hostvars[groups.php[0]].phpfpm_port}}"

phpfpm:
  phpfpm_addr: 0.0.0.0
  phpfpm_port: "{{phpfpm_port}}"

7.8.4 MySQL Role

MySQL的任務比較多,也比較零散,所以將它們分開單獨定義。

把用不上的目錄刪掉:

$ rm -rf lnmp/roles/mysql/{defaults,tests,files,meta,README.md}

lnmp/roles/mysql/tasks/main.yml文件內容如下:

---
- import_tasks: install_mysql.yml
- import_tasks: mysql_cnf.yml
- import_tasks: modify_mysql_temp_passwd.yml
  when: not temporary_passwd_updated
- import_tasks: ansible_mysql_connection_info.yml
- import_tasks: do_anything_in_mysql.yml

lnmp/roles/mysql/tasks/install_mysql.yml文件內容如下:

---
- name: install mysql
  yum: 
    name: "{{item}}"
    state: installed
  loop: 
    - mysql-community-server
    - mysql-community-client
    - python2-PyMySQL
    - MySQL-python

lnmp/roles/mysql/tasks/mysql_cnf.yml文件內容如下:

---
- name: render mysql config
  template:
    src: mysql.cnf.j2
    dest: /etc/my.cnf
  notify: restart mysql

- name: create mysql datadir
  file: 
    name: "{{mysqld.datadir}}"
    state: directory
    owner: mysql
    group: mysql

# 提供了MySQL配置文件後就要啓動MySQL服務,以便後面的任務連接MySQL
# 可以編寫啓動服務的任務,也可以直接flush handlers來啓動MySQL服務
- meta: flush_handlers

lnmp/roles/mysql/tasks/modify_mysql_temp_passwd.yml文件內容如下:

---
- name: get initialize temp password
  shell: |
    sed -rn '/temp.*pass/s/^.*root@localhost: (.*)$/\1/p' {{mysqld.log_error}}
  register: tmp_passwd

- name: modify root@localhost password before any op
  shell: |
    mysql -uroot -p'{{tmp_passwd.stdout}}' \
    --connect-expired-password \
    -NBe \
    'ALTER USER "root"@"localhost" identified by "{{mysql_passwd}}";'

- name: update variable temporary_passwd_updated to TRUE
  lineinfile:
    path: "{{role_path}}/vars/main.yml"
    line: 'temporary_passwd_updated: true'
    regexp: '^temporary_passwd_updated:.*'
  delegate_to: localhost

修改臨時密碼的任務是隻執行一次的任務,上面採用的方法是前文提到過的提供狀態變量,然後判斷它。

注意上面使用了一個Ansible的預定義特殊變量role_path,它表示的是當前Role的路徑,這對於修改mysql Role的變量文件來說正合適。

lnmp/roles/mysql/tasks/ansible_mysql_connection_info.yml文件內容如下:

---
- name: set mysql connection info for ansible
  template:
    src: .my.cnf.j2
    dest: /root/.my.cnf

lnmp/roles/mysql/tasks/do_anything_in_mysql.yml文件內容如下:

---
# 移除localhost的匿名用戶
- name: remove anonymous user for localhost
  mysql_user:
    name: ""
    host: localhost
    state: absent

# 移除所有匿名用戶
- name: remove all anonymous user
  mysql_user:
    name: ""
    host_all: true
    state: absent

# 創建用戶並指定權限
- name: create user and grant privileges
  mysql_user: 
    name: "junmajinlong"
    host: "192.168.200.%"
    password: 'P@ssword2!'
    priv: '*.*:ALL'
    state: present
    login_user: root
    login_password: 'P@ssword1!'

# 創建數據庫
- name: Create new databases with names 'foo' and 'bar'
  mysql_db:
    name: test
    state: present

# 創建多個數據庫
- name: Create new databases with names 'foo' and 'bar'
  mysql_db:
    name:
      - "foo"
      - "bar"
    state: present

# 刪除test數據庫
- name: drop test database
  mysql_db: 
    name: test
    state: absent

# 刪除多個數據庫
- name: drop databases with names 'foo' and 'bar'
  mysql_db:
    name:
      - "foo"
      - "bar"
    state: absent

lnmp/roles/mysql/templates/mysql.cnf.j2內容如下:

[client]
socket = {{client.socket}}

[mysqldump]
max_allowed_packet = {{mysqldump.max_allowed_packet}}

[mysqld]
port = {{mysqld.port}}
datadir = {{mysqld.datadir}}
socket = {{mysqld.socket}}
server_id = {{mysqld.server_id}}
log-bin = {{mysqld.log_bin}}
sync_binlog = {{mysqld.sync_binlog}}
binlog_format = {{mysqld.binlog_format}}
character-set-server = {{mysqld.character_set_server}}
skip_name_resolve = {{mysqld.skip_name_resolve}}
pid-file = {{mysqld.pid_file}}
log-error = {{mysqld.log_error}}

lnmp/roles/mysql/templates/.my.cnf.j2內容如下:

[client]
user = root
password = {{mysql_passwd}}
socket = {{mysqld.socket}}

lnmp/roles/mysql/handlers/main.yml內容如下:

- name: restart mysql
  service:
    name: mysqld
    state: restarted

lnmp/roles/mysql/vars/main.yml內容如下:

temporary_passwd_updated: false
mysql_passwd: "P@ssword1!"
client: 
  socket: "/data/mysql.sock"
mysqldump: 
  max_allowed_packet: "32M"
mysqld: 
  port: 3306
  datadir: "/data"
  socket: "/data/mysql.sock"
  server_id: 100
  log_bin: "mysql-bin"
  sync_binlog: 1
  binlog_format: "row"
  character_set_server: "utf8mb4"
  skip_name_resolve: 1
  pid_file: "/data/mysql.pid"
  log_error: "/data/error.log"

7.9 爲每個Role提供一個playbook

有時候看別人寫的Role,可能會發現爲每個Role提供了一個playbook。

比如nginx Role有一個nginx.yml playbook,php Role有一個php.yml,最後還有一個操作所有Role的playbook(本文lnmp Role示例便是這種方式)。如下:

$ tree -L 2 -F lnmp 
lnmp
├── common.yml   # 單獨操作common Role的playbook
├── group_vars/
│   └── all.yml
├── inventory_lnmp
├── lnmp.yml     # 彙總了所有Role的playbook
├── mysql.yml    # 單獨操作MySQL Role的playbook
├── nginx.yml    # 單獨操作nginx Role的playbook
├── php.yml      # 單獨操作php   Role的playbook
└── roles/
    ├── common/
    ├── mysql/
    ├── nginx/
    └── php/

之所以要爲每個Role都單獨提供一個playbook,僅僅只是希望在某些情況下能單獨操作那個Role,這樣就不用每次都執行所有Role。比如寫完common Role的時候,爲了測試,會執行一下這個Role以保證這個Role中沒有錯誤,但既然它已經執行過了,下次寫完nginx Role就沒必要再執行common Role。

另一方面,爲每個Role提供單獨的playbook,還能將這些Role解耦。

比如在這個LNMP的示例中,nginx Role、php Role和MySQL Role分別在不同的節點上執行,而且它們沒有任何依賴和關聯關係,如果將它們彙集在同一個playbook中執行,效率會非常低。比如Ansible控制nginx節點執行nginx Role任務的時候,php節點和MySQL節點都空閒着。特別是這幾個Role中都會yum安裝軟件包,而剛初始化的系統或更新了repo文件時,安裝軟件包時都會先更新yum的metadata,這個過程體現在執行yum安裝包任務時,速度會非常非常慢。

很多人(也包括我)都覺得yum模塊比yum命令更慢,而且慢的多。所以如果可以的話,將你的playbook中所有的yum模塊替換成shell模塊執行yum命令吧。

因此,非常有必要優化這種低效。如果將所有Role的任務集中在同一個playbook中,將沒有辦法優化,Ansible只提供了任務級別的並行執行能力,沒有提供play級別和Role級別的並行。但如果爲每個Role提供一個playbook,就可以在Shell層次下實現並行,比如寫一個簡單的Shell腳本:

#!/bin/bash

# common不能放在後臺,因爲其它Role依賴common Role
ansible-playbook -i inventory_lnmp common.yml
ansible-playbook -i inventory_lnmp nginx.yml &
ansible-playbook -i inventory_lnmp php.yml &
ansible-playbook -i inventory_lnmp mysql.yml &

關於優化Ansible效率的話題,後面我會專門寫一篇文章。但希望大家記住,這裏提供的Shell層次的並行也是一種優化方式,而且很香,但很多人都忽略了這種優化方式。

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