Linux 自动化配置工具—— Ansible

Easior Lars posted @ 2015年11月09日 07:02 in Linux with tags YAML Ansible OpenSSH , 2027 阅读

在开源领域 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 命令。


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter