DevOps基础(2)--Ansible自动化运维工具

Shell脚本能够为我们提供一部分的系统运维功能, 例如定时任务, 由Jenkins所管理的边缘触发任务等等, 但是如果想要对多台服务器进行管理和运维, 就需要Ansible来协助完成。

1. Ansible是什么?

Ansible是一个由Python语言编写的自动化运维工具, 底层基于SSH框架, 帮助运维以及开发人员进行批量的服务器管理。

假设我们有10台服务器, 需要查看当前每台服务器的磁盘使用状况, 如果说采用传统的SSH登录, 然后输入密码, 进入远程服务器, 使用df -h来查看磁盘的使用状态的话, 这个过程需要持续10次。 就算我们将客户端的公钥上传至服务器使得我们可以免密登录远程服务器, 这个过程仍然是很花时间的。

此时Ansible就发挥其作用了, 由于Ansible是基于SSH框架所实现的, 所以Ansible可以批量的进行远程服务器的SSH连接, 在该连接之上执行df -h, 并将结果返回给客户端。 而且Ansible还提供了多进程的方式进行工作, 进一步的提升执行效率以及节省运维人员的时间。

2. Ansible的安装与配置

正如前面所提到的, Ansible是由Python所编写的工具, 那么自然需要相应的Python环境或者是Python的虚拟环境。 在Linux操作系统中, 本身就包含了python2.7以及python3.6+的环境, 所以如果不使用虚拟环境的话, 可以直接使用

1
pip install ansible

进行安装。 在安装完成后, 目录/etc/ansible即会生成。 在该目录下, 只有两个文件:

  • ansible.cfg: ansible的全局配置文件
  • hosts: ansible默认的服务器配置文件, 有时又称为资产清单文件

两个文件的配置都很简单且易懂。 对于hosts文件, 定义的格式以及具体的实例如下:

1
2
3
4
5
[group-name]
server-ip/server-domain ansible_ssh_user=USERNAME

[local_server]
192.168.1.106 ansible_ssh_user=smart

在上面的实例中, 定义了local_server这个主机组, 在该组下只有一台主机, IP地址为192.168.1.106, 并定义了ansible在当前主机执行任务时的用户名称为smart。

而对于ansible.cfg文件, 更多的是定义默认的服务器配置文件路径, 执行的并发数, 以及客户端的公钥等。

为了能够使用公钥登录服务器, 得先将客户端的公钥上传至服务器的authorized_keys文件中, 这个过程可以交给ssh来自动完成:

1
2
ssh-copy-id user@server-ip
输入密码即可

将当前客户端的公钥地址配置在ansible.cfg中, 其余的配置保持默认即可:

1
ansible_ssh_private_key_file=/home/smart/.ssh/id_rsa.pub

3. ansible的模块

对于一个任务而言, 需要的要素就是谁在哪儿做什么。 在Ansible自动化管理中, 当然是由Ansible来做了, 任务执行的范围即定义在hosts服务器配置文件之中, 剩下的就是定义具体的任务了。

Ansible提供了两种任务定义的方式, 一种叫ad-hoc, 一种叫playbooks。 ad-hoc就像是我们在Terminal中执行shell命令一样, 是一种临时的、无法保存成文件的任务执行方式。 而playbooks则是永久的、能够多次执行并保存成文件的任务, 相当于Shell脚本。

在前一篇Shell脚本的文章有提到, Shell脚本的核心是一条一条的Linux命令, Shell语言只是提供粘合剂将它们组合形成一个整体。 对于Ansible而言, 其核心就是模块, 而playbook则是将多个模块组合在一起。

那么Ansible模块又是什么? Ansible模块其实就是一个又一个的Python脚本, 为用户提供各种各样功能的脚本。 例如使用最为广泛的copy模块, 其作用是将本地的文件拷贝至服务器的目标目录中。

如果我们自己来完成文件拷贝的需求, 可以使用

1
scp file_path user@server-ip:server_path

如果是对很多台服务器进行文件拷贝的话, 可以使用一个Shell脚本来完成:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
set -xe
exec &> /home/smart/shell/file-copy.log

USER="admin"
LOCAL_PATH="/home/smart/monitor"
SERVER_PATH="/home/monitor"

for server in server_list
do
scp -R ${LOCAL_PATH} ${USER}@${server}:${SERVER_PATH}
done

而这么多行的Shell脚本, Ansible使用一个copy模块就可以完成, 这就是Ansible模块的威力。

如果使用Ansible命令行的模式来完成文件拷贝的任务, 只需要一行命令:

1
ansible all -m copy -a "src=/home/smart/monitor dest=/home/monitor" -f 6

all表示对hosts文件中的所有主机执行任务, 也可以执行组名, 例如local_server-m指定模块名称, 这里选用copy模块。 -a添加模块所需要的参数, 而-f则指定并行的数量, 通常会和客户端的CPU核心数相同。

回到文章开始的地方, 对10台服务器执行df -h命令该怎么做? 由于这是一个Shell命令, 所以理所当然的使用shell模块, 传递给该模块的参数就是所要执行的命令:

1
ansible local_server -m shell -a "df -h"

将会得到这样的结果:

1
2
3
4
5
6
7
8
192.168.1.106 | SUCCESS | rc=0 >>
Filesystem Size Used Avail Use% Mounted on
udev 7.8G 0 7.8G 0% /dev
tmpfs 1.6G 2.2M 1.6G 1% /run
/dev/sdb3 95G 34G 57G 38% /
tmpfs 7.9G 265M 7.6G 4% /dev/shm
tmpfs 5.0M 4.0K 5.0M 1% /run/lock
tmpfs 7.9G 0 7.9G 0% /sys/fs/cgroup

Ansible和Shell一样, 提供了非常多封装了各种各样功能的模块, 到目前为止, Ansible大概提供了约2100个模块, 几乎涵盖了服务器运维的所有方面。 所以我认为这进一步地印证了在Ansible中, 最为重要的并不是如何编写playbooks, 而是如何在这2100个模块中找到自己想要的, 并正确的使用它。

Ansible所有模块文档地址: ansible list of modules

4. Ansible playbooks

通常来讲ad-hoc模式常常拿来做测试, 例如:

1
ansible all -m ping

来测试服务器的配置以及服务器的可达状态, 或者是说批量的添加用户并分配用户组等小功能。 在自动化部署这一需求下, 仍然会使用playbooks来组合测试、QA以及生产环境的相关任务。

在playbooks下有两个很重要的概念: roles, tasks。 roles是一个或多个任务(task)的集成, 表示当前的任务所运行的环境。 通常都会分为dev, test, QA, prod, 主要用于环境区分。 而tasks则是变量列表和具体的任务列表的集成, 代表了真正要执行的任务。 所以, 一个playbooks的结构往往是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
├── dev.yaml
├── inventory.cfg
├── prod.yaml
├── roles
│   ├── dev
│   │   ├── tasks
│   │   │   └── main.yaml
│   │   └── vars
│   │   └── main.yaml
│   ├── prod
│   │   ├── tasks
│   │   │   └── main.yaml
│   │   └── vars
│   │   └── main.yaml
│   └── test
│   ├── tasks
│   │   └── main.yaml
│   └── vars
│   └── main.yaml
└── test.yaml

看起来会很复杂, 其实非常的简单。 在根目录下, dev|test|prod.yaml是playbooks的主要入口文件, 而reoles目录下的dev|test|prod目录中则保存着对应环境的环境变量以及所要执行的任务。 inventory.cfg则保存着当前项目所设计的服务器资产清单, 包括开发, 测试, QA以及生产服务器的分组和ip。

4.1 入口文件test.yaml

这里以test.yaml入口文件为例, 该配置文件其实非常的简单:

1
2
3
- hosts: test
roles:
- test

没有更多内容了, 首先指定测试环境所用到的服务器组名, 其次指定测试环境的roles目录。 playbooks的运行是从入口文件开始的:

1
ansible-playbook -i inventory.cfg test.yaml
4.2 任务列表文件

对于一个task.yaml而言, 也非常简单: 组合多个模块。

1
2
3
4
5
6
7
- name: print current date
shell: date

- name: copy file
copy:
src: /home/monitor/test.log
dest: /home/monitor

只不过是将ad-hoc模式下变量传递的方式改写成yaml文件的格式而已, 本质上仍然是对模块的应用。 但是这种任务编写的方式提供了额外的拓展功能, 例如chdir改变当前任务的工作目录, register将当前任务的执行结果保存至某一个变量中, 可以用于后续的DEBUG。

1
2
3
4
5
6
- name: print current date
shell: date
args:
chdir: /home/monitor
register: date_result
-debug: var=date_result
4.3 变量列表文件

出于编码的最佳规范, 一些变量或者是常量最好是保存至某一个文件中, 而后进行引用。 在playbooks中, 我们只需要将变量写入vars/main.yaml, 以及在tasks/main.yaml中使用即可, 至于中间是怎么工作的, 可以完全不用关心。

1
2
3
4
5
6
7
# vars/main.yaml
home_path: /home/smartkeyerror

# tasks/main.yaml
- name: ls home_path
shell: ls "{{home_path}}"
# 如果使用{{}}进行变量引用的话, 必须添加""

既然变量文件使用yaml的数据格式进行写入, 那么就可以使用dict数据结构来进行变量引用:

1
2
3
4
5
6
7
8
# 定义
foo:
field1: A
field2: B

# 引用
foo["field1"]
foo.field1

这种引用方式更多的是在模板(Template)中进行使用, task.yaml更多的是直接引用简单的变量。

5. Jinja2模板

如果使用过Django或者是Flask等Python Web框架的小伙伴对Jinja2模板一定不会很陌生。

Ansible中的Jinja2模板并不是用于HTML文件的数据填充与渲染, 而是当做配置文件的模板进行远程配置文件的填充。

举一个并不是很恰当的例子, 但是能够说明问题。

假设有如下Nginx配置文件:

1
2
3
4
5
6
7
server {
listen 80;
server_name gitlab.zero.com;
location / {
proxy_pass http://127.0.0.1:8181;
}
}

一个非常简单gitlab端口转发配置, 现在我想要将server_name配置项的值放入到变量文件中, 然后将该配置文件上传至服务器中。 当然我们可以使用copy模块来完成, 但是copy模块没有办法在拷贝的文件中填充变量, 这个时候就需要使用到模板。

1
2
3
4
5
6
7
8
9
# nginx.gitlab.j2

server {
listen 80;
server_name {{server_name}};
location / {
proxy_pass http://127.0.0.1:8181;
}
}
1
2
3
4
- name: generate gitlab nginx config to server
template:
src: ./templates/nginx.gitlab.j2
dest: /etc/nginx/conf.d/gitlab.conf

模板文件通常置于playbooks/templates目录中。 当执行完该playbooks之后, Ansible将会把填充好变量数据的Nginx配置文件置于应有的服务器目录下。

当然, Jinja2模板的功能远不止于此, 还可以在模板文件中添加条件判断以及数据的迭代:

1
2
3
4
5
6
7
{% for key, value in environment.items() %}
{{key}}: "{{value}}"
{% endfor %}

{% if used_kafka %}
...
{% endif %}

6. Ansible Vault对变量文件进行加密

有时为了保证变量文件的安全性, 以及在网络传输时的隐蔽性, 通常都需要对变量文件进行加密, 只有在使用变量文件时才对其进行解密。

ansible-vault命令就是Ansible提供给我们对文件进行加密的工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 变量文件只有一个变量配置
smart@Zero:~$ cat main.yaml
server_name: gitlab.zero.com
# 使用ansible-vault encrypt file进行加密
smart@Zero:~$ ansible-vault encrypt main.yaml
New Vault password:
Confirm New Vault password:
Encryption successful
# 再次查看main.yaml
smart@Zero:~$ cat main.yaml
$ANSIBLE_VAULT;1.1;AES256
31356236643435613539353331383734376438373966393064666538636635643934663736636437
3961316333633462376234386437346462333539393039310a663932663832306464316435646539
36636665366233343266386466313831343165303238623163373237313764333363373662303862
3561646430623230620a663964363462366435386139383666356330333336343535373336346232
36386236373639666633666130653861636530613034623635626135313130366632

在这里使用了手工输入密码的方式进行加密与解密, 除此之外还可以将密码写入文件, 在进行加密解密时执行文件:

1
2
3
4
5
6
7
8
9
# 生成密码文件
smart@Zero:~$ echo "mypassword" > .password.conf
# 修改文件权限以及所属用户组
smart@Zero:~$ sudo chmod 600 .password.conf
smart@Zero:~$ sudo chmod 600 .password.conf
# 加密
smart@Zero:~$ sudo ansible-vault encrypt --vault-id .password.conf main.yaml
# 解密
smart@Zero:~$ sudo ansible-vault decrypt --vault-id .password.conf main.yaml

7. 小结

以上就是Ansible的核心内容, 不会特别的复杂, 但是由于模块众多的原因, 还是需要花时间去阅读具体的模块文档。

可以看到, Ansible真的就只是一个能够在多台主机上执行同一个任务的运维工具而已。 而在我看来, 运维最重要的并不是工具, 而是运维的体系。

在接下来的文章中可以看到, 当我们使用了Docker容器以后, 一个真正的自动化运维体系才算刚刚开始。 如果更进一步地使用Kubernetes的话, 甚至可以不需要Ansible。