Linux 自动化配置工具—— Ansible
在开源领域 Linux 风声水起,缘于一众富有想象力的开源工具。在 Linux 服务器的自动化管理方面,也有众多选择,例如 Puppet、Chef、SaltStack、Ansible。前三个管理工具采用的是服务端/客户端模式,换而言之,既要配置客户端又要配置服务端才能让它们工作。Ansible 与它们不同,它充分利用 Linux 服务器的现有设施。使用 Ansible 无需安装服务端和客户端,只要配置好 OpenSSH 服务即可。Ansible 的配置文件采用的是易读格式,例如,Ansible 的主机文件使用 INI 格式,支持分组,能够指定模式;而 Playbook 则是 YAML 格式。这比 Puppet 等工具使用独有配置文件格式要容易读写。当前使用 Ansible 较为知名的用户包括 Fedora、Rackspace、Evernote 等等。
1、安装 Ansible
Ansible 能够安装到 Linux、BSD、Mac OS X 等平台,Python 版本最低要求为 2.6。大多数 Linux 发行版可以通过包管理器安装 Ansible。例如,对于 Red Hat 系来说,需要配置 EPEL 源,然后执行
$ sudo yum install ansible
Debian 系只需
$ sudo apt-get install ansible
Gentoo 可执行
$ sudo emerge -avt ansible
如果所用系统的软件包仓库中找不到 Ansible 或者系统本身没有软件仓库,那么也可以通过 pip 来安装 Ansible
$ pip install --user ansible
上述命令行执行同时也会安装 paramiko、PyYAML、jinja2 等 Python 依赖库。 现在来看看 Ansible 是否能正常工作:
$ ansible 127.0.0.1 -m ping -u james -k
请注意此地连接的是本地主机,为保证 ansible 正常工作,需在本地配置 OpenSSH 服务并以允许密码认证方式登录,同时还需创建 james 账户。
2、Ansible 的主机文件
Ansible 的主机文件用来定义你要管理的主机,系统级主机文件的默认位置在 /etc/ansible/hosts。当然也可以设置 Ansible 的用户级主机文件,它可覆盖系统级主机文件,具体来说就是在用户家目录创建 Ansible 配置文件并在其中指定用户级主机文件的路径,例如
$ nano -w ~/.ansible.cfg [defaults] # some basic default values... # ansible 1.8 or ago #hostfile = $HOME/.ansible/hosts # ansible 1.9 or later inventory = $HOME/.ansible/hosts $ mkdir -p ~/.ansible $ touch ~/.ansible/hosts
最后,还可通过 ansible 命令行选项 -i 指定主机文件的路径,它能覆盖前面两者的配置。主机文件是一些被管理的机器的 IP 或域名组成的。这些主机可以分组存放,组名可以使用 [] 指定,未分组的机器需保留在主机文件的顶部,主机文件中可以指定远程主机的 SSH 端口。例如:
202.121.111.8 [vps] 202.121.111.10 [web] 202.121.111.18 172.16.10.124:5555 [db] 202.121.111.19
同时,分组也能嵌套:
[vps:child] web db
此外,也可以通过数字和字母模式来指定一系列连续主机,如:
202.121.111.[1:3] # 相当于 202.121.111.1、202.121.111.2、202.121.111.3 [a:c].exmaple.org # 相当于 a.example.org b.example.org、c.example.org
3、Ansible 的 Ad-Hoc 模式
Ansible 的强大很大程度上体现在它 Playbook 上,后者基本上就是一些写好的 "Ansible 脚本"。不过在正式介绍它之前,先从简单的 ansible 命令行开始,这就是 ansible 的 Ad-Hoc 模式。Ad-Hoc 模式与在 Bash Shell 中执行单行命令差不多,用来快速完成简单任务。先来看看 ansible 的命令行选项:
- -i,--inventory-file=:指定主机文件,不指定的话默认依顺序使用用户级、系统级的主机文件
- all:针对主机文件中定义的所有主机执行,也可以指定组名或模式
- -m,--module-name=:指定所用的模块,不指定的话默认使用 command 模块
- -a:指定所用模块的参数,不指定的话所用模块不使用参数
- -u,--user=:指定远端机器的用户,若不指定,默认是执行 ansible 的本地用户账户名
- -c,--connection=:指定远端主机用户登录时的认证模块,通常有 smart、paramiko、sshpass,默认是 smart
- -f,--forks=:参数让 ansible 在主机上并行运行指令
- -k,--ask-pass:询问 SSH 密码;若使用 SSH 密匙,请不要使用 -k
通常,执行 Ansible 需要 SSH 密码认证,提示 SSH 密码:
$ ansible all -m ping -u root --ask-pass
在一些机器上使用 --ask-pass 会需要指定连接方式,例如:
$ ansible all -m ping -u root --ask-pass -c paramiko
当然也可以使用模块 sshpass,然而 sshpass 并不总是在标准的仓库中提供,因此 paramiko 可能更为简单。需要注意,操作很多主机时需逐个输入账户名与密码,这些都是相当麻烦的。为简单起见,可做下述配置
$ sudo nano -w /etc/ansible/hosts #定义被控主机 [webservers] 172.16.10.123 ansible_ssh_user=root ansible_ssh_pass=centos 172.16.10.125 ansible_ssh_port=5555 ansible_ssh_user=root ansible_ssh_pass=centos web1 ansible_ssh_host=172.16.10.126 ansible_ssh_port=5555 ansible_ssh_user=root ansible_ssh_pass=centos [dbservers] 172.16.10.125 ansible_ssh_user=root ansible_ssh_pass=centos
下面再看看远端主机的 uptime:
$ ansible webservers -a 'uptime'
此处省略了 -i,ansible 使用默认的主机文件;webservers 代表默认主机文件中的某个主机组;也省略了 -m,ansible 默认使用 command 模块;-a 指定模块的参数,即执行 uptime 命令。如果被管理端主机的 Python 为 2.4,那么需要 python-simplejson 包,可以通过以下命令在所有 CentOS 主机上安装它:
$ ansible webservers -m raw -a 'yum -y install python-simplejson'
此处使用 Ansible 的 raw 模块,它的作用相当于直接在 OpenSSH 中执行 -a 后面的命令。再如利用 Ansible 可执行系统更新
$ ansible webservers -m raw -a 'yum update -y'
或者安装某些服务
$ ansible webservers -m raw -a 'yum -y install httpd' $ ansible webservers -m raw -a "service httpd start" $ ansible webservers -m raw -a "chkconfig httpd on"
实际上,将 OpenSSH 密码放入主机文件是非常不安全的,强烈推荐为 Ansible 设置 SSH 公钥认证。除了密码安全问题之外,对于非超级用户登录远程主机并执行超级用户命令,需要开启 su 或 sudo 权限。例如,vps 组中的账户 james 配置了 sudo 权限,可用该账户升级系统,例如
$ ansible vps -m raw -a "yum upgrade -y" -u james -k -b --become-user=root --become-method=sudo
或者
$ ansible vps -m raw -a "sudo yum upgrade -y" -u james -k
4、Ansible 的帮助文档
Ansible 的强大功能均是由它众多模块提供的,诸如为 Linux 创建用户及组、安装软件包、分发配置文件、管理服务等等。要想了解 Ansible 模块的全部功能,必要的文档是不可少的。在命令行下,可通过 ansible-doc 查询模块文档,如:
$ ansible-doc raw
列出模块 raw 中相关指令
$ ansible-doc -s raw
要查看所有安装模块
$ ansible-doc -l
若 Ansible 提供了模块功能,尽量使用模块,例如 Red Hat 系有 yum 模块,因此前述 CentOS 上的操作可替换为
$ ansible webservers -m yum -a 'name=* state=latest' $ ansible webservers -m yum -a 'name=httpd state=latest' $ ansible webservers -m service -a 'name=httpd state=started' $ ansible webservers -m service -a 'name=httpd enabled=yes'
5、设置 SSH 公钥认证
现在让我们创建和配置 SSH 公钥认证,以便省去 -c 和 --ask-pass 选项。首先,生成密匙对:
$ ssh-keygen -t rsa
上述命令会在当前用户的 ~/.ssh 中生成密匙对,假设当前用户为 james。显然有很多种方式把 SSH 公匙放到远程主机上,既然要介绍 ansible 的用法,就用它来完成这个操作吧:
$ ansible all -m copy -a "src=/home/james/.ssh/id_rsa.pub dest=/tmp/id_rsa.pub" --ask-pass -c paramiko
下一步,把公钥文件添加到远程服务器里。输入:
$ ansible all -m shell -a "cat /tmp/id_rsa.pub >> /root/.ssh/authorized_keys" --ask-pass -c paramiko
由于把密匙放入超级用户目录,但又没有指定 -u 选项,因此默认登录远程主机的账户是执行 ansible 的本地账户,通常会因权限不够执行会失败。不要急,需要用 root 来执行这个命令,加上 -u 参数即可:
$ ansible all -m shell -a "cat /tmp/id_rsa.pub >> /root/.ssh/authorized_keys" --ask-pass -c paramiko -u root
刚才演示了通过 ansible 来传输文件的操作。事实上 ansible 有一个更加方便的内置 SSH 密钥管理支持:
$ ansible all -m authorized_key -a "user=james key='{{ lookup('file', '/home/james/.ssh/id_rsa.pub') }}' path=/home/james/.ssh/authorized_keys manage_dir=no" --ask-pass -c paramiko
至此,公钥已经设置好了。试着随便跑一个命令,比如 hostname,希望不会被提示要输入密码
$ ansible all -m shell -a "hostname" -u root
现在可以用 root 来执行命令,并且不会被输入密码的提示干扰了。最后,把 /tmp 中的公钥文件删除:
$ ansible all -m file -a "dest=/tmp/id_rsa.pub state=absent" -u root
下面来做一些更复杂的事情,而且无须输入密码。例如:
$ ansible all -m yum -a "name=httpd state=latest" -u root
利用 Ansible 的 yum 模块安装好了最新版的 Apache 服务器。实际上,上一条命令可以用下述 Playbook 替代:
--- - name: test.yaml # Playbook 的名称 hosts: webservers # Playbook 所作用的主机列表 remote_user: root # 远程主机的用户名 tasks: # 任务列表 - yum: name=httpd state=latest # 使用 yum 模块执行任务一
现在有一个简单的 Playbook 了,可以这样运行它:
$ ansible-playbook test.yaml -f 10
既然 Playbook 的操作如此简单,不妨把导入 SSH 公钥的操作从 ansible 命令行转移到 Playbook 中,这将在设置新主机的时候提供很大的方便,甚至让新主机直接可以运行一个 Playbook。为了演示,把之前的公钥例子放进一个 Playbook 里:
--- - hosts: webservers # Playbook 所作用的主机列表 remote_user: james # 执行任务的远程主机用户名 sudo: yes # 启用 sudo 权限执行任务 tasks: - authorized_key: user=root key="{{ lookup('file', '/home/james/.ssh/id_rsa.pub') }}" path=/root/.ssh/authorized_keys manage_dir=no
在准备开始写更多更复杂的 Playbook 之前,另一个值得考虑的事情是,引入版本管理器可以有效节省时间。虽然需要管理的 Linux 的服务器需求会不断变化,然而并不需要在每次机器发生变化时都重新写一个 Playbook,只需要更新相关的部分并提交这些修改。这里强烈推荐版本管理器 Git 来管理各类 Playbook 脚本。
6、使用 Playbook 管理复杂任务
对于需反复执行的、较为复杂的任务,我们可以通过定义 Playbook 来搞定。Playbook 是 Ansible 真正强大的地方,它允许使用变量、条件、迭代以及模板,也能通过角色及包含指令来重用既有内容。我们来看一个简单的例子,该例子在远端机器上创建一个新的用户:
$ nano -w user.yaml --- - name: create user # Playbook 的名称 hosts: vps # 执行任务的主机表 user: root # 执行任务的远程主机用户 gather_facts: false # 不获取远程主机的信息 vars: # 自定义变量列表 - user1: "toy" # 第一个自定义变量 tasks: # 任务列表 - name: create {{ user1 }} on vps # 第一个任务的名称 user: name="{{ user1 }}" # 任务格式:"action: module options" 或 "module: options" - name: template configuration file # 第二个任务的名称 template: src=template.j2 dest=/etc/foo.conf notify: # 可用于在每个 play 的最后被触发,避免多次有改变发生时每次都执行指定的操作,取而代之,仅在所有的变化发生完成后一次性地执行指定操作。 - restart memcached # 调用 handler 中定义的操作名 - restart apache handlers: # 用于当关注的资源发生变化时采取一定的操作。handler 是 task 列表,这些 task 与前述的 task 并没有本质上的不同。 - name: restart memcached # 第一个 handler 名 service: name=memcached state=restarted - name: restart apache # 第二个 handler 名 service: name=httpd state=restarted
首先,我们给 Playbook 指定了一个名称;接着,通过 hosts 让该 Playbook 仅作用于 vps 组;user 指定以 root 帐号执行,Ansible 也支持普通用户以 sudo 方式执行任务;gather_facts 的作用是搜集远端机器的相关信息,稍后可通过变量形式在 Playbook 中使用;vars 用于定义变量,也可单独放在文件中;tasks 指定要执行的任务,其中 notify 用来监视资源变化,据此调用相关的 handlers;handlers 类似于 tasks,但它仅根据 notify 信号来决定是否执行相应的 handler。要执行 Playbook,可以敲入:
$ ansible-playbook user.yaml
6.1、Ansible 中的变量
正如前述例子中所看到的,Ansible 的 Playbook 中是可以定义变量的。Ansible 的变量名仅能由字母、数字和下划线组成,且只能以字母开头。Ansible 本身也保存了一些变量,它们称之为 facts。facts 是由正在通信的远程目标主机发回的信息,这些信息被保存在 Ansible 变量中。要获取指定的远程主机所支持的所有 facts,可使用如下命令进行:
$ ansible vps -m setup
通过上述命令,Ansible 会存储很多远程主机相关的变量供 Playbook 使用,例如 ansible_os_family 等。 Ansible 可以通过角色传递变量。当给一个远程主机应用角色的时候可以传递变量,然后在角色内使用这些变量。例如:
--- - hosts: webservers roles: # 角色列表 - common # 第一个角色 - { role: wordpress, dir: '/var/www/htdocs/blog', port: 8080 } # 第二个角色
Ansible 通过 register 把任务的输出定义为变量,然后用于其他任务,示例如下:
--- - tasks: - shell: /usr/bin/foo register: foo_result # 将该任务的输出作为 foo_result 变量, ignore_errors: True
除此之外,Ansible 可以通过命令行传递变量。在运行 Playbook 的时候,通过命令行可以传递一些变量供 Playbook 使用。例如:
$ ansible-playbook test.yml --extra-vars "hosts=www user=mageedu"
上述命令行传入了 hosts 与 user 变量可覆盖 Playbook 中的相关定义。
6.2、Ansible 中的条件
如果需要根据变量、facts 或此前任务的执行结果来作为某 task 执行与否的前提时要用到条件测试。只需在 tasks 后添加 when 子句即可使用条件测试;when 语句支持 Jinja2 表达式语法。when 语句中还可以使用 Jinja2 的大多 filter,例如要忽略此前某语句的错误并基于其结果(failed 或者 success)运行后面指定的语句,可使用类似如下形式:
--- - tasks: - command: /bin/false register: result # 注册器 ignore_errors: True # 忽略错误信息 - command: /bin/something when: result|failed # 第一条命令失败时(result 为 failed 时),才执行第二条命令 - command: /bin/something_else when: result|success #(result 为 success 时),才执行 - command: /bin/still/something_else when: result|skipped # skipped:已经执行过跳过执行
此外,when 语句中还可以使用 facts 或 Playbook 中定义的变量。例如:
--- - tasks: - name: "shutdown Red Hat flavored systems" command: /sbin/shutdown -h now when: ansible_os_family == "RedHat" #条件测试,ansible_os_family 来自 facts
6.3、Ansible 中的迭代
在迭代中只能使用 item 变量,变量引用为{{ }}两个大括号,变量两边有空格。当有需要重复性执行的任务时,可以使用迭代机制。其使用格式为将需要迭代的内容定义为 item 变量引用,并通过 with_items 语句来指明迭代的元素列表即可。例如:
--- - name: add several users user: name={{ item }} state=present groups=wheel stae=present # 用户得存在且加入 wheel 组中 with_items: - testuser1 # 分别使用 testuser1 替换 name={{item}} 中的 item 项 - testuser2
上面语句的功能等同于下面的语句:
--- - name: add user testuser1 user: name=testuser1 state=present groups=wheel - name: add user testuser2 user: name=testuser2 state=present groups=wheel
事实上,with_items 中可以使用元素还可为 hashes,例如:
--- - name: add several users user: name={{ item.name }} state=present groups={{ item.groups }} with_items: - { name: 'testuser1', groups: 'wheel' } - { name: 'testuser2', groups: 'root' }
迭代还支持列表,使用 with_flattened 语句。例如
--- - vars: - packages_LNMP: # 定义列表 - [ 'nginx', 'mysql-server', 'php-fpm' ] - tasks: - name: Install LNMP yum: name={{ item }} state=present with_flattened: - packages_LNMP # 再在迭代中引用它
6.4、Ansible 中的角色
Playbook 还可以构造非常重要的文档目录组织结构,也即被称为角色,一个带有角色的 Playbook 的结构如下
$ tree /root/lamp /root/lamp/ ├── hosts ├── group_vars │ └── all ├── roles │ ├── Apache │ │ ├── defaults │ │ │ └── main.yml │ │ ├── files │ │ │ └── httpd.conf │ │ ├── handlers │ │ │ └── main.yml │ │ ├── meta │ │ │ └── main.yml │ │ ├── tasks │ │ │ ├── delete_httpd.yml │ │ │ └── main.yml │ │ ├── templates │ │ │ └── file.j2 │ │ └── vars │ │ └── main.yml │ ├── MariaDB │ │ ├── tasks │ │ │ └── main.yml │ │ ... │ └── PHP │ ├── tasks │ │ └── main.yml │ ... └── site.yml
以上显示了名为 lamp 的 Playbook 的目录结构。其中 site.yml 是该 Playbook 的主文件,,它里面的可能写法是
--- - name: install LAMP hosts: all user: root roles: - Apache # 调用 roles/Apache/tasks/main.yml - MariaDB # 调用 roles/MariaDB/tasks/main.yml - PHP # 调用 roles/PHP/tasks/main.yml
hosts 是该 Playbook 的主机文件,它只在该 Playbook 中有效;roles 目录中有三个角色 Apache、MariaDB 与 PHP。下面再来看看角色 Apache 中各个子目录或文件的作用:
- defaults:当前 role 中的各种变量,主要存放在 defaults/main.yml 中,也可以通过 include 包含更多。它具有最低级别,可被 vars 等处的变量覆盖;
- files:存放各种文件,Ansible 默认会到这里目录去找文件,对应 task 里面的 copy 或 script 等模块;
-
tasks:tasks:任务列表主要存放在 tasks/main.yml 中,也可以通过 include 包含更多的任务列表。可能的写法如下
--- - name: install httpd yum: name=httpd state=present - name: configuration Apache copy: src=httpd.conf dest=/etc/httpd/httpd.conf - name: configuration iptables template: src=file.js2 dest=/etc/sysconfig/iptables notify: # 会调用 roles/Apache/handlers/main.yml - restart httpd # 对应 name 为 restart httpd 和 restart iptables 的相应命令并执行, - restart iptables # 若之前 Apache 服务已安装,再次执行,不会触发 notify #- include: delete_httpd.yml
-
handlers:处理列表主要存放在 handlers/main.yml 中,也可以通过 include 包含更多的处理列表。可能的写法如下
--- - name: restart httpd service: name=httpd state=restarted - name: restart iptables service: name=iptables state=restarted
- templates:存放模板,对应 tasks 里面的模块 template,会自动在此目录中寻找 Jinja2 模板文件;
- vars:定义的变量主要存放在 vars/main.yml 中,也可以通过 include 包含更多。它只对当前 role 有作用;
-
meta:元信息主要存放在 meta/main.yml 中,也可以通过 include 包含更多。它的作用是定义 role 和 role 直接的依赖关系,标准格式如下
--- dependencies: - { role: Apache, port: 80 } - { role: mariadb, dbname: blarg, other_parameter: 12 }
group_vars 目录中存放供各个角色共同使用的 Ansible 全局变量,一般存放在 group_vars/all 文件中,也可以通过 include 包含更多。
运行带角色的 Playbook 方法与前面类似,只需要调用该 Playbook 的主文件。例如:
$ ansible-playbook site.yml
6.5、Ansible 中的 include
通过一个具体的 Playbook 的例子来说明 include 的用法
$ tree /root/sample /root/sample/ ├── hosts ├── roles │ └── install_client │ └── tasks │ ├── db.yml │ ├── app.yml │ └── main.yml └── site.yml $ nano -w ~/sample/hosts [db] 192.168.24.10 [app] 192.168.24.11 $ nano -w ~/sample/site.yml --- - hosts: '{{ myhosts }}' user: james sudo: yes sudo_user: root roles: - install_client $ nano -w ~/sample/roles/install_client/tasks/main.yml # 本次测试的关键地方 --- - include: db.yml when: "myhosts == 'db'" - include: app.yml when: "myhosts == 'app'" $ nano -w ~/sample/roles/install_client/tasks/db.yml --- - name: Touch db file shell: touch /tmp/db.txt $ nano -w ~/sample/roles/install_client/tasks/app.yml --- - name: Touch app file shell: touch /tmp/app.txt
6.6、Ansible 中的标签
当 Playbook 中有很多配置工作时,若中间的某个环节出错了,修改后重新执行的话,会发现有一大堆无关步骤可能隐藏错误。虽然 Ansible 提供了 retry 文件,但它却只是根据主机来判断是否重新执行,仍然不够方便;又或者,中间的某些步骤特别耗时,比如下载一个很大的数据包,每次执行特别浪费时间。注释掉 Playbook 中不需要的部分是绕过它中间环节的办法之一,但不是最最好的办法。实际上,Playbook 中有一个名为 tags 的关键字,它可以有效的解决 Playbook 的调试问题。tags 可以和一个 play(就是很多个 task)或者一个 task 进行捆绑;而 ansible-playbook 提供了 "--skip-tags" 和 "--tags" 来指明是跳过特定的 tags 还是执行特定的 tags。请看例子:
--- - name: test1.yml hosts: test-agent tasks: - command: echo "test1" tags: - test1 - command: echo "test2" tags: - test2 - command: echo "test3" tags: - test3
执行
$ ansible-playbook test1.yml --tags="test1,test3"
则只会执行 test1 和 test3 的 echo 命令。执行
$ ansible-playbook test1.yml --skip-tags="test2"
同样只会执行 test1 和 test3 的 echo 命令。