Playbook

在上一节中,我们详细介绍了Ansible提供的一些常用模块。可以看到,Ansible中的每个模块专注于某一方面的功能。虽然每个模块实现的功能都比较简单,但是,将各个模块组合起来就可以实现比较复杂的功能。在Ansible中,将各个模块组合起来的文件是一个YAML格式的配置文件。这个配置文件,在Ansible中称为Playbook。

在这一节中,我们将循序渐进地介绍Ansible中的Playbook,我们将首先介绍Playbook的定义,然后介绍如何使用Playbook完成远程服务器部署,之后详细介绍Playbook的基本语法,使用Playbook的基本讲法就能够完成大部分的部署任务。

在这一节中,找们将介绍如何使用Playbook的基本语法完成nginx与MongoDB的部署,最后,我们介绍了部分Playbook的高级语法。

1、Playbook的定义

Playbook不同于其他使用单个模块操作远程服务器,Playbook的功能更加强大。如果只使用Playbook的基本功能,那么,Playbook是一个非常简单的配、管理和部署系统。此外,Playbook也可以实现各种高级功能,如指定任务的执行顺序,委派其他主机来执行某一个任务,与监控服务器和负载均衡组件进行交互等。

有一个非常恰当的比喻,,Ansible中的模块类似于Linux下的命令,Ansible中的Playbook类似于Linux下的Shell脚本文件。Shell脚本文件将各个Linux命令组合起来,以此实现复杂的功能,Playbook将各个模块组合起来也可以实现复杂的部署功能。在shell脚本中,除了调用Linux命令以外,还有一些基本的语法,如变量定义、if语句、for循环等。在Playbook中,一方面通过YAML格式进行定义提高Playbook的可读性、可维护性,降低工程师的学习负担;另一方面,Ansible提供了若干可以应用在Playbook中的选项,以便工程师实现更加高级的功能。

一个Playbook可以包含一到多个Play,每一个Play是一个完整的部署任务。在Play中,我们需要指定对哪些远程服务器执行操作,以及对这些远程服务器执行哪些操作。

下面是一个名为first_playbook.yml的Playbook。在这个Playbook中,我们定义了两个Play,前者用来在数据库服务器上部署MongoDB,后者用来在web服务器上部署“应用”。这里只是为了对Playbook进行演示,并没有真的部署应用。

[root@python ~]# vim first_playbook.yml

---
- hosts: dbservers
  become: yes
  become_method: sudo
  tasks:
  - name: install mongodb
    yum: name=mongodb-server state=present

- hosts: webservers
  tasks:
  - name: copy file
    copy: src=/tmp/data.txt dest=/tmp/data.txt

  - name: change mode
    file: dest=/tmp/data.txt mode=655 owner=root group=root

这个Playbook中包含了两个Play。一个Playbook可以包含一到多个Play,所以即使Playbook中值包含一个Play,也需要使用列表的形式进行定义。在YAML语法中,“- hosts”前面的“-”表示定义列表。

在Ansible中,一个Play必须包含以下两项:

1. hosts:需要对哪些远程服务器执行操作
2. tasks:需要在这些服务器上执行的任务列表

例如,对web服务器进行部署时,我们仅仅使用了hosts和tasks两个选项。前者表示对哪些服务器执行操作,后者表示对服务器执行哪些操作。在部署数据库服务器时需要安装软件,因此使用了become与become_method两个选项,用来表示使用管理员的身份去安装MongoDB数据库。

一个Play可以包含一到多个task,因此task也必须以YAML的列表形式进行定义。可以看到,在这个例子中,对数据库服务器进行操作时仅包含了一个task,对web服务器进行部署时包含了两个task。

在Ansible中,task有两种定义形式:

1. action:module options
2. module:options

前一种形式是Ansible的旧版本语法,第2种形式是新版本的语法,直接使用模块的名称作为键,使用模块的参数作为值。如下所示:

- name: install httpd
  yum: name=httpd update_cache=yes state=present

在安装Apache的例子中,“name=httpd update_cache=yes state=present”是一个完整的字符串,而不是一个字典。只是字符串的值是一个“key=value”形式的参数。

在参数较多时,为了增加Playbook的可读性,我们也可以像下面这样定义一个task:

- name: install httpd
  yum: >
    name=httpd
    update_cache=yes
    state=present

在Ansible中,当参数较长时,除了使用“>”进行折叠换行以外,也可以使用缩进字块的形式:

- name: install httpd
  yum: 
    name: httpd
    update_cache: yes
    state: present

虽然从字面来看,这两种指定参数的方式相差不大。但是,从YAML的语法来说,这是完全不同的两个方法。前者是一个比较长的字符串,后者是一个字典。

task的定义中,name是可选的。所以,像下面这样定义task也是完全合法的:

- yum: name=httpd update_cache=yes state=present

name的作用在于,执行Playbook时作为注释进行显示,以便使用者知道当前执行到哪一步。因此,在定义task时,一般都会定义name字段。

在实际工作中,虽然一个Playbook可以包含多个Play,但是为了Playbook的可读性和可维护性,我们一般只会在Playbook中编写一个Play。例如,对于这里的例子,我们可以将first_playbook.yml这个Playbook拆分成两个Playbook,分别名为db.yml与web.yml。其中,db.yml文件包含了与数据库服务器相关的部署任务,web.yml文件包含了与web服务器相关的部署任务。

当我们需要部署数据库服务器和web服务器时,可以先执行db.yml文件,再执行web.yml文件。除此之外,Ansible还提供了一种便捷方式来处理这种情况。例如,我们可以编写一个名为all.yml的Playbook,它的内容如下:

---
- include: db.yml
- include: web.yml

include选项是Ansible提供的,用于在一个Playbook中导入其他Playbook。在Ansible中,只需要使用include选项导入其他Playbook文件,执行这个Playbook时,被导入的Playbook便会依次执行。

上面详细介绍了Ansible的Playbook定义,这个Playbook定义虽然比较简单,但是,是一个比较完整的Playbook例子。在实际工作中使用的Playbook也不会比这个Playbook复杂很多。

我们接下来将介绍如何使用ansible-playbook命令执行Playbook,然后再介绍Playbook的其他语法。

2、ansible拆分playbook.yml

查看一下所需文件是否正确

[root@python ~]# cat hosts 
127.0.0.1
[webservers]
192.168.1.60
[dbservers]
192.168.1.80
[common:children]
dbservers
webservers
[root@python ~]# cat /etc/ansible/ansible.cfg 
[defaults]
remote_user = root
remote_port = 22
inventory = /root/hosts

拆分playbook.yml

[root@python ~]# cat db.yml 
---
- hosts: dbservers
  become: yes
  become_method: sudo
  tasks:
  - name: install mongodb
    yum: name=mongodb-server state=present  #mongodb-server 可欢成其他服务如(git)
[root@python ~]# cat web.yml 
---
- hosts: webservers
  tasks:
  - name: copy file
    copy: src=/tmp/data.txt dest=/opt/data.txt 
  - name: change mode
    file: dest=/opt/data.txt mode=655 owner=root group=root
[root@python ~]# cat all.yml 
---
- include: db.yml
- include: web.yml
[root@python ~]# touch /tmp/data.txt
[root@python ~]# touch /opt/data.txt

3、使用Ansible-playbook执行Playbook

上一小节中,我们简单地介绍了Playbook的定义。那么,当我们有了一个Playbook文件以后,如何执行这个文件完成应用部署呢?我们知道,Ansible安装完成以后存在多个可执行的命令行工具,其中,ansible-playbook便是用于执行Playbook的命令行工具。

ansible-playbook的执行方式如下:

ansible-playbook first_playbook.yml

ansible-playbook命令也有若干命令行选项,其中,有部分选项与ansible命令相同。Ansible中也存在一些ansible-playbook特有的命令行选项。

ansible-playbook命令与ansible命令相同的命令行选项:

-T --timeout:建立SSH连接的超时时间
--key-file --private-key:建立SSH连接的私钥文件
-i --inventory-file:指定Inventory文件,默认是/etc/ansible/hosts
-f --forks:并发执行的进程数,默认为5
--list-hosts:playbooks匹配的服务器列表。

ansible-playbook也有一个名为--list-hosts的选项,该选项的作用是列出匹配的服务器列表。例如,在我们这个 Playbook的例子中,hosts文件的内容如下:

127.0.0.1
[webservers]
192.168.1.60
[dbservers]
192.168.1.80
[common:children]
dbservers
webservers

我们知道,Ansible中的Play定义了需要对哪些服务器执行哪些操作,也就是说,每一个Play都可以指定匹配的远程服务器。在我们这个Playbook的例子中,对数据库服务器安装MongoDB,对web服务器部署“应用“。因此,ansible-playbook命令与ansible命令的--list-hosts选项输出的结果将会大不相同。ansible-playbook命令的--list-hosts选项输出的结果如下:

[root@python ~]# ansible-playbook all.yml --list-hosts

python中Ansible模块的Playbook理解

ansible-playbook命令有一些特有的选项,如下所示:

--list-tasks:列出任务列表
--step:每执行一个任务后停止,等待用户确认
--syntax-check:检查Playbook语法
-C --check:检查当前这个Playbook是否会修改远程服务器,相当于预测Playbook的执行结果。

这里的几个选项,除了--step以外,其他几个选项都不会执行Playbook中的任务。这些选项存在主要是为了便于调试Playbook。例如,--list-tasks选项,该选项用来显示当前Playbook中的任务列表。当Playbook比较大时,可以通过这个方式快速查看任务列表。如下所示:

[root@python ~]#  ansible-playbook  all.yml --list-tasks
playbook: all.yml

  play #1 (dbservers): dbservers    TAGS: []
    tasks:
      install mongodb   TAGS: []

  play #2 (webservers): webservers  TAGS: []
    tasks:
      copy file TAGS: []
      change mode   TAGS: []

当我们查看任务列表时,任务的名称就是task的name字段。因此,name的定义需要具有较好的描述性,让使用者通过名字就能知道该任务需要做什么事情。

--step选项类似于编程语言中的单步调试。当我们使--step选项执行Playbook时,ansible-playbook在每一个任务之前都会停住,等侍用户输入yes,、no或continue。如下所示:

[root@python ~]# ansible-playbook all.yml --step
[DEPRECATION WARNING]: 'include' for playbook includes. You should use 
'import_playbook' instead. This feature will be removed in version 2.12. 
Deprecation warnings can be disabled by setting deprecation_warnings=False in 
ansible.cfg.

PLAY [dbservers] ***************************************************************
Perform task: TASK: Gathering Facts (N)o/(y)es/(c)ontinue: y

Perform task: TASK: Gathering Facts (N)o/(y)es/(c)ontinue: *********************

TASK [Gathering Facts] *********************************************************
ok: [192.168.1.80]
Perform task: TASK: install mongodb (N)o/(y)es/(c)ontinue: y

Perform task: TASK: install mongodb (N)o/(y)es/(c)ontinue: *********************

TASK [install mongodb] *********************************************************
changed: [192.168.1.80]

PLAY [webservers] **************************************************************
Perform task: TASK: Gathering Facts (N)o/(y)es/(c)ontinue: y

Perform task: TASK: Gathering Facts (N)o/(y)es/(c)ontinue: *********************

TASK [Gathering Facts] *********************************************************
ok: [192.168.1.60]
Perform task: TASK: copy file (N)o/(y)es/(c)ontinue: y

Perform task: TASK: copy file (N)o/(y)es/(c)ontinue: ***************************

TASK [copy file] ***************************************************************
changed: [192.168.1.60]
Perform task: TASK: change mode (N)o/(y)es/(c)ontinue: y

Perform task: TASK: change mode (N)o/(y)es/(c)ontinue: *************************

TASK [change mode] *************************************************************
changed: [192.168.1.60]

PLAY RECAP *********************************************************************
192.168.1.60               : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.1.80               : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

输入yes以后,任务将会继续执行,并在下一个任务时停止,等待用户继续输入。当我们输入continue时,Ansible会执行完当前这个Play,当执行到下一个Play时再停止,并等待用户输入。

二、Playbook的详细语法

到目前为止,我们已经学习了如何编写Playbook以及如何运行Playbook。但是,我们仅仅介绍了最简单的Playbook。在这一节中,我们将会介绍Playbook是如何通过不同的选项提供丰富多样的功能。灵活使用这些选项,能够编写出形式各异的Playbook,以此应对自动部署中的各种情况。

在定义Play时,只有hosts与tasks是必选选项,其他选项都是根据需要添加的。在这一小节中。我们将介绍Playbook提供的不同功能,以Playbook的功能为线索,介绍Play与task中可以使用的选项。

(1)权限

在Ansible中,默认使用当前用户连接远程服务器执行操作。我们也可以在anaible.cfg文件中配置连接远程服务器的默认用户。此外,如果是不同的用户使用不同类型的远程服务器,那么也可以在Playbook的Play定义中指定连接远程服务器的用户。例如,我们可以指定执行Play的用户:

---
- hosts: webservers
  remote_user: root

用户可以细分每一个task,如下所示:

---
- hosts: werbservers
  remote_user: root
  tasks:
    - name: test connection
      ping:
        remote_user: yourname

很多时候,我们需要的不是以某个特定用户连接远程服务器,而是在需要更高级别的权限时,使用管理员身份去执行操作。在ansible中,可以通过become与become_ method选项实现:

---
- hosts: werbservers
  remote_user: root
  become: yes

与remote_user选项类似,我们也可以为单个任务使用管理员权限,如下所示:

---
- hosts: werbservers
  remote_user: yourname
  tasks:
    - name: installed nginx
      service: name=nginx state=started
      become: yes
      become_method: sudo

实例

先修改远程服务器中test的权限为(0:0)

[root@192 ~]# vim /etc/passwd
test:x:0:0::/home/test:/bin/bash
[root@python ~]# chown test:root /etc/ansible/*
[root@python ~]# su test
[test@python root]$ cd 
[test@python ~]$ vim hosts
[db]
192.168.1.60

[test@python ~]$ vim db.yml

---
- hosts: db
  remote_user: root             #远程服务器登陆的用户
  tasks:
    - name: installed nginx
      become: yes
      become_method: sudo
      ping:
        remote_user: root         #远程服务器登陆的用户
[test@python ~]$ vim /etc/ansible/ansible.cfg 

[defaults]
inventory = /home/test/hosts

执行一下

[test@python ~]$ sudo ansible-playbook db.yml --step

We trust you have received the usual lecture from the local System
Administrator. It usually boils down to these three things:

    #1) Respect the privacy of others.
    #2) Think before you type.
    #3) With great power comes great responsibility.

[sudo] password for test: 

PLAY [db] **********************************************************************
Perform task: TASK: Gathering Facts (N)o/(y)es/(c)ontinue: y

Perform task: TASK: Gathering Facts (N)o/(y)es/(c)ontinue: *********************

TASK [Gathering Facts] *********************************************************
Enter passphrase for key '/root/.ssh/id_rsa': 
ok: [192.168.1.60]
Perform task: TASK: installed nginx (N)o/(y)es/(c)ontinue: 

Perform task: TASK: installed nginx (N)o/(y)es/(c)ontinue: *********************

PLAY RECAP *********************************************************************
192.168.1.60               : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

(2)通知

在Ansible中,模块是幂等的。例如,我们要在远程服务器上创建一个用户,如果该用户已经存在,那么Ansible不会将该用户删除以后重新创建,而是直接返回成功,并通过changed字段表示是否对远程服务器进行了修改。

考虑这样一种需求:我们要通过Ansible修改Apache的配置文件,并重启Apache服务,使得新的配置文件生效。由于Ansible的模块是幂等的,当我们修改Apache的配置文件时,如果配置文件的内容已经与我们想要修改成的内容一样(例如,不小心将Ansible执行了两次的情况),那么,Ansible就什么也不做。既然Apache的配置文件并没有真的被修改,那么我们也不应该去重启Apache的服务器。在Ansible中,通过notify与handler机制来实现这里的功能。

在下面的例子中,我们首先尝试安装Apache,然后修改Apache的配置文件。如果配置文件被修改,则通过notify选项通知handler进行后续处理。

handler是Ansible提供的条件机制,与tasks比较类似,都是去执行某些操作。但是,handler只有在被notify触发以后才会执行,如果没有被触发则不会执行。在Playbook中,如果task后面存在notify选项,那么,当Ansible识别到task改变了系统的状态,就会通过notify去触发handler。

Ansibie是通过什么条件判断notify触发的是哪一个handler呢?很简单,在Ansible中,task使用handler的名字作为参数,以此来触发特定的handler。例如,在我们这里的例子中,notify触发的是“restart apache"这个handler, handlers中也存在一个名为" restart apache“的handler。

---
- hosts: webservers
  tasks:
  - name: ensure apache is at the latest version
    yum: name=httpd state=latest

  - name: write the apache config file
    template: src=/srv/httpd.j2 dest=/etc/httpd.conf
    notify:
    - restart apache

  - name: ensure apache is running
    service: name=httpd state=started

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

需要注意的是,handler只会在所有task执行完后执行。并且,即便一个handler被触发多次,它也只会执行一次。handler并不是在被触发时立即执行,而是按照Play中定义的顺序执行。一般情况下,handler都位于Play的最后,即在所有任务执行完成以后再执行。

Ansible官方文档提到handler的唯一用途,就是重启服务与服务器,正如找们这个例子所演示的。

在这个例子中,我们还用到T了template模块。template模块用以渲染Jinja模板。

(3)变量

在Inventory管理章节,我们已经介绍了如何定义变量。在Ansible中,还有其他几种定义变量的方式。对于简单的Playbook,最直接的方式是将变量定义在Playbook的vars选项中。如下所示:

- hosts: dbservers
  vars:
    mysql_port: 3307

在Playbook中定义变量,可以在模板渲染时使用。例如:Ansible官方给出的例子中,MySQL配置文件的部分模板如下:

[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
user=mysql
port={{ mysql_port }}

当变量较少的时候,定义在vars选项中完全没有问题。当变量较多时,可以将变量保存在一个独立的文件中,并通过vars_files选项引用该文件。如下所示:

---
- hosts: all
  vars:
    favcolor: blue
  vars_files:
    - /vars/external_vars.yml

  tasks:
  - name: this is just a placeholer
    command: /bin/echo foo

保存变量的文件是一个简单的YAML格式的字典,如下所示:

---
# in th above example, this would be vars/external_vars.yml
somevar: somevalue
password: magic

在shell脚本中,我们可以通过获取上一条命令的返回码判断命令是否执行成功。在Ansible中,我们也可以获取任务的执行结果,将任务的执行结果保存在一个变最中,并在之后引用这个变量。这样的变量在Ansible中使用register选项获取,也称为注册变量。

例如,在下面这个例子中,我们首先执行/usr/bin/foo命令,并通过register选项获取命令的执行结果,将结果保存在foo_result中。在之后的task中,使用这个变量名引用/usr/bin/foo命令的执行结果。

- hosts: web_servers
  tasks:
  - shell: /usr/bin/foo
    register: foo_result
    ignore_errors: True

  - shell: /usr/bin/bar
    when: foo_result == 5

这个例子还涉及了两个新的选项,分别是ignore_errors与when。前者表示忽略当前task中的错误,后者是一个条件语句,只有条件为真时才会执行这个task。

(4)Facts变量

在Ansible中,还有一些特殊的变量,这些变量不需要我们进行任何设置就可以直接使用,这样的变量称为Facts变量。Facts变量是Ansible执行远程部署之前从远程服务器中获取的系统信息,包括服务器的名称、IP地址、操作系统、分区信息、硬件信息等。Facts变量可以配合Playbook实现更加个性化的功能需求。例如,将MongoDB数据库的数据保存在/var/mongo-\<hostname>/目录下。

我们可以通过setup模块查看Facts变量的列表,如下所示:

ansible all -m setup

有了Facts变量以后,如何在Ansible中使用它们呢?答案是直接使用。我们可以在Playbook中直接通过变量的名字引用变量,也可以在Jinja2模板中通过变量的名字引用变量。下面是一个名为test_facts.yml的Playbook。在这个Playbook中,我们输出了操作系统的类型,并且只有在操作系统为“RedHat"类操作系统时才会执行安装操作。

---
- hosts: dbservers
  tasks:
    - shell: echo {{ ansible_os_family }}
      register: myecho

    - debug: var=myecho.stdout_lines

    - name: install git on Red Hat Linux
      yum: name=git state=installed
      when: ansible_os_family == "RedHat"

setup模块为了输出结果的可读性,对模块的输出进行了归类和整理。因此,当我们要访问复杂变量的子属性时,需要使用嵌套结构。例如,我们可以通过下面两种方式访问Ansible中的ipv4地址:

ansible_ens33['ipv4']['address']
ansible_ens33.ipv4.address

访问复杂的变量的Playbook:

---
- hosts: dbservers
  gather_facts: yes
  tasks:
    - shell: echo {{ ansible_ens33['ipv4']['address'] }}
      register: myecho

    - debug: var=myecho.stdout_lines

    - shell: echo {{ ansible_ens33.ipv4.address }}
      register: myecho

    - debug: var=myecho.stdout_lines

在实际工作中,我们一般会在Jinja2模板中引用Facts变量。使用方式与这里的例子一样,为了节省篇幅就不再赘述了。

在Playbook中,可以通过gather_ facts选项控制是否收集远程服务器的信息。该选项默认取值为yes,如果确定不需要用到远程服务器的信息,可以将该选项设置为no,以此提高Ansible部署的效率。如下所示:

---
- hosts: dbservers
  gather_factes: no
  tasks:

(5)循环

- name: Install Mysql package
  yum: name={{ item }} state=installed
  with_items:
    - mysql-server
    - MySQL-python
    - libselinux-python
    - libsemanage-python

(6)条件

有时候,一个任务是否执行取决于一个变量的取值,或者上一个任务的执行结果,这个时候找们就需要条件语句。再或者说,在循环的时候想要跳过一些特定的元素,在服务器部署时只对某些特定的操作系统进行操作。所有这些行为都可以使用条件语句解决。Ansible的Playbook不是一门编程语言,因此没有相应的条件语句,不过Ansible提供了一个类似的选项。

在Playbook中可以通过when选项执行条件语句,when就类似于编程语言中的if语句。

下面是一个简单的when选项使用示例:

# 查看Linux系统版本:cat /etc/redhat-release
tasks:
  - name: "系统关机"
    command: /sbin/shutdown -t now
    when: ansible_os_family == "RedHat"    

when选项也支持多个条件语句,下面是一个YAML格式的多条件:

tasks:
  - name: "shutdown CentOS 7 systems"
    command: /sbin/shutdown -t now
    when:
      - ansible_distribution == "CentOS"
      - ansible_distribution_major_version == "7"

对于更复杂的条件可以使用and、or与括号进行定义:

tasks:
  - name: "shutdown CentOS 7 and Debian 6 systems"
    command: /sbin/shutdown -t now
    when: (ansible_distribution == "CentOS" and ansible_distribution_major_version == "7") or (ansible_distribution == "Debian" and ansible_distribution_major_version == "6")

在when选项中可以读取变量的取值,例如:

vars:
  epic: true

tasks:
  - shell: echo "This certainly is epic!"
    when: epic

when选项可以与循环一起使用,以实现过滤的功能:

tasks:
  - command: echo {{ item }}
    with_items: [0, 2, 4, 6, 8, 10]
    when: item > 5

(7)任务执行策略

在Ansible中,Playbook的执行是以task为单位进行的。Ansible默认使用5个进程对远程服务器执行任务。在默认情况的任务执行策略( linear)中,Ansible首先执行task1,并且等到所有服务器执行完task1以后再开始执行task2,以此类推。从Ansible 2.0开始,Ansible支持名为free的任务执行策略,允许执行较快的远程服务器提前完成Play的部署,不用等待其他远程服务器一起执行task。如下所示:

- hosts: all
  strategy: free
  tasks:
   ……

在这一节中,我们比较详细地介绍了Ansible中的Playbook选项。在Ansible中,Play与task都有很多选项,每个选项可以实现不同的功能。Ansibie官方并没有通过功能的形式介绍不同的选项给出一个完整的选项列表。我们也可以参考https://github.com/lorin/ansible-quickref快速了解Play与task中的选项,以及各个选项的含义。

4、案例:使用Playbook部署nginx

wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo
//下载源

在这个例子中,我们使用Ansible配置一台服务器运行nginx进程。部署nginx的Playbook如下:

---
- hosts: webservers
  become: yes
  become_method: sudo
  vars:
    worker_prosess: 4
    worker_connections: 768
    max_open_files: 65506
  tasks:
    - name: install nginx
      yum: name=nginx update_cache=yes state=present

    - name: copy nginx config file
      template: src=/root/test_ansible/nginx.conf.j2 dest=/etc/nginx/nginx.conf
      notify: restart nginx

    - name: copy index.html
      template:
        src: /root/test_ansible/index.html.j2
        dest: /usr/share/nginx/www/index.html
        mode: 0644
      notify: restart nginx
  handlers:
    - name: restart nginx
      service: name=nginx state=restarted

在这个Playbook中,我们首先通过hosts选项指定了要对哪些远程服务器执行操作。随后,我们通过become与become_method选项声明了部署时使用sudo权限。接下来,我们在vars字段中定义了三个变量,这三个变量将用在nginx的配置文件中。我们在tasks选项下定义了部署nginx服务的任务列表,包括软件安装、模板渲染、定制s首页和重启nginx进程。

为了避免配置文件在没有任何修改的情况下重启了nginx进程,这里使用了Ansible的handler机制。在这个Playbook中,存在两个notify选项,以及一个handler选项。无论是nginx的配置文件,还是定制首页发生了修改,我们都会重启nginx进程。由于我们使用了Ansible的handlers机制,因此,在没有任何修改的情况下,Ansible并不会重启nginx进程。使用handler机制还有一个好处,notify多次,handler也只会执行一次,避免了反复多次重启nginx进程。

在这个部署nginx服务的Playbook中,我们用到了nginx.conf.j2这个配置模板。这个模板使用的是Jinja2的语法,所以后缀名为j2。模板的内容如下:

[root@python ~]# mkdir test_ansible
[root@python ~]# vim /root/test_ansible/nginx.conf.j2
worker_processes  {{ worker_prosess }};
worker_rlimit_nofile {{ max_open_files }};

events {
    worker_connections  {{ worker_connections }};
}

http {
    server {
        listen       80;

        listen  443 ssl;

        server_name  localhost;

        location / {
            root   /usr/share/nginx/www;
            index  index.html index.htm;

            tr_files $uri $uri/ =404;
        }
    }
}

Ansible会使用我们在Playbook的vars字段中定义的变量,将Jinja2模板渲染成真实的配置文件。

我们的Playbook还用到了一个名为index.html.j2的模板,该模板用于渲染网站首页。index.html.j2的内容如下:

[root@python ~]# vim /root/test_ansible/index.html.j2
<html>
    <meta charset="utf-8">
    <head>
        <title>wellcome to ansible</title>
    </head>
    <body>
        <h1>nginx, configured by ansible</h1>
        <p>如果你能看到这个页面,说明ansible自动部署nginx成功了!</p>

        <p>{{ ansible_hostname }}<p>
    </body>
</html>

在index.html.j2中,我们用到了一个名为ansible_hostname的变量。这个变量是Facts变量,是Ansible在执行Playbook之前从远程服务器获取到的信息。因此,我们不需要定义,直接使用即可。

有了Playbook以后,使用ansible-playbook命令进行部署。如下所示:

[root@python ~]# pip install Jinja2
[root@python ~]# vim /etc/ansible/ansible.cfg 

[defaults]
inventory = /root/hosts

[root@bogon ~]# ansible-playbook nginx.yml

PLAY [webservers] **********************************************************************************************************

TASK [Gathering Facts] *****************************************************************************************************
ok: [127.0.0.1]

TASK [install nginx] *******************************************************************************************************
ok: [127.0.0.1]

TASK [copy nginx config file] **********************************************************************************************
ok: [127.0.0.1]

TASK [copy index.html] *****************************************************************************************************
ok: [127.0.0.1]

PLAY RECAP *****************************************************************************************************************
127.0.0.1                  : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

[root@bogon ~]# 

如果安装失败,可能是端口被占用,可以停止使用该端口的服务,或者更改nginx端口。

[root@bogon ~]# netstat -ntlp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN      1/systemd           
tcp        0      0 0.0.0.0:6000            0.0.0.0:*               LISTEN      7470/X              
tcp        0      0 192.168.122.1:53        0.0.0.0:*               LISTEN      7654/dnsmasq        
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      7337/sshd           
tcp        0      0 127.0.0.1:631           0.0.0.0:*               LISTEN      7340/cupsd          
tcp        0      0 127.0.0.1:6010          0.0.0.0:*               LISTEN      31653/sshd: root@pt 
tcp        0      0 127.0.0.1:6011          0.0.0.0:*               LISTEN      31653/sshd: root@pt 
tcp        0      0 127.0.0.1:6012          0.0.0.0:*               LISTEN      31653/sshd: root@pt 
tcp6       0      0 :::111                  :::*                    LISTEN      1/systemd           
tcp6       0      0 :::80                   :::*                    LISTEN      17867/httpd         
tcp6       0      0 :::6000                 :::*                    LISTEN      7470/X              
tcp6       0      0 :::22                   :::*                    LISTEN      7337/sshd           
tcp6       0      0 ::1:631                 :::*                    LISTEN      7340/cupsd          
tcp6       0      0 ::1:6010                :::*                    LISTEN      31653/sshd: root@pt 
tcp6       0      0 ::1:6011                :::*                    LISTEN      31653/sshd: root@pt 
tcp6       0      0 ::1:6012                :::*                    LISTEN      31653/sshd: root@pt 
[root@bogon ~]# systemctl stop httpd.service

第二台服务器启动一下nginx

[root@192 ~]# nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
[root@192 ~]# nginx
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] still could not bind()

浏览器访问一下

python中Ansible模块的Playbook理解