在4.6节LAMP实战中,我们就已经使用了Handlers来实现了重启Apache的功能,该实例中,一些修改Apache配置文件的操作使用notify:restart apache触发Handlers,从而实现了Apache的重启。
handlers:
- name: restart apache
service: name=apache2 state=restarted
tasks:
- name: 开启Apache rewrite模块
apache2_module: name=rewrite state=present
notify: restart apache
下面的例子中,实现了一个任务同时调用多个Handlers。
- name: Rebuild application configuration
command: /opt/app/rebuild.sh
notify:
- restart apache
- restart memcached
handlers:
- name: restart apache
service: name=apache2 state=restarted
notify: restart memcached
- name: restart memcached
service: name=memcached state=restarted
在使用Handlers的过程中,有以下几点需要格外注意。
在Ansible中设置和使用环境变量的方法多种多样。例如,如果我们想为连接远程主机的账号设置一些环境变量,我们可以使用lineinfile模块直接修改远程用户的~/.bash_profile文件,如下代码所示:
- name: 为远程主机上的用户指定环境变量
lineinfile: dest=~/.bash_profile regexp=^ENV_VAR= line=ENV_VAR=value
- name: 获取刚刚指定的环境变量,并将其保存到自定义变量foo中
shell: 'source ~/.bash_profile && echo $ENV_VAR'
register: foo
- name: 打印出环境变量
debug: msg="The variable is {{ foo.stdout }}"
Linux同样也使用文件/etc/environment来读取环境变量,所以我们也可以使用如下方法来指定远程主机上用户的环境变量。
- name: Add a global environment variable.
lineinfile: dest=/etc/environment regexp=^ENV_VAR= line=ENV_VAR=value
sudo: yes
对于某一个Play来说,我们可以使用environment选项来为其设置单独的环境变量。比如,我们现在需要为一个下载任务设置http代理。最简单的情况,我们可以这样实现:
- name: 使用指定的代理服务器下载文件
get_url: url=http://www.example.com/file.tar.gz dest=~/Downloads/
environment:
http_proxy: http://example-proxy:80/
vars:
var_proxy:
http_proxy: http://example-proxy:80/
https_proxy: https://example-proxy:443/
[etc...]
tasks:
- name: 使用指定的代理服务器下载文件
get_url: url=http://www.example.com/file.tar.gz dest=~/Downloads/
environment: var_proxy
vars:
proxy_state: present
task:
- name: Configure the proxy
lineinfile:
dest: /etc/environment
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
state: "{{ proxy_state }}"
with_items:
- { regexp: "^http_proxy=",line:"http_proxy=http://example-proxy:80/" }
- { regexp: "^http_proxy=",line:"https_proxy=https://example-proxy:443/" }
- { regexp: "^ftp_proxy=",line:"ftp_proxy=http://example-proxy:80/" }
我们可以使用如下命令来检测我们在远程主机设置的环境变量是否生效:
ansible test -m shell -a 'echo $TEST'
Ansible中变量的命名规则与其他语言或系统中变量的命名规则非常相似。在Ansible中,变量以英文小写字母开头,中间可以包含下划线(_)和数字。
在Inventory文件中,比如Ansible的Hosts文件,我们使用等号"="来为变量赋值,如
foo=bar
在Playbook和包含变量设置的配置文件中,我们使用冒号":"来为变量赋值,如:
foo:bar
Ansible中有多种不同的途径来定义变量。
比如在运行Playbook时,使用--extra-vars选项指定额外的变量。
ansible-playbook example.yml --extra-vars "foo=bar"
ansible-palybook example.yml --extra-vars "@even_more_vars.json"
ansible-playbook example.yml --extra-vars "@even_more_vars.yml"
在Playbook中,最常见的定义变量的方法是使用vars代码块。如Playbook内容如下:
---
- hosts: example
vars:
foo: bar
tasks:
# Prints "Variable 'foo' is set to bar".
- debug: msg="Variable 'foo' is set to {{ foo }}"
Playbook文件内容如下:
---
-hosts: example
vars_files:
- vars.yml
tasks:
- debug: msg="Variable 'foo' is set to {{ foo }}"
---
foo: bar
利用Ansible的内置环境变量(即使用setup模块可以查看到的变量),我们还可以实现变量配置文件的有条件导入。
我们来看以下的应用场景:现在生产环境中都有多台主机,分别安装了CentOS系统和Debian系统,同时我们为两套系统设置了两个变量定义文件:apache_CentOS.yml和apache_default.yml,里面同时定义了同一个变量apache,在apache_CentOS.yml文件中定义为apache:httpd,在apache_default.yml文件中定义为apache:apache2,这样我们就实现了同一个Playbook可以针对不同系统环境实施不同的操作的效果。Playbook内容如下:
---
- hosts: example
vars_files:
- [ "apache_{{ ansible_os_family }}.yml","apache_default.yml" ]
tasks:
- service: name={{ apache }} state=running
在Ansible中,Inventory文件通常是指Ansible的主机和组的定义文件Hosts(默认路径为/etc/ansible/hosts,简称Hosts文件)。在Hosts文件中,变量会被定义在主机名的后面或组名的下方,如下面这个例子所示:
# 为某台主机指定变量,作用范围仅限于当台主机
[shanghai]
app1.example.com proxy_state=present
app2.example.com proxy_state=absent
# 为主机组指定变量,指定范围为整个主机组
[shanghai:vars]
cdn_host=sh.static.example.com
api_version=3.0.
举例来说,我们现在要给主机app1.example.com设置一组变量,那就可以直接在/etc/ansible/host_vars/目录下创建一个名为app1.example.com的空白文件,然后在文件中以YAML语法来定义所需的变量,如以下代码所示:
---
foo: bar
baz: qux
同理,要想针对整个shanghai主机组定义一些变量,则只需在/etc/ansible/group_vars/目录下创建于主机组名的YAML文件来定义变量就可以了。
注册变量,其实就是将操作的结果,包括标准输出和标准错误输出,保存到变量中,然后再根据这个变量的内容来决定下一步的操作,在这个过程中用来保存操作结果的变量就叫注册变量。我们在Playbook中使用register来声明一个变量为注册变量。
在第4章中,我们就曾使用register来声明注册变量来保存命令运行结果,然后用其来判断是否需要启动Node.js,再来回顾一下那段代码:
- name: 获取正在运行的Node.js app列表
command: forever list
register: forever_list
changed_when: false
- name: 启动Node.js app
command: forever start {{ node_apps_location }}/app/app.js
when: "forever_list.stdout.find('{{ node_apps_location }}/app/app.js') == -1"
对于普通变量,例如由Ansible命令行设定的、在Hosts文件中定义的,再或者在Playbook和变量定义文件中定义的,这些变量都被称为简单变量或普通变量,我们可以直接在Playbook中使用双大括号加变量名来读取变量内容,形如{{variable}}。比如下面的例子:
- command: /opt/my-app/rebuild {{ my_environment }}
Ansible中除了这些普通变量之外,还有数组变量或者叫列表变量。由于Ansible是基于Python语言开发的,所以我们这里就称之为列表。列表的定义方法如下:
foo_list:
- one
- two
- three
foo[0]
foo|first
tasks:
- debug: var=ansible_eth0
当我们想要读取IPv4地址时,可使用如下两种方法实现:
{{ ansible_eth0.ipv4.address }}
{{ ansible_eth0['ipv4']['address'] }}
Ansible为用户提供了用于批量定义主机的管理文件,及Hosts文件,默认存放位置是/etc/ansible/hosts。有了这个文件,我们可以非常便捷地在里面为主机分组,极大地简化了多主机的操作。
在Hosts文件中,我们使用如下格式定义主机组:
[gorup]
host1
host2
为每个主机定义自己专属变量最直接、最简单的办法就是:在Hosts文件中,在对应主机名的后面直接定义。如下所示:
[group]
host1 admin_user=jane
host2 admin_user=jack
host3
这样我们就为host1和host2分别定义了一个变量,host3主机则无法使用该变量。
如果要对整个主机组设置变量,则采用如下方法:
[group:vars]
admin_user=john
这样一来,变量将会对主机组group下面的所有主机生效,就相当于给其下的每一台主机分别定义了一次变量admin_user。
以上定义主机变量和主机组变量的方法,在主机或主机组数量较少的情况下非常方便有效。但当我们要为非常多的主机和主机组分别设置不同的变量时,这种方法就会显得比较笨拙。
1.group_vars和host_vars
Ansible在运行任务前,都会搜索与Hosts文件同一目录下的两个用于定义变量的目录:group_vars和host_vars。
我们可以在这两个目录下放一些使用YAML语法编辑的 定义变量的文件,并以对应的主机名和主机组名来命名这些文件,这样在运行Ansible时,Ansible会自动去这两个目录下读取针对不同主机和主机组的变量定义。我们可以通过下面的例子来加深一下理解。
1)对主机组group设置变量。
---
# File: /etc/ansible/group_vars/group
admin_user: john
---
# File: /etc/ansible/host_vars/host1
admin_user: jane
2.巧妙使用主机变量和组变量
hostvars可以获取从一台远程主机上获取另一台远程主机的变量信息,变量hostvars包含了指定主机上所定义的所有变量。
比如,我们想获取host1上的变量admin_user的内容,在任意主机上直接使用下面这行代码即可。
{{ hostvars['host1']['admin_user'] }}
1.Facts信息
在运行任何一个Playbook之前,Ansible默认会先抓取Playbook中所指定的所有主机的系统信息,这些信息我们称之为Facts。在之前我们运行的所有Playbook任务中,都会出现类似下面代码的内容:
ansible-playbook playbook.yml
上述命令的运行结果如下:
Facts信息包括(但不仅限于)远程主机的CPU类型、IP地址、磁盘空间、操作系统信息以及网络接口信息等,这些信息对于Playbook的运行
至关重要。我们可以根据这些信息来决定是否要继续运行下一步任务,或者将这些信息写入某个配置文件中。
我们可以使用setup模块来获取对应主机上面的所有可用的Facts信息。比如:
[root@localhost ~]# ansible proxy -m setup
192.168.230.100 | SUCCESS => {
"ansible_facts": {
"ansible_all_ipv4_addresses": [
"192.168.230.100"
],
"ansible_all_ipv6_addresses": [
"fe80::c8f4:9dff:fe3d:9953"
],
"ansible_apparmor": {
"status": "disabled"
},
...
在某些用不到Facts信息的Playbook任务中,我们可以在Playbook中设置gather_facts:no来暂时让Ansible在执行Playbook任务之前跳过收集
远程主机Facts信息这一步,这样可以为任务节省几秒钟的时间,如果主机数量多的话,就能节省更多的时间。在Playbook中设置gather_facts的方法如下:
- hosts: db
gather_facts:no
我们可以把需要定义的变量写进一个以.fact结尾的文件中,这个文件可以是JSON文件或INI文件,或者是一个可以返回JSON代码的可执行文件。
然后将其放置在/etc/ansible/facts.d文件夹中,Ansible在执行任务时会自动到这个文件夹下读取变量信息。
比如,我们在远程主机上创建了一个.fact文件/etc/ansible/facts.d/settings.fact,文件内容如下:
[users]
admin=jane,john
normal=jim
接下来,使用setup模块就可以读取到这两个变量,如下所示:
如果在一个Playbook中,只有部分Playbook任务用到了远程主机自定义的本地Facts,那么我们可以使用下面一段代码来明确的指明只显示这些本地Facts。
- name: 重新获取本地Facts
setup: filter=ansible_local
Ansible自带的Vault加密功能,Vault可以将经过加密的密码和敏感数据同Playbook存储在一起。
Ansible Vault可以为我们提供非常高的安全加密级别,这将很好地帮我们解决后顾之忧。使用如下命令,可以利用Vault给文件加密:
ansible-vault encrypt api_key.yml
除了encrypt选项之外,关于ansible-vault命令有几个比较常用的选项,列举如下。
除了手动输入密码进行解密以外,Ansible还提供了以密码文件的形式来解密的认证方式,Ansible Vault将密码文件放置于~/.ansible/,需设置其权限为600。
现在我们可以在~/.ansible/目录下创建一个权限为600的纯文本文件vault_pass.txt,并写入我们的Vault密码,使用如下命令就可非交互式地使用被加密过的Playbook运行任务了。
ansible-playbook test.yml --vault-password-file ~/.ansible/vault_pass.txt
Ansible官方给出了如下由高到低的优先级排序:
Jinja2支持的数据类型有:字符串型(如"strings")、整数型(如45)、浮点数型(如42.33)、列表(如[1,2,3,4])、元组(与列表类型格式一样,只是内容无法修改)、字典(如{key:value,key2:value2},还有布尔型(如true或false)。
Jinja2同时也支持基本的数据运算,如加、减、乘、除和比较(==表示相等,!=表示不相等,>=表示大于等于,等等)。逻辑运算,可以使用小括号来对逻辑运算符进行分组使用。
下列表达式的运算结果都为'true':
1 in [1,2,3]
'see' in 'Can you see me?'
foo != bar
(1<2) and ('a' not in 'best')
除此之外,Jinja2还提供了非常有用的"test"语句。比如,我们可以使用如下语句来判定变量foo是否被定义过。
foo is defined
当变量foo被定义过,那么这个表达式的结果就是true,相反则为false。类似地还有:undefined,equalto(与==等效),even(判断对象是否是偶数)以及iterable(判断对象是否可迭代)。
我们来看如下一个应用场景,目前我们有一款软件版本号为4.6.1,现在有一个任务需要通过判断软件的版本号来确定要不要执行接下来的任务,如果主版本号为4就执行任务,其他版本则不执行。这时,Jinja2表达式将不再适合,我们可以通过Python内置方法,使用点号"."来对版本号进行拆分后取得第1位主版本号,然后用它与数字4进行比较。具体代码如下:
- name: 当软件主版本号为4的时候进行操作
[task here]
when: software_version.split('.')[0] == '4'
任何一个任务都可以注册一个变量来存储其运行结果,该注册变量在随后的任务中将像其他普通变量一样被使用。
大部分情况下,我们使用注册器用来接收shell命令的返回结果,结果中包含标准输出(stdout)和错误输出(stderr)。使用下面一段代码即可调用注册器来获取shell命令的返回结果。
- shell: my_command_here
register: my_command_result
如果想查看一个注册变量都有哪些属性,那么在运行一个Playbook的时候,使用-v选项来检查Playbook的运行结果,通常我们会得到如下4种类型的运行结果。
假设我们的所有服务器上都有一个布尔变量is_db_server,在数据库服务器上,其值为true,其他主机上值为false,我们只需要在数据库服务器上安装MySQL软件包,
- yum: name=mysql-server state=present
when: is_db_server
- yum: name=mysql-server state=present
when: (is_db_server is defined) and is_db_server
- command: my-app --status
register: myapp_result
- command: do-something-to-my-app
when: "'ready' in myapp_result.stdout"
对于Ansible来说,其很难判断一个命令的运行是否符合我们的实际预期,尤其是当我们使用command模块和shell模块时,如果不使用changed_when语句,Ansible将永远返回changed。大部分模块都能正确返回运行结果是否对目标主机产生影响,我们依然可以使用changed_when语句来对返回信息进行重写,根据任务返回结果来判定任务的运行结果是否真正符合我们预期。
正常情况下,当我们使用PHP Composer来安装项目依赖的时候,无论是否安装或升级的某些软件,Ansible任务的返回结果都是changed。但是当我们使用changed_when语句,并结合注册变量对任务返回结果进行判断后,在来决定是否显示状态为changed,将更加符合我们的实际需求。比如:
- name: Install dependencies via Composer.
command: "/usr/local/bin/composer global require phpunit/phpunit --prefer-dist"
register: composer
changed_when: "'Nothing to install or update' not in composer.stdout"
有一些命令会将自己的运行结果写入标准错误输出stderr中,而不是通常的标准输出stdout中,这时可以使用failed_when来对结果进行判断,从而告诉Ansible真正的运行结果到底是成功还是失败。
在下面的例子中,我们将通过判断Jenkins CLI命令的错误输出来判定命令是否真的运行失败。代码如下:
- name: 通过CLI导入Jenkins任务
shell: >
java -jar /opt/jenkins-cli.jar -s http://localhost:8080/
create-job "My Job" < /usr/local/my-job.xml
register: import
failed_when: "import.stderr and 'already exists' not in import.stderr"
在有些情况下,一些必须运行的命令或脚本会报一些错误,而这些错误并不一定真的说明有问题,但是经常会给接下来要运行的任务造成困扰,甚至直接导致Playbook运行中断。
这时候,我们可以在相关任务中添加ignore_errors:true来屏蔽所有错误信息,Ansible也将视该任务运行成功,不再报错,这样就不会对接下来要运行的任务造成额外困扰。但是要注意的是,我们不应过度依赖ingore_errors,因为它会隐藏所有的报错信息,而应该把精力集中在寻找报错的原因上面,这样才能从根本上解决问题。
默认情况下,Ansible的所有任务都是在我们指定的机器上面运行的,当在一个独立的群集环境中配置时,这并没有什么问题。而在有些情况下,比如给某台服务器发送通知或向监控服务中添加被监控主机,这个时候任务就需要在特定的主机上运行,而非一开始指定的所有主机。此时就需要用到Ansible的任务委托功能。
使用delegate_to关键字便可以配置任务在指定的机器上执行,而其他任务还是在hosts关键字配置的所有机器上运行,当到了这个关键字所在的任务时,就使用委托的机器运行。而facts还使用与当前的host,下面我们演示一个例子,使用Munin在监控服务器中添加一个被监控主机。
---
- hosts: webservers
tasks:
- name: Add server to Munin monitoring configuration.
command: monitor-server webservers {{ inventory_hostname }}
delegate_to: "{{ monitoring_master }}"
如果我们想将一个任务在Ansible服务器本地运行,除了将任务委托给127.0.0.1之外,还可以全用local_action方法来完成。看下面两个功能一模一样的例子:
- name: Remove server from load balancer.
command: remove-from-lb {{ inventory_hostname }}
delegate_to: 127.0.0.1
- name: Remove server from load balancer.
local_action: command remove-from-lb {{ inventory_hostname }}
在有些情况下,一些任务的运行需要等待一些状态的恢复,比如某一台主机或者应用刚刚重启,我们需要等待它上面的某个端口开启,此时我们就不得不将正在运行的任务暂停,直到其状态满足我们的需求。先来看下面的例子:
- name: Wait for webserver to start.
local_action:
module: wait_for
host: webserver1
prot: 80
delay: 10
timeout: 300
state: started
总结一下,Ansible的wait_for模块常用于如下一些场景中:
在少数情况下,Ansible任务运行的过程中需要用户输入一些数据,这些数据要么比较私密不方便保存,或者数据是动态的,不同用户有不同的需求,比如输入用户自己的账号和密码或者输入不同的版本号会触发不同的后续操作等。Ansible的vars_prompt关键字就是用来处理上述这种与用户交互的情况的。
我们先来看一个例子:我们需要用户提供自己的账号和密码来登录自己的网络账户,并且可以给用户已适当的文字提示。代码如下所示:
---
- hosts:all
vars_prompt:
- name: share_user
prompt: "What is your network username?"
- name: share_pass
prompt: "What is your network password?"
private: yes
默认情况下,Ansible在执行一个Playbook时,会执行Playbook中定义的所有任务。Ansible的标签(Tags)功能可以给角色(Roles)、文件、单独的任务甚至整个Playbook打上标签,然后利用这些标签来指定要运行Playbook中的个别任务,或不执行指定的任务,并且它的语法非常简单。
在下面这个例子中,我们将展示多种不同的打标签的方法。
---
# 可以给整个Playbook的所有任务打一个标签
- hosts: proxy
tags: deploy
roles:
# 给角色打的标签将会应用于角色下所有的任务
- { role: tomcat,tags: ['tomcat','app'] }
tasks:
- name: Notify on completion.
local_action:
module: osx_say
msg: "{{ inventory_hostname }} is finished!"
voice: Zarvox
tags:
- notifications
- say
- include: foo.yml
tags: foo
ansible-palybook tags.yml --tags "say"
ansible-playbook tags.yml --skip-tags "notifications"
我们可以为一个对象添加多个标签,但是在添加多标签时必须使用YAML列表格式。YAML列表格式如下:
# 最简洁的写法
tags: ['one','two','three']
# 最清晰的写法
tags:
- one
- two
- three
# 不正确的写法
tags: one,two,three
Ansible从2.0.0版本开始引入了块功能,块功能可以将任务进行分组,并且可以在块级别上应用任务变量。同时,块功能还可以使用类似于其他编程语言处理异常那样的方法,来处理块内部的任务异常。
来看一个例子:
---
- hosts: web
tasks:
# Install and configure Apache on RedHat/CentOS hosts.
- block:
- yum: name=httpd state=present
- template: src=httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
- service: name=httpd state=started enabled=yes
when: ansible_os_family == 'RedHat'
sudo: yes
# Install and configure Apache on Debian/Ubuntu hosts.
- block:
- apt: name=apache2 state=present
- template: src=httpd.conf.j2 dest=/etc/apache2/apache2.conf
- service: name=apache2 state=started enabled=yes
when: ansible_os_family == 'Debian'
sudo: yes
在上例中,我们使用了带有when语句的块来指定在不同平台上运行一组不同的安装配置任务,我们可以看到,块将apt,template,service三个模块任务包含在内作为一个整块,这样就不用再每一个模块任务后都跟一个when语句进行操作系统的判断了。由此我们可以看出,块功能非常适合于多个任务共用同一套任务参数的情况。
块功能也可以用来处理任务的异常。比如有一个Ansible任务时监控一个并不太重要的应用,这个应用的正常运行与否对后续的任务并不产生影响,这时我们就可以通过块功能来处理这个应用的报错。如下代码所示:
tasks:
- block:
- name: Shell script to connect the app to a monitoring service.
script: monitoring-connect.sh
rescue:
- name: 只有脚本报错时才执行
debug: msg="There was an error in the block."
always:
- name: 无论结果如何都执行
debug: msg="This always executes."
当块中的任意任务出错时,rescue关键字对应的代码块就会被执行,而always关键字对应的代码块无论如何都会被执行。