El Psy Congroo

Ansible学习笔记

Why Ansible

Ansible是一个轻量级的IT自动化工具,和Fabric类似,实现了批量系统配置、程序部署、运行命令等功能。

目前我们开发环境大部分自动化脚本都是用Fabric实现的,因此在学习使用Ansible之前,我们肯定要了解下使用Ansible替换Fabric的收益和成本。

首先两者都是通过SSH来完成大部分工作的,因此不需要安装Agent,也没有Server的概念,其中Fabric直接通过SSH运行命令,Ansible先将Module推送到目标服务器,再远程执行。

两者最大的区别在于抽象层次的不同,Fabric是所见即所得的实现方式,抽象比较少,基本上所有的内容都在一个文件中,容易理解,上手简单,并且得益于Python的强大,可以灵活实现各种功能。但这种扁平的管理方式也有着显著的缺点,随着主机数量,主机间差异,脚本功能不断增加时,整套系统会越来越难以维护。

有句古话说计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决,Ansible就使用这种方式,设计了较厚的抽象层次,显著的降低了使用者需要面对的复杂度。通过Module来封装功能,Task调用Module实现需求,用Role来组织可重用的Task,用Inventory来管理主机,用Playbook将Role和Inventory编排在一起,形成最终需要执行的操作。这里需要强调一点,Playbook的作用不是让主机拥有Role,而是用Role来decorate目标主机。

相对于Fabric使用Python作为载体,Ansible则是使用YAML创建了一套DSL,并内建了大量Module,因此有一定的学习成本。下面通过一个简单任务:在目标系统中安装Apache,来直观了解下Fabric和Ansible的使用差异。

  • 使用Fabric
1
2
3
4
5
6
7
# fabfile.py
from fabric.api import *
env.hosts = ["root@localhost:9022",]
def install_apache():
run('yum install httpd -y')
1
$ fab install_apache
  • 使用Ansible
1
2
3
4
5
6
7
8
9
# tree
├── group_vars/
├── host_vars/
├── hosts
├── roles/
│   └── httpd/
│      └── tasks/
│      └── main.yml
└── site.yml
1
2
# hosts
localhost:9022 ansible_user=root
1
2
3
4
5
# main.yml
- name: install the latest version of Apache
yum:
name: httpd
state: latest
1
2
3
4
5
6
# site.yml
name: install the latest version of Apache
hosts: all
roles:
- httpd
1
$ ansible-playbook site.yml

看起来Ansible似乎更繁琐,完成一个简单任务需要更多的代码量,但在实际使用时,我们关心的并不是执行的细节,而是需要达成的目标,site.yml明确的描述了这个目标,相对于Fabric,需要阅读的代码量其实更少了。

Step by Step

首先花三分钟学习下YAML,Ansible中的一切都是用YAML来组织的。YAML是专门为可读性设计的配置语言,基本上就是一个用换行分隔属性,用缩进代表嵌套的JSON。接下来我们来学习Ansible的一些基本概念。

Inventory

用于配置由Ansible管理的主机,支持分组及分组嵌套。这里也可以直接配置主机和分组对应的变量,但还是推荐使用前面演示的目录结构来管理变量。Ansible默认从/etc/ansible/hosts文件读取配置,可以在ansible.cfg中修改默认配置,或运行时使用-i <path>设置。
一个典型的Inventory文件如下:

1
2
3
4
5
6
7
8
9
10
11
# hosts
[web]
foo.example.com
bar.example.com
[db]
mysql.example.com
[all_server:children]
web
db

How to run

有了Inventory以后,我们就可以运行第一条Ansible命令了。

1
2
# 对Inventory中所有主机执行ping module,探测是否可达
ansible all -m ping

以这种方式运行的命令被Ansible称为ad-hoc命令,适合进行一些简单的一次性操作,基本语法如下:

1
2
3
4
5
6
7
8
9
10
ansible [-i inventory] <pattern> [-m <module>] [-a args]
# -i: inventory文件路径
# pattern: 主机匹配模式,常用的是分组名,主机名,或者all,此外还支持一些更复杂的语法,包括集合操作等
# -m: module名称,不指定时,默认为command
# -a: module参数,对于一般module,使用KV对形式,对于run commands类例如command、shell,输入需要执行的命令
# e.g.
ansible all -a "hostname -i"
ansible db -m shell -a "echo $HOME"
ansible web -m yum -a "name=httpd state=latest"

但如果要执行一些复杂任务,例如应用部署,ad-hoc命令就不合适了,那我们接着来介绍playbook

Playbook

官网有首好诗,很好的解释了playbook中的一些概念,引用过来大家感受下…

Playbooks contain plays
Plays contain tasks
Tasks call modules

Tasks run sequentially

Handlers are triggered by tasks,
and are run once, at the end of plays

下面通过一个简单的例子来具体看下上述几个概念。

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
# deploy_tomcat.yml
---
- name: deploy tomcat
hosts: all
vars:
tomcat_home: ~/tomcat
tomcat_port: 9080
tasks:
- name: check whether tomcat is deployed
stat: path={{ tomcat_home }}
register: out_check
- name: copy and unpack tomcat
unarchive: src=upload/apache-tomcat-8.5.9.prod.tar.gz dest=~/
register: out_unpack
when: not out_check.stat.exists
- name: rename tomcat
shell: mv ~/apache-tomcat-8.5.9 {{ tomcat_home }}
when: out_unpack.changed
- name: update server.xml
template:
src: upload/server.xml.j2
dest: "{{ tomcat_home }}/conf/server.xml"
backup: yes
notify: restart tomcat
- name: add test html
copy: src=upload/test.html dest="{{ tomcat_home }}/webapps/test/index.html"
notify: restart tomcat
handlers:
- name: shutdown tomcat
command: "/bin/bash -l -c {{ tomcat_home }}/bin/shutdown.sh"
ignore_errors: yes
listen: "restart tomcat"
- name: start tomcat
command: "/bin/bash -l -c 'nohup {{ tomcat_home }}/bin/startup.sh'"
listen: "restart tomcat"
- name: wait tomcat start
wait_for:
port: "{{ tomcat_port }}"
listen: "restart tomcat"

deploy_tomcat.yml这个文件就是一个playbook,包含了deploy tomcat这个play
play指定了目标主机all,设置了变量vars,并且包含了一系列task
task是顺序执行的,但可以指定执行条件,例如copy and unpack tomcat这个task只有当tomcat_home目录不存在时才会执行

这里要提到Ansible的一个设计理念

If it ain’t broke, don’t fix it

写自动化脚本的目的是让目标主机达到所需的状态,而不仅仅是机械的执行一连串命令,Ansible就是以这样的理念来设计的,大部分module都会由当前状态来判断是否需要执行,因此module的执行是幂等的,此外每一个task都有一个changed状态,来表示这次运行是否对目标主机产生了改变,没有改变就不要去fix,像handler只会在taskchanged == true时才会被执行。这也是为什么我们写task时应该尽量使用相应的module,而少用run commands例如shell的原因,因为Ansible无法判断shell运行内容,因此changed永远为true,此外大部分的shell命令都不是幂等的,重复执行结果无法预期。

PS: 这里为了展示playbook的功能,因此没有按最佳实践来操作,这里的tasks显然属于一个role,这些变量直接定义在playbook中也并不合适。

Role

Role只是一个抽象概念,从实现角度来看role就是一种特殊形式的playbook。引入role这个概念主要是为了将实现细节从playbook抽象出来,一方面方便复用,另一方面增加了role这层抽象之后,playbook就不再需要关心实现细节了,需要关心的只有目标主机和目标状态,这样playbook的可读性也大大提高了。同时role通过使用约定的目录结构来存储playbook中的各个元素,更有利于我们管理。

  • Role的目录结构
1
2
3
4
5
6
7
8
9
10
roles/
webservers/
files/
templates/
tasks/
handlers/
vars/
defaults/
meta/
common/...
  • 使用role之后的playbook
1
2
3
4
5
---
- hosts: webservers
roles:
- common
- webservers

变量

  • 变量名:字母,数字及下划线,必须字母开头
  • 变量使用
    变量可以使用在playbook及template文件中,基本引用语法如下:

    1
    2
    3
    {{ var }}
    # PS: 由于YAML会将{}解释为map,因此当变量位于value行首时,整个value需要加上双引号
    "{{ var }} start a value"

    Ansible是基于Jinja2来提供变量访问功能的,因此有大量的表达式可以用来对变量进行动态处理

  • 特殊的变量: Facts
    既然Ansible是基于状态运行的,那目标主机状态的采集自然也是必不可少的。运行playbook时,Ansible默认会先运行一个setup的task,采集大量目标主机的信息,包括操作系统版本,IP地址,磁盘空间,内存使用,当前登录用户信息等等,供我们在playbook中使用。具体采集的内容可以用如下方式查看:

    1
    ansible hostname -m setup
  • 变量优先级
    由于变量可以来自各个地方,优先级是个坑,因此我们首先要做的是

    避免在多个地方设置同一个变量!

    另外一个简单的规律是:运行值覆盖配置值,配置值中child覆盖parent

Advanced playbook capability

// TODO

最佳实践

  • 原则

    • Complexity kills productivity
    • Optimize your Ansible content for readability
    • Think declaratively - Ansible is a desired state engine by design, do not “write code”
  • 合理的命名Plays和Tasks

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    - hosts: web
    name: installs and starts apache
    tasks:
    - name: install apache packages
    yum:
    name: httpd
    state: latest
    # output
    # PLAY [install and starts apache]
    # TASK [install apache packages]
    # ok: [web1]
  • 使用前缀和有助理解的词命名变量,并保持命名的一致性

    1
    2
    3
    apache_max_keepalive: 25
    apache_port: 80
    tomcat_port: 8080
  • 使用YAML语法,少用KV对,增加可读性(感觉适当使用KV对可读性还是有帮助的)

    1
    2
    3
    4
    5
    6
    7
    - name: configure telegraf
    template: src=telegraf.conf.j2 dest=/etc/telegraf/telegraf.conf
    - name: configure telegraf
    template:
    src: telegraf.conf.j2
    dest: /etc/telegraf/telegraf.conf
  • 尽量使用Modules替代Run Commands

    包括command,shell,raw,script,这类命令没有状态的概念,也不保证幂等性,在ad-hoc命令中使用没有问题,但是在playbook中,无法预期的返回结果以及各种可能出现的异常会使脚本的健壮性大大降低。以删除文件为例,rm foo当foo不存在时会报错,因此我们还需要额外判断文件是否存在

    1
    if [ -f foo ];then rm foo;fi

    这类异常事先很容易忽略,处理起来的代码也不好看,如果用file module来处理就简洁多了

    1
    2
    3
    - file:
    path: foo
    state: absent
  • 设置debug日志级别

    1
    2
    3
    4
    5
    6
    - debug:
    msg: "This always displays"
    - debug:
    msg: "This only displays with ansible-playbook -vv+"
    verbosity: 2

参考

ansible基础教程
DevOps Technologies: Fabric or Ansible
Ansible Documentation
Ansible Best Practices: The Essentials