IT 自动化工具 Ansible 入门指南

Ansible 是一个非常简单的 IT 自动化引擎,可以完成诸如云端资源调配、配置管理、应用部署、服务协调等众多 IT 自动化任务。它不需要在受控端安装配置额外的 agent 软件,因此部署流程非常简单。
Ansible 的运行机制也并不复杂,它通过 SSH 协议向远程节点推送特定的“小程序”(Ansible modules)并运行,一旦运行完成则将其移除。
同时 Ansible 提供了一种非常简单易懂的语言(YAML)用于编写自动化代码。

一、简介

Ansible 通常会被描述为一个配置管理工具,类似于 ChefPuppetSalt 等。
通过配置管理工具,我们可以确保服务器主机处于某种期望的状态,比如指定的软件包已安装,配置文件中包含正确的值,运行着特定的服务等。
与其他配置管理工具一样,Ansible 也加入了自己的 DSL(领域专用语言)用来描述服务器的状态,即后面会介绍到的用于编写 Playbook 的 YAML 语言。

Ansible 也可以用于部署任务,比如通过源代码生成可执行程序或静态资源并发布到服务器,然后开启远程主机上的对应服务。类似的开源部署工具如 Fabric 等。

还有一种需求叫做流程编排,通常会涉及到多个远程服务器,用于确保各类型的任务以特定顺序执行。比如在开启 Web 服务器之前确保数据库服务处于已经运行的状态。
Ansible 从设计之初即关注多个服务器状态下任务的执行,它有一个非常简单的模块用于控制动作的顺序。

此外,Ansible 还提供众多的模块用来与常见的云服务进行交互,包括 Amazon EC2、Azure、Digital Ocean、Linode 以及任何支持 OpenStack API 的云端服务。

一个简单的 Ansible 运行实例如下图:
在三个服务器上运行 Playbook

二、Ansible 安装

绝大部分 Linux 发行版的软件镜像中都默认包含了 Ansible 的安装包, 可以直接通过对应的包管理器进行安装。如 Ubuntu 系统:
$ sudo apt-get install ansible

Ansible 是基于 Python 语言开发的,因此也可以通过 Python 的包管理器进行安装,命令如下:
$ pip install ansible

我用 VirtualBox 软件搭建了两台 Ubuntu 19.04 虚拟机,配置了内部网络(Internal)的联网方式。
server1 IP 地址为 192.168.1.101,用于安装 Ansible 环境。
server2 IP 地址为 192.168.1.102,作为远程主机进行测试。两者可以相互 ping 通。

SSH 的密钥认证

为了使得每次运行 Ansible 任务时,不需要重复输入远程主机的认证信息,这里先配置好 SSH 连接的无密码认证(即 RSA 密钥认证)。命令如下:

1
2
3
4
$ # 生成 SSH 密钥文件
$ ssh-keygen
$ # 复制公钥文件到远程主机(需要输入密码)
$ ssh-copy-id remoteuser@remoteserver

运行成功后,使用 $ ssh remoteuser@remoteserver 命令进行测试,如可以自动登录,则配置完成。

PS:被控制的远程主机不需要安装任何客户端软件,但是必须确保已安装 Python 且 sshd 服务处于运行状态。如两者未正确安装,可运行以下命令:

1
2
$ sudo apt-get install python
$ sudo apt-get install openssh-server

主机清单(Inventory)

Ansible 通过一个主机清单文件(hosts)存放被管理的服务器的列表。
创建 playbooks 目录作为存放 ansible 脚本和配置文件的项目文件夹,进入该目录并编辑如下 hosts 文件:

1
server2 ansible_ssh_host=192.168.1.102 ansible_ssh_user=skitar ansible_ssh_private_key_file=~/.ssh/id_rsa

通过 hosts 中定义的服务器别名和连接信息访问远程主机并执行 ping 模块:
$ ansible server2 -i hosts -m ping

输出内容如下:

1
2
3
4
5
6
7
server2 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}

如看到类似上面的输出则 Ansible 环境配置完成。

PS:Ansible 主机清单的配置文件默认为 /etc/ansible/hosts。如需使用其他位置的主机清单文件,可以通过 -i 选项手动指定,或者修改 ansible.cfg 配置文件的 inventory 项。

ansible.cfg

ansible.cfg 文件中包含了 Ansible 工具的一些默认配置,该文件通常位于以下位置:

  • 当前目录(./ansible.cfg)
  • Home 目录(~/.ansible.cfg)
  • /etc/ansible/ansible.cfg

在当前目录下创建 ansible.cfg 文件并输入以下内容:

1
2
3
4
5
[defaults]
inventory = hosts
remote_user = skitar
private_key_file = ~/.ssh/id_rsa
host_key_checking = False

由于部分默认选项已配置,此时的 hosts 文件则可以省略用户名和密钥文件等信息,简化为如下形式:

1
server2 ansible_ssh_host=192.168.1.102

使用 ansible 命令时也无需再通过 -i hosts 选项手动指定主机清单文件。即之前的 ping 模块可以这样调用:
$ ansible server2 -m ping

Ad-Hoc 命令

即通过命令行的形式直接调用 Ansible 模块。
Ansible 有着功能丰富的内置模块,可以通过 ansible-doc -l 命令显示所有的内置模块,通过 ansible-doc <module> 命令查看指定模块的介绍以及使用案例。

Ansible 内置的 command 模块可以用来在远程主机上执行 Linux 命令。如执行 uptime 命令:

1
2
3
$ ansible server2 -m command -a uptime
server2 | CHANGED | rc=0 >>
17:45:12 up 5:13, 1 user, load average: 0.12, 0.08, 0.02

其中 -m 用于指定调用的模块,-a 用于指定传递给该模块的选项,此处即某个具体的命令。
由于 command 模块很常用,它其实是不指定任何模块情况下的默认模块。所以上面的命令也可以使用如下形式:
$ ansible server2 -a uptime

当远程主机上执行的命令中包含空格时,需要用引号括起来,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
$ ansible server2 -a "tail /var/log/syslog"
server2 | CHANGED | rc=0 >>
Jul 8 18:18:18 server2 systemd[6250]: Reached target Paths.
Jul 8 18:18:18 server2 systemd[6250]: Listening on GnuPG cryptographic agent and passphrase cache (access for web browsers).
Jul 8 18:18:18 server2 systemd[6250]: Listening on D-Bus User Message Bus Socket.
Jul 8 18:18:18 server2 systemd[6250]: Reached target Sockets.
Jul 8 18:18:18 server2 systemd[6250]: Reached target Basic System.
Jul 8 18:18:18 server2 systemd[1]: Started User Manager for UID 1000.
Jul 8 18:18:18 server2 systemd[1]: Started Session 47 of user skitar.
Jul 8 18:18:18 server2 systemd[6250]: Reached target Default.
Jul 8 18:18:18 server2 systemd[6250]: Startup finished in 75ms.
Jul 8 18:18:19 server2 python3[6304]: ansible-command Invoked with _raw_params=tail /var/log/syslog warn=True _uses_shell=False stdin_add_newline=True strip_empty_ends=True argv=None chdir=None executable=None creates=None removes=None stdin=None

对于某些需要 root 权限才能执行的操作,则需要加上 --become --ask-become-pass 或者 -b -K 选项通过 sudo 执行。如使用 service 模块重启远程主机上的 nginx 服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ ansible server2 -b -K -m service -a "name=nginx state=restarted"
BECOME password:
server2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": true,
"name": "nginx",
"state": "started",
"status": {
"ActiveEnterTimestamp": "Mon 2019-07-08 18:16:58 CST",
"ActiveEnterTimestampMonotonic": "20685653757",
"ActiveExitTimestamp": "Mon 2019-07-08 18:16:57 CST",
"ActiveExitTimestampMonotonic": "20685035019",
"ActiveState": "active",
...

其他常用的内置模块还有 apt、copy、user 等。

如通过 apt 模块管理远程主机上的软件包:

1
2
3
4
5
6
7
8
9
10
11
12
$ ansible server2 --b -K -m apt -a "name=nginx state=latest"
BECOME password:
[WARNING]: Could not find aptitude. Using apt-get instead

server2 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"cache_update_time": 1562598780,
"cache_updated": false,
"changed": false
}

通过 copy 模块复制本地文件到远程主机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ansible server2 -m copy -a "src=ansible.cfg dest=/home/skitar/ansible.cfg owner=skitar mode=644"
server2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": true,
"checksum": "2f4f9975ef1875adcc2a299d3962f70630f49965",
"dest": "/home/skitar/ansible.cfg",
"gid": 1001,
"group": "skitar",
"mode": "0644",
"owner": "skitar",
"path": "/home/skitar/ansible.cfg",
"size": 109,
"state": "file",
"uid": 1000
}

通过 user 模块管理远程主机上的用户(需要先通过 openssl 命令生成密码,因为 user 模块的 password 参数只接受加密后的值):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ echo ansible | openssl passwd -1 -stdin
$1$ZyNqkbXH$i.4R0EDQZV.zu8akyJAu10
$ ansible server2 -b -K -m user -a 'name=starky password="$1$ZyNqkbXH$i.4R0EDQZV.zu8akyJAu10" shell=/bin/bash'
BECOME password:
server2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"append": false,
"changed": true,
"comment": "",
"group": 1002,
"home": "/home/starky",
"move_home": false,
"name": "starky",
"password": "NOT_LOGGING_PASSWORD",
"shell": "/bin/bash",
"state": "present",
"uid": 1001
}

三、Playbooks

Playbooks 即 Ansible 用于执行自动化配置的脚本文件。它使用非常简单的 YAML 语言描述期望达到的状态,YAML 之于 JSON 类似于 Markdown 之于 HTML 。

一个简单的用于配置 nginx 站点的 playbook 示例(web-notls.yml)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- name: Configure webserver with nginx
hosts: webservers
become: True
tasks:
- name: install nginx
apt: name=nginx update_cache=yes

- name: copy nginx config file
copy: src=files/nginx.conf dest=/etc/nginx/sites-available/default

- name: enable configuration
file: >
dest=/etc/nginx/sites-enabled/default
src=/etc/nginx/sites-available/default
state=link

- name: copy index.html
template: src=templates/index.html.j2 dest=/var/www/html/index.html mode=0644

- name: restart nginx
service: name=nginx state=restarted

编辑 nginx 配置文件,即 playbook 中 copy 模块的 src 选项指定的文件(files/nginx.conf),内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80 default_server;

root /var/www/html;
index index.html;

server_name 192.168.1.102;

location / {
try_files $uri $uri/ =404;
}
}

编辑 nginx 站点的主页文件,即 playbook 中 template 模块的src 选项指定的文件(templates/index.html.j2),内容如下:

1
2
3
4
5
6
7
8
9
10
<html>
<head>
<title>Welcome to ansible</title>
</head>
<body>
<h1>nginx, configured by Ansible</h1>
<p>If you see this, Ansible successfuly installed nginx.</p>
<p>Current time is {{ now() }}</p>
</body>
</html>

编辑 Inventory (主机清单)即hosts 文件,创建 webservers 主机组

1
2
[webservers]
server2 ansible_ssh_host=192.168.1.102

主机组中可以包含一个或多个远程主机,便于同时管理多个远程节点。

上述配置完成后,通过 ansible-playbook web-nolts.yml -K 命令运行 playbook,输出如下:

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
$ ansible-playbook web-notls.yml -K
BECOME password:

PLAY [Configure webserver with nginx] **********************************************************

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

TASK [install nginx] ***************************************************************************
[WARNING]: Could not find aptitude. Using apt-get instead

ok: [server2]

TASK [copy nginx config file] ******************************************************************
changed: [server2]

TASK [enable configuration] ********************************************************************
ok: [server2]

TASK [copy index.html] *************************************************************************
changed: [server2]

TASK [restart nginx] ***************************************************************************
changed: [server2]

PLAY RECAP *************************************************************************************
server2 : ok=6 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

注意 ansibe-playbook 命令的 -K 选项(即 --ask-become-pass),由于 playbook 中的部分任务需要 root 权限执行,加上 -K 选项后,执行 playbook 时会出现 BECOME password: 提示用于输入 sudo 密码。否则会报错。

playbook 执行成功后,使用 curl 192.168.1.102 命令访问 server2 上刚刚配置的 nginx 站点,输出如下:

1
2
3
4
5
6
7
8
9
10
11
$ curl 192.168.1.102
<html>
<head>
<title>Welcome to ansible</title>
</head>
<body>
<h1>nginx, configured by Ansible</h1>
<p>If you see this, Ansible successfuly installed nginx.</p>
<p>Current time is 2019-07-09 00:28:46.484053</p>
</body>
</html>

参考资料

Ansible: Up and Running, 2nd Edition