自动化运维之Ansible

自动化运维第一步, 走起.

由于这是第一篇学习自动化运维工具的文章, 所以我们先来瞎扯扯..

自动化运维相关

我们的运维工作, 假设从系统的安装开始, 这里就已经有很多说的点了. 比如我们可以从设备厂商协调好, 安装指定版本的系统, 这样拿过来的机器就已经是有系统了. 另外, 还可以使用我们之前说的PXE技术进行网络引导, 只要接通电源开机就可以自动进行安装了. 这是物理机的情况. 如果是虚拟机就更简单了, 我们可以直接生成一个模板, 模板中都携带了需要配置的各个参数, 但是这个时候, 我们可能需要在安装的时候有工具能够动态的去插入一些私有的信息, 例如MAC地址. 接着安装完系统, 我们就进入程序包的安装, 配置和服务启动, 想象一个十几台机器的场景, 难道一个一个手动的配置? 显然不可能, 这里我们就需要有能够统一集中部署的方法, 也就是批量的操作. 再往后, 我们就需要进行服务的发布了, 这个一般就是比较重要的一环了, 我们需要进行平滑的, 滚动的更新和版本回滚. 再往后, 就是监控的工作. 例如:告警, 自动修复等等.

再说说我们的程序发布问题, 首先在程序的研发阶段完成了之后, 显然是需要进行测试的. 这一步叫做预发布验证, 新版本的代码先发布到测试环境中, 测试环境可能和线上的环境配置相同, 只不过没有接入调度器罢了. 在我们的程序发布中, 有一些原则性的问题需要保证:

  • 不能影响用户体验
  • 系统不能停机
  • 不能导致系统故障或者系统不可用

这样我们就需要一个发布模型, 常见的一种叫做灰度模型, 灰度发布, 一般我们可以使用符号链接的方式进行版本间切换. 接着 按照我们说过的 只要路径相同, 或者说都是标准的. 我们就可以通过应用程序或者脚本进行自动的切换.

Ansible入门

现在就来说说主角了, Ansible是一个使用Python语言的自动化运维工具, 功能挺强大的. 不过不管怎么说, 先来想个这样的问题, 我们怎么在远端执行命令呢? 我们知道所有的操作都是需要用户的权限来执行的. 这个时候还记得之前在说keepalived和heartbeat的时候使用的:

1
ssh VM-node3 "date"

吗?

是的, 我们的ansible就是使用ssh协议进行的远端调用. 这就说明, 我们只需要一个ansible服务端就行了, 被调用的, 或者说被管理的节点不需要特地安装其他的程序. 这就是agentless的运维工具. 而另外一种 就是agent的, 这就是运维工具的一个小分类.

除了ansible, fabric也是基于ssh的. 另外, puppet , func 这些就是典型的需要agent的类型了.

当然, 是否需要agent各有各的好处和缺点.

截止目前(2017-10-25 13:51), Ansible的github仓库star数是26w, 3420个仍旧open的issue和1.1万个已经closed的.

Ansible能够实现统一配置, 统一部署, 批量执行, 多层次多用户并行等功能. 来看一张架构图:

img

左上角的Host Inventory就是字面意思, 存放管理的主机清单. Ansible通过才能够下面定制的Playbooks从Inventory中抓取主机清单, 接着通过右上角的连接插件进行连接和管理操作. 而且还可以使用插件和其他语言建立自定义的模块.

Ansible依靠Python中的Paramiko, PyYAML, Jinja2等库和模块构建.

直接安装看看, ansible被收录在epel源中, 配置好yum源就可以直接yum install了.

安装完成后, 我们可以看到, 几个配置文件:

1
2
3
4
5
6
[root@VM-node1 ~]# rpm -ql ansible | less
/etc/ansible
/etc/ansible/ansible.cfg
/etc/ansible/hosts
/etc/ansible/roles
..(omitted)

主配置文件就是那个cfg了, 额hosts就是我们的inventory. 但是这个清单文件也不是随随便便就可以写的, 里面要指明Web服务器, 存储服务器等等包括其他的一些信息.

由于使用ssh的方式, 所以不妨还是先把基于密钥的认证做一下, 当然这不是必须的, 我们也可以在配置文件中指明主机的账号和密码. 我已经配好了:

1
2
3
4
[root@VM-node1 ~]# ssh VM-node2 'date'
Thu Oct 26 05:40:41 CST 2017
[root@VM-node1 ~]# ssh VM-node3 'date'
Wed Oct 25 21:40:44 CST 2017

接下来我们就来看一下ansible基本命令是怎么使用的吧.

ansible基于模块的设计, 所以我们使用ansible, 其实就是使用它的模块, ansible的默认模块是一个叫做command的模块. 总体的命令格式如下:

1
ansible <host-pattern> [options]

这里的host-pattern就是可以指定的主机, 默认使用的配置文件就是/etc/ansible/hosts, 我们先来看一下这个文件:

1
2
3
4
5
6
[testservers]
192.168.206.9
192.168.206.10
192.168.206.22
#####
www[001:006].example.com

通过这样的组定义, 我们可以一个服务器组, 也支持这样的, ansible可以自动展开.

定义了主机之后 ,我们就可以使用ansible了, 示例:

1
2
3
4
5
6
7
8
9
[root@VM-node1 ~]# ansible testservers -m command -a "uname -r"
192.168.206.22 | SUCCESS | rc=0 >>
2.6.32-220.el6.x86_64

192.168.206.10 | SUCCESS | rc=0 >>
3.10.0-693.2.2.el7.x86_64

192.168.206.9 | SUCCESS | rc=0 >>
3.10.0-693.2.2.el7.x86_64

来解释一下, 首先我们指明对哪些主机执行操作, 这里就使用的是当时在文件中定义的组名, 接着我们使用-m指明使用的模块command, 后面的参数就是对于每一个模块而言的了. 每一个模块有自己的参数.

那么我该怎么样获取每个模块的用法呢? ansible-doc就是为了这个存在的, 我们可以使用下面来查看所有的模块和每个模块的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@VM-node1 ~]# ansible-doc -l
a10_server Manage A10 Networks AX/SoftAX/Thunder/vThunder devices' server object.
a10_server_axapi3 Manage A10 Networks AX/SoftAX/Thunder/vThunder devices
a10_service_group Manage A10 Networks AX/SoftAX/Thunder/vThunder devices' service groups.
a10_virtual_server Manage A10 Networks AX/SoftAX/Thunder/vThunder devices' virtual servers.
accelerate Enable accelerated mode on remote node
aci_aep Manage attachable Access Entity Profile (AEP) on Cisco ACI fabrics (infra:AttEntityP)
aci_ap Manage top level Application Profile (AP) objects on Cisco ACI fabrics (fv:Ap)
aci_bd Manage Bridge Domains (BD) on Cisco ACI Fabrics (fv:BD)
aci_bd_subnet Manage Subnets on Cisco ACI fabrics (fv:Subnet)
aci_bd_to_l3out Bind Bridge Domain to L3 Out on Cisco ACI fabrics (fv:RsBDToOut)
aci_config_rollback Provides rollback and rollback preview functionality for Cisco ACI fabrics (config:ImportP)
aci_config_snapshot Manage Config Snapshots on Cisco ACI fabrics (config:Snapshot, config:ExportP)
...(omitted)

太多了, 我们看看一共有多少的模块支持:

1
2
3
4
5
6
7
8
9
10
11
12
[root@VM-node1 ~]# ansible-doc -l | wc -l
1375
[root@VM-node1 ~]# ansible-doc command
> COMMAND (/usr/lib/python2.7/site-packages/ansible/modules/commands/command.py)

The `command' module takes the command name followed by a list of space-delimited arguments. The given command will be executed
on all selected nodes. It will not be processed through the shell, so variables like `$HOME' and operations like `"<"', `">"',
`"|"', `";"' and `"&"' will not work (use the [shell] module if you need these features). For Windows targets, use the
[win_command] module instead.

OPTIONS (= is mandatory):
...(omitted)

1000+的模块, 但是我们使用的也是很有限的几个. 可以直接在后面加上模块的名字, 就可以获取到相关的说明和文档了.

另外, 由于command是默认模块, 所以也可以不用特定指明:

1
2
3
4
5
6
7
8
9
[root@VM-node1 ~]# ansible all -a "uname -r"
192.168.206.22 | SUCCESS | rc=0 >>
2.6.32-220.el6.x86_64

192.168.206.10 | SUCCESS | rc=0 >>
3.10.0-693.2.2.el7.x86_64

192.168.206.9 | SUCCESS | rc=0 >>
3.10.0-693.2.2.el7.x86_64

使用all关键字, 就可以对整个文件中的主机发出指令.

接下来就来学习一下常见的几个模块吧.

cron

先来看一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[root@VM-node1 ~]# ansible testservers -m cron -a "minute=*/10 job='/bin/echo Hello' name='test of Ansible cron module' state=present"
192.168.206.22 | SUCCESS => {
"changed": true,
"envs": [],
"failed": false,
"jobs": [
"test of Ansible cron module"
]
}
192.168.206.10 | SUCCESS => {
"changed": true,
"envs": [],
"failed": false,
"jobs": [
"test of Ansible cron module"
]
}
192.168.206.9 | SUCCESS => {
"changed": true,
"envs": [],
"failed": false,
"jobs": [
"test of Ansible cron module"
]
}

接着我们来稍微确认一下:

1
2
3
4
5
6
7
8
9
10
11
12
[root@VM-node1 ~]# ansible testservers -a 'crontab -l'
192.168.206.22 | SUCCESS | rc=0 >>
#Ansible: test of Ansible cron module
*/10 * * * * /bin/echo Hello

192.168.206.10 | SUCCESS | rc=0 >>
#Ansible: test of Ansible cron module
*/10 * * * * /bin/echo Hello

192.168.206.9 | SUCCESS | rc=0 >>
#Ansible: test of Ansible cron module
*/10 * * * * /bin/echo Hello

已经添加好了.

我们来说说一些相关的参数吧, 首先肯定要提供的就是我们的时间间隔和任务是什么, 对应minute, hour, day, weekday, 如果不指明就是默认的*. 接着我们需要指明添加的名字是什么, 这样Ansible才可以对他们进行操作. 如果不指定就是None, 这样造成的后果是很严重的, 相当于每一次操作就会改变全体. 接着最后我们写了一个state, 指明是添加还是去除, 默认是添加.

比如我们现在把之前添加的删除掉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@VM-node1 ~]# ansible testservers -m cron -a "name='test of Ansible cron module' state=absent"
192.168.206.22 | SUCCESS => {
"changed": true,
"envs": [],
"failed": false,
"jobs": []
}
192.168.206.10 | SUCCESS => {
"changed": true,
"envs": [],
"failed": false,
"jobs": []
}
192.168.206.9 | SUCCESS => {
"changed": true,
"envs": [],
"failed": false,
"jobs": []
}

这样就没有了.

user

user模块听名字也就知道功能是什么了吧哈哈. 对就是进行用户管理的, 有很多属性和指令, 必须要给的一个就是name用户名了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
[root@VM-node1 ~]# ansible all -m user -a 'name=user1'
192.168.206.22 | SUCCESS => {
"changed": true,
"comment": "",
"createhome": true,
"failed": false,
"group": 500,
"home": "/home/user1",
"name": "user1",
"shell": "/bin/bash",
"state": "present",
"system": false,
"uid": 500
}
192.168.206.10 | SUCCESS => {
"changed": true,
"comment": "",
"createhome": true,
"failed": false,
"group": 1000,
"home": "/home/user1",
"name": "user1",
"shell": "/bin/bash",
"state": "present",
"system": false,
"uid": 1000
}
192.168.206.9 | SUCCESS => {
"changed": true,
"comment": "",
"createhome": true,
"failed": false,
"group": 1001,
"home": "/home/user1",
"name": "user1",
"shell": "/bin/bash",
"state": "present",
"system": false,
"uid": 1001
}
[root@VM-node1 ~]# ansible all -m user -a 'name=user1'
192.168.206.22 | SUCCESS => {
"changed": true,
"comment": "",
"createhome": true,
"failed": false,
"group": 500,
"home": "/home/user1",
"name": "user1",
"shell": "/bin/bash",
"state": "present",
"system": false,
"uid": 500
}
192.168.206.10 | SUCCESS => {
"changed": true,
"comment": "",
"createhome": true,
"failed": false,
"group": 1000,
"home": "/home/user1",
"name": "user1",
"shell": "/bin/bash",
"state": "present",
"system": false,
"uid": 1000
}
192.168.206.9 | SUCCESS => {
"changed": true,
"comment": "",
"createhome": true,
"failed": false,
"group": 1001,
"home": "/home/user1",
"name": "user1",
"shell": "/bin/bash",
"state": "present",
"system": false,
"uid": 1001
}

接着我们也可以确认一下:

1
2
3
4
5
6
7
8
9
[root@VM-node1 ~]# ansible all -a 'tail -1 /etc/passwd'
192.168.206.22 | SUCCESS | rc=0 >>
user1:x:500:500::/home/user1:/bin/bash

192.168.206.10 | SUCCESS | rc=0 >>
user1:x:1000:1000::/home/user1:/bin/bash

192.168.206.9 | SUCCESS | rc=0 >>
user1:x:1001:1001::/home/user1:/bin/bash

要删除也很简单了, 直接state=absent就行了. 其他的功能通过读文档也能很快的OK.

group

就好比我们的groupadd命令的参数极其的少一样, 这个模块也很简单:

1
2
3
4
5
6
7
[root@VM-node1 ~]# ansible-doc -s group
- name: Add or remove groups
group:
gid: # Optional `GID' to set for the group.
name: # (required) Name of the group to manage.
state: # Whether the group should be present or not on the remote host.
system: # If `yes', indicates that the group created is a system group.

设置gid, 组名, 状态, 是否为系统组.完了. 就是这么简洁.

copy

主要用来实现文件复制, 先来实际操作一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
[root@VM-node1 ~]# ansible testservers -m copy -a "src=/etc/fstab dest=/tmp/fstab.ansible owner=root mode=640"
192.168.206.22 | SUCCESS => {
"changed": true,
"checksum": "bcc5409249b05584582f46eb8cc41d6f93df01f8",
"dest": "/tmp/fstab.ansible",
"failed": false,
"gid": 0,
"group": "root",
"md5sum": "4fba41ce7527a0977db246e220f94855",
"mode": "0640",
"owner": "root",
"size": 465,
"src": "/root/.ansible/tmp/ansible-tmp-1509005226.85-229350602804565/source",
"state": "file",
"uid": 0
}
192.168.206.10 | SUCCESS => {
"changed": true,
"checksum": "bcc5409249b05584582f46eb8cc41d6f93df01f8",
"dest": "/tmp/fstab.ansible",
"failed": false,
"gid": 0,
"group": "root",
"md5sum": "4fba41ce7527a0977db246e220f94855",
"mode": "0640",
"owner": "root",
"secontext": "unconfined_u:object_r:admin_home_t:s0",
"size": 465,
"src": "/root/.ansible/tmp/ansible-tmp-1509005226.81-171401749560627/source",
"state": "file",
"uid": 0
}
192.168.206.9 | SUCCESS => {
"changed": true,
"checksum": "bcc5409249b05584582f46eb8cc41d6f93df01f8",
"dest": "/tmp/fstab.ansible",
"failed": false,
"gid": 0,
"group": "root",
"md5sum": "4fba41ce7527a0977db246e220f94855",
"mode": "0640",
"owner": "root",
"secontext": "unconfined_u:object_r:admin_home_t:s0",
"size": 465,
"src": "/root/.ansible/tmp/ansible-tmp-1509005226.8-44103016607587/source",
"state": "file",
"uid": 0
}

接着简单的验证一下:

1
2
3
4
5
6
7
8
9
[root@VM-node1 ~]# ansible all -a 'ls -l /tmp/fstab.ansible'
192.168.206.22 | SUCCESS | rc=0 >>
-rw-r----- 1 root root 465 Oct 26 16:07 /tmp/fstab.ansible

192.168.206.10 | SUCCESS | rc=0 >>
-rw-r-----. 1 root root 465 Oct 26 16:51 /tmp/fstab.ansible

192.168.206.9 | SUCCESS | rc=0 >>
-rw-r-----. 1 root root 465 Oct 26 16:07 /tmp/fstab.ansible

确实也存在了.

其实我们会用到的参数也不是很多, 就那一些. 目的地址是必须要指明的,而且需要使用绝对地址. 另外, 对于目录, 如果出现了不存在的父目录, 会报错并执行失败.

使用owner/group可以来指定属主属组, 接着mode用来指定权限, 都是很好理解的. 有意思的是, src并不是必须的. 我们也可以使用content来指明文件的内容, 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[root@VM-node1 ~]# ansible all -m copy -a 'content="Hello World\nHello Ansible\n" dest=/tmp/test.ansible'
192.168.206.22 | SUCCESS => {
"changed": true,
"checksum": "b9169ba8226473029923516a19a73c4c68f10daa",
"dest": "/tmp/test.ansible",
"failed": false,
"gid": 0,
"group": "root",
"md5sum": "0bb9e5050a1820c59d836bf9ef6a5080",
"mode": "0644",
"owner": "root",
"size": 26,
"src": "/root/.ansible/tmp/ansible-tmp-1509008122.2-242725231441981/source",
"state": "file",
"uid": 0
}
...(omitted)
[root@VM-node1 ~]# ansible all -a "cat /tmp/test.ansible"
192.168.206.22 | SUCCESS | rc=0 >>
Hello World
Hello Ansible

192.168.206.10 | SUCCESS | rc=0 >>
Hello World
Hello Ansible

192.168.206.9 | SUCCESS | rc=0 >>
Hello World
Hello Ansible

虽然, 该模块也可以提供对文件的操作, 但是更多的时候, 我们使用file模块

file

对于我们刚刚复制过去的那两个文件, 我们来修改一下属组属主, 修改一下权限, 并且来做个符号链接吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@VM-node1 ~]# ansible all -m file -a 'owner=user1 group=user1 mode=600 path=/tmp/fstab.ansible'
192.168.206.22 | SUCCESS => {
"changed": true,
"failed": false,
"gid": 500,
"group": "user1",
"mode": "0600",
"owner": "user1",
"path": "/tmp/fstab.ansible",
"size": 465,
"state": "file",
"uid": 500
}
...(omitted)

还是..来验证一下吧:

1
2
3
4
[root@VM-node1 ~]# ansible all -a 'ls -l /tmp/fstab.ansible'
192.168.206.22 | SUCCESS | rc=0 >>
-rw------- 1 user1 user1 465 Oct 26 16:07 /tmp/fstab.ansible
...(omitted)

接着, 我们在做一个符号链接指向它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@VM-node1 ~]# ansible all -m file -a 'path=/tmp/fstab.link src=/tmp/fstab.ansible state=link'
192.168.206.22 | SUCCESS => {
"changed": true,
"dest": "/tmp/fstab.link",
"failed": false,
"gid": 0,
"group": "root",
"mode": "0777",
"owner": "root",
"size": 18,
"src": "/tmp/fstab.ansible",
"state": "link",
"uid": 0
}
...(omitted)

也来…验证一下吧:

1
2
3
4
[root@VM-node1 ~]# ansible all -a 'ls -l /tmp/fstab.link'
192.168.206.22 | SUCCESS | rc=0 >>
lrwxrwxrwx 1 root root 18 Oct 26 17:03 /tmp/fstab.link -> /tmp/fstab.ansible
...(omitted)

注意在我们进行链接的时候要指明state=link才行. 否则会报错的. 另外, 如果创建的是硬链接, 状态就是hard了.

ping

这是一个超级简单的模块哈哈, 这样用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@VM-node1 ~]# ansible all -m ping
192.168.206.22 | SUCCESS => {
"changed": false,
"failed": false,
"ping": "pong"
}
192.168.206.10 | SUCCESS => {
"changed": false,
"failed": false,
"ping": "pong"
}
192.168.206.9 | SUCCESS => {
"changed": false,
"failed": false,
"ping": "pong"
}

如果主机可以ping通, 就会显示pong!

service

看名字就可以意识到这是一个超级重要的模块了吧哈哈. 我们来看一下吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@VM-node1 ~]# ansible testservers -a "service httpd status"
[WARNING]: Consider using service module rather than running service

192.168.206.22 | FAILED | rc=3 >>
httpd is stoppednon-zero return code

192.168.206.10 | FAILED | rc=3 >>
● httpd.service - The Apache HTTP Server
Loaded: loaded (/usr/lib/systemd/system/httpd.service; disabled; vendor preset: disabled)
Active: inactive (dead)
Docs: man:httpd(8)
man:apachectl(8)

Oct 25 03:50:01 VM-node2 systemd[1]: Unit httpd.service cannot be reloaded because it is inactive.Redirecting to /bin/systemctl status httpd.servicenon-zero return code

由于我们的后端两个主机不是一个系统, 这就造成输出的信息并不一致的情况了, 接着我们启动并且设置成开机自动启动httpd服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[root@VM-node1 ~]# ansible testservers -m service -a 'enabled=true state=started name=httpd'
192.168.206.22 | SUCCESS => {
"changed": true,
"enabled": true,
"failed": false,
"name": "httpd",
"state": "started"
}
192.168.206.10 | SUCCESS => {
"changed": true,
"enabled": true,
"failed": false,
"name": "httpd",
"state": "started",
"status": {
"ActiveEnterTimestampMonotonic": "0",
"ActiveExitTimestampMonotonic": "0",
...(omitted)
"UnitFileState": "disabled",
"Wants": "system.slice",
"WatchdogTimestampMonotonic": "0",
"WatchdogUSec": "0"
}
}

( CentOS7输出的信息真可怕…

接着我们确认一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@VM-node1 ~]# ansible testservers -a 'service httpd status'
[WARNING]: Consider using service module rather than running service

192.168.206.22 | SUCCESS | rc=0 >>
httpd (pid 14688) is running...

192.168.206.10 | SUCCESS | rc=0 >>
● httpd.service - The Apache HTTP Server
Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled)
Active: active (running) since Thu 2017-10-26 18:19:33 CST; 2min 48s ago
Docs: man:httpd(8)
man:apachectl(8)
Main PID: 9510 (httpd)
Status: "Total requests: 0; Current requests/sec: 0; Current traffic: 0 B/sec"
CGroup: /system.slice/httpd.service
├─9510 /usr/sbin/httpd -DFOREGROUND
├─9511 /usr/sbin/httpd -DFOREGROUND
├─9513 /usr/sbin/httpd -DFOREGROUND
├─9514 /usr/sbin/httpd -DFOREGROUND
├─9516 /usr/sbin/httpd -DFOREGROUND
└─9517 /usr/sbin/httpd -DFOREGROUND

Oct 26 18:19:33 VM-node2 systemd[1]: Starting The Apache HTTP Server...
Oct 26 18:19:33 VM-node2 systemd[1]: Started The Apache HTTP Server.Redirecting to /bin/systemctl status httpd.service

确实已经启动, 而且, 开机自启动也已经OK了.

总结一下 service模块通过enabled来决定是否开机自动启动, 取值true或者false. name: 服务名称. state: 状态(stopped, started, restarted)

shell

shell模块和command模块有点类似, 但是不同之处在于, command不提供变量支持. 我们来做个测试就知道了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@VM-node1 ~]# ansible all -m command -a "tail -1 /etc/shadow"
192.168.206.22 | SUCCESS | rc=0 >>
user1:!!:17465:0:99999:7:::

192.168.206.10 | SUCCESS | rc=0 >>
user1:!!:17465:0:99999:7:::

[root@VM-node1 ~]# ansible all -m command -a "echo 'password' | passwd --stdin user1"
192.168.206.22 | SUCCESS | rc=0 >>
password | passwd --stdin user1

192.168.206.10 | SUCCESS | rc=0 >>
password | passwd --stdin user1

[root@VM-node1 ~]# ansible all -m command -a "tail -1 /etc/shadow"
192.168.206.22 | SUCCESS | rc=0 >>
user1:!!:17465:0:99999:7:::

192.168.206.10 | SUCCESS | rc=0 >>
user1:!!:17465:0:99999:7:::

没有设置成功, 原因是我们使用了管道符. 接下来换做shell试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@VM-node1 ~]# ansible all -m shell -a "echo 'password' | passwd --stdin user1"
192.168.206.22 | SUCCESS | rc=0 >>
Changing password for user user1.
passwd: all authentication tokens updated successfully.

192.168.206.10 | SUCCESS | rc=0 >>
Changing password for user user1.
passwd: all authentication tokens updated successfully.

[root@VM-node1 ~]# ansible all -m command -a "tail -1 /etc/shadow"
192.168.206.22 | SUCCESS | rc=0 >>
user1:$6$paTv//V6$UmwQlj0OinDI9RLkIcHUWFvQNHxZF9aacuH272zDaRyoKpn21jH568IVLEJv.Q8ShxPrtie9CHdfEuiVFgOwl0:17465:0:99999:7:::

192.168.206.10 | SUCCESS | rc=0 >>
user1:$6$3DO.U5ry$uAXVYHoNz7R5IfdksZZZMvcYLnlXYKuIHebpg65obLoTupzZZmbAFvY4soylGBwepad33v6QJ6sPqbduAwMUX.:17465:0:99999:7:::

提示语也是不一样的. 密码也成功的更新了.

shell虽然支持这样的, 但是如果是要执行脚本, 我们就需要下一个模块了

script

script模块用于脚本执行, 比较坑的一个地方是, 它不支持绝对路径, 也就是说仅仅只有当你使用相对路径才接受:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@VM-node1 ~]# cat script 
#!/bin/bash
echo "Test" > /tmp/script.ansible
[root@VM-node1 ~]# chmod +x script
[root@VM-node1 ~]# ansible all -m script -a 'script'
192.168.206.22 | SUCCESS => {
"changed": true,
"failed": false,
"rc": 0,
"stderr": "Shared connection to 192.168.206.22 closed.\r\n",
"stdout": "",
"stdout_lines": []
}
192.168.206.10 | SUCCESS => {
"changed": true,
"failed": false,
"rc": 0,
"stderr": "Shared connection to 192.168.206.10 closed.\r\n",
"stdout": "",
"stdout_lines": []
}

按照惯例, 验证一下:

1
2
3
4
5
6
[root@VM-node1 ~]# ansible all -a 'cat /tmp/script.ansible'
192.168.206.22 | SUCCESS | rc=0 >>
Test

192.168.206.10 | SUCCESS | rc=0 >>
Test

yum

见名知意啦, 为了yum安装软件包的一个模块, 使用起来也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@VM-node1 ~]# ansible all -m yum -a 'name=zsh'
192.168.206.22 | SUCCESS => {
"changed": true,
"failed": false,
"msg": "",
"rc": 0,
"results": [
"Loaded plugins:
...(omitted)
]
}
192.168.206.10 | SUCCESS => {
"changed": true,
"failed": false,
"msg": "",
"rc": 0,
"results": [
...(omitted)
]
}

这样就安装完成了, 我们来验证一下咯:

1
2
3
4
5
6
7
8
[root@VM-node1 ~]# ansible all -a 'rpm -q zsh'
[WARNING]: Consider using yum, dnf or zypper module rather than running rpm

192.168.206.22 | SUCCESS | rc=0 >>
zsh-4.3.11-4.el6.centos.2.x86_64

192.168.206.10 | SUCCESS | rc=0 >>
zsh-5.0.2-28.el7.x86_64

删除也是很简单的, 直接指明state=absent就行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[root@VM-node1 ~]# ansible all -m yum -a 'name=zsh state=absent'
192.168.206.10 | SUCCESS => {
"changed": true,
"failed": false,
"msg": "",
"rc": 0,
"results": [
...(omitted)
]
}
192.168.206.22 | SUCCESS => {
"changed": true,
"failed": false,
"msg": "",
"rc": 0,
"results": [
...(omitted)
]
}
[root@VM-node1 ~]# ansible all -a 'rpm -q zsh'
[WARNING]: Consider using yum, dnf or zypper module rather than running rpm

192.168.206.22 | FAILED | rc=1 >>
package zsh is not installednon-zero return code

192.168.206.10 | FAILED | rc=1 >>
package zsh is not installednon-zero return code

这样就删除掉了.

setup

这个模块可以用来获取主机的fact信息. 什么是fact呢? 为了能够管理各个主机, ansible会获取每一个主机的一些信息, 包括系统信息, 内核版本, IP地址, BIOS日期等等大量信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@VM-node1 ~]# ansible all -m setup
192.168.206.22 | SUCCESS => {
"ansible_facts": {
"ansible_all_ipv4_addresses": [
"192.168.206.22"
],
"ansible_all_ipv6_addresses": [
"fe80::20c:29ff:feb0:b47f"
],
"ansible_apparmor": {
"status": "disabled"
},
"ansible_architecture": "x86_64",
"ansible_bios_date": "07/02/2015",
"ansible_bios_version": "6.00",
"ansible_cmdline": {
"KEYBOARDTYPE": "pc",
"KEYTABLE": "us",
"LANG": "en_US.UTF-8",
"SYSFONT": "latarcyrheb-sun16",
"quiet": true,
...(omitted)

YAML

说了一些模块的使用, 接下来我们就可以开始尝试写一下playbook了. 但在此之前, 我们还是要了解一下playbook的格式 – YAML

如果你使用过hexo, 并且乐于配置的话. 相信对YAML挺熟悉的了.不过我们在说说这个东西吧.

YAML也是一个递归缩写, 即YAML Ain’t Markup Language.

YAML通过缩进和键值对来表示数据结构. 使用一个-来表示列表, 使用一个:来表示键值对, 一个示例:

1
2
3
4
5
6
7
8
9
name: Justin
age: 20
gender: Male
attr:
isAlive: True
isHappy: True
skills:
- Linux
- Windows

很简单了..

Playbook

想要写一个playbook, 我们先要了解到playbook涉及到那些东西, 请看下面:

  • Inventory
  • Modules
  • Playbooks
    • Tasks
    • Variable
    • Templates
    • Handlers
    • Roles

我们一个一个说, 首先还是我们的Inventory. 这个时候用来定义主机的对不上面说过了, 现在来补充一下.

首先我们可以把几个节点放在一起构成一个组:

1
2
3
4
5
[webservers]
192.168.207.100:8080
192.168.207.200
web1.justin.com
web2.justin.com

如果主机遵循相同的命名规范, 我们还可以简写:

1
2
[webservers]
web[1:10].justin.com

另外, 我们还可以在inventory中定义一些主机变量供playbook使用,这些变量仅仅属于这些主机.

1
2
3
[frontend]
web1.scuec.com http_port=80 maxRequestPerChild=1000
web2.scuec.com http_port=8080 maxRequestPerChild=500

除了主机变量, 还有组变量, 也就是属于一个组的变量:

1
2
3
4
5
6
[webservers]
www[1:10].justin.com

[webservers:vars]
nfs_server=nfs.justin.com
ntp_server=ntp.justin.com

另外, Inventory还可以进行组嵌套:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Group1]
www.douban.com
www.zhihu.com

[Group2]
www.sciencenet.cn
www.weather.com.cn

[target:children]
Group1
Group2

[target:vars]
var=XXX

还有一些属于Inventory的变量, 例如ansible_ssh_port, ansible_ssh_host, ansible_ssh_pass等等.

现在我们来看一个playbook的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
- hosts: webnodes # 指明在哪些主机执行
vars:
http_port: 80
max_client: 256
remote_user: root # 指明是用什么用户的身份执行, 在tasks中也可以定义每一个task单独的执行者
tasks:
- name: ensure apache is at the latest ver.
yum: name=httpd state=latest
- name: ensure apache is running
service: name=httpd state=started
Handlers:
- name: restart apache
service: name=httpd state=restarted

tasks就是任务了, 调用模块完成操作, vars就是声明变量的区域, handlers就是由某事件触发的一些处理器. 最后的roles就是角色之意. 我们继续往后.

先不说后面的处理器, 我们来写一个简单的playbook执行试试.

1
2
3
4
5
6
7
8
9
10
11
- hosts: testservers
remote_user: root
vars:
ntp_server: edu.ntp.org.cn
tasks:
- name: install ntp client
yum: name=ntp state=latest
- name: update time immediately
command: "ntpdate {{ ntp_server }}"
- name: create cron
cron: hour=*/12 job="ntpdate {{ ntp_server }}" name=ntp_sync state=present

很简单的一个剧本, 就是安装ntp接着进行时间同步, 定制定时任务以及显示当前的时间.

我们来执行一下看看:

执行playbook的时候, 我们使用ansible-playbook程序, 最直接的方法就是在后面跟上yml的playbook文件, 也可以使用-e(添加额外变量)等选项. 另外这个地方我把testservers的inventory做了一下小修改:

1
2
3
4
5
6
[testservers]
VM-node[2:3]

[webservers]
192.168.206.22
192.168.206.10

就是这样的效果, 很简单吧:

ansible-playbook2

再来写一个和tinyproxy相关的playbook, 接着引出我们的handler,

web_tinyproxy.yml内容如下:

1
2
3
4
5
6
7
8
9
- hosts: testservers
remote_user: root
tasks:
- name: Install tinyproxy service
yum: name=tinyproxy state=latest
- name: Copy configuration file
copy: src=/etc/tinyproxy/tinyproxy.conf dest=/etc/tinyproxy/tinyproxy.conf
- name: Start tinyproxy service
service: name=tinyproxy state=started

执行效果:

ansible-playbook3

有意思的是, 现在的配置文件监听端口是2333, 我们来验证一下:

1
2
3
4
5
6
[root@VM-node1 ~]# ansible testservers -m shell -a "ss -tnl | grep 2333"
VM-node3 | SUCCESS | rc=0 >>
0 128 *:2333 *:*

VM-node2 | SUCCESS | rc=0 >>
LISTEN 0 128 *:2333 *:*

接着我们修改一下本地的配置, 将端口换成23333:

1
[root@VM-node1 ~]# sed -i 's/2333/23333/' /etc/tinyproxy/tinyproxy.conf 

再次执行一次playbook:

ansible-playbook4

从转型结果我们可以看出来, 服务并没有得到重启. 尽管我们的配置文件是已经重新复制了( 因为文件被修改了 ). 这个时候就需要我们的Handler来根据task的执行情况来触发特定的task.

说来也简单, 当我们的配置文件被修改的时候, 显然是应该重启服务的, 所以我们应该通知一下, 从而去执行特定的操作, handler和我们的tasks同级别, 如果没有人**通知(notify)**它. 就不会执行里面的任务, 通知的方式就是通过notify关键字, 指明哪一个handler, 直接看我们的实例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- hosts: testservers
remote_user: root
tasks:
- name: Install tinyproxy service
yum: name=tinyproxy state=latest
- name: Copy configuration file
copy: src=/etc/tinyproxy/tinyproxy.conf dest=/etc/tinyproxy/tinyproxy.conf
notify:
- restart tinyproxy
- name: Start tinyproxy service
service: name=tinyproxy state=started
handlers:
- name: restart tinyproxy
service: name=tinyproxy state=restarted

这个时候, 我们还需要修改一下配置文件, 比如随便再把端口换一换, 9990吧. 接着再次执行一次:
ansible-playbook6

多了一个RUNNING HANDLER吧. 这个时候看一下, 端口也确实是改变了.

另外, 我们的playbook中还可以使用条件测试, 通过when关键字可以根据具体的一些情况来选择性的执行, 来看这样的一个playbook

1
2
3
4
5
6
7
[root@VM-node1 ~]# cat test.yml 
- hosts: testservers
remote_user: root
tasks:
- name: make a test
copy: content="This is CentOS7" dest=/tmp/version.ansible
when: ansible_distribution_major_version == "7"

执行之后就是这样的了:
ansible-playbook7

其中ansible_distribution_major_version是fact中的变量, 我们可以直接进行引用.

从结果看出来了, 因为我们的两个节点一个是CentOS7, 一个是 CentOS6, 所以仅仅执行了一个.

除了条件测试 , playbook还可以使用迭代, 就像这样:

1
2
3
4
5
6
7
8
- hosts: testservers
remote_user: root
tasks:
- name: add users
user: name={{ item.name }} groups={{ item.groups }} state=present
with-items:
- { name: 'test1', groups: 'wheel' }
- { name: 'test2', groups: 'testgroup' }

其中, item是一个固定的变量, 会从with-items中定义的变量中取值. 不仅如此, 我们的变量可以通过字典的形式来取值.

上面的迭代展开, 其实就像是两个差不多的task. 这样写就更加简洁.

接下来, 我们再来了解一下Jinja2的模板语言. 由于Ansible使用的就是Jinja2, 所以支持Jinja2的各种语法. 不过说真的, 用的地方用的不是很多, 我们使用最多的地方应该就是变量替换了吧.

来看看一个配置文件好了:

1
2
Listen {{ port }}
ServerName {{ servername }}

还记得我们说过的Inventory中的变量吗, 在这里就可以派上用场了:

1
2
3
[webservers]
192.168.206.22 port=80 servername=VM-node3
192.168.206.10 port=8080 servername=VM-node2

这样我们就可以通过模板这个模块来展开变量, 从而达到不同配置的需求.

接着我们看一下Tag功能, 如果只想运行一个playbook中的一个单独的或者特定的task, 我们可以在task中写上tags关键字:

1
2
3
4
5
6
7
8
9
- hosts: webservers
remote_user: root
tasks:
- name: Install httpd service
yum: name=httpd state=latest
- name: Copy configuration file
template: src=/root/templates/httpd.j2 dest=/etc/httpd/conf/httpd.conf
- name: Ensure apache service is running
service: name=httpd state=restarted

假设说每次执行这个playbook他都会从头执行到尾, 但是假设我现在就指向执行其中的复制和重启, 而不像执行安装的话, 怎么办呢? 我们在template和service下加上tags关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- hosts: testservers
remote_user: root
tasks:
- name: Install tinyproxy service
yum: name=tinyproxy state=latest
- name: Copy configuration file
copy: src=/etc/tinyproxy/tinyproxy.conf dest=/etc/tinyproxy/tinyproxy.conf
notify:
- restart tinyproxy
tags:
- skip-install
- name: Start tinyproxy service
service: name=tinyproxy state=started
handlers:
- name: restart tinyproxy
service: name=tinyproxy state=restarted

我们在复制文件的那个地方加入了tags, 接着我们执行:

1
[root@VM-node1 ~]# ansible-playbook web_tinyproxy.yml --tags='skip-install'

ansible-playbook8

看, 只有加上tag的那一个任务才会执行.