DevOps技术 - Ansible

Why Ansible

做IT的的都知道,除了设计,写代码,离不开装机器,装软件,装系统,原始的办法是手动安装和配置,但如果一个大型项目或产品:

  • 需要做很多次(100+)
  • 需要在很多机器上做(100+)
  • 项目中,有安装文档,但是都是手工行为,步骤超多,如何防止人为疏忽
  • 不同的项目,不同的环境怎么办,特别是大型项目和产品类软件,支持的OS不同,版本不同,配置不同,需求不同:n个客户 x n个环境 x n个发布 x n台机器

种一颗树和种1000颗树的方法是不同的!!!

原始的解决办法就是写脚本,尽量自动化,让机器来替我们干活 - 机器不累,不出错。这也就是为什么需要Ansible,它完成两个最基本的事情:

  • 配置管理(configuration or change management):Ansible不是专门的配置管理系统CMDB(configuration management database),但没有这些配置信息无法干活啦,一般软件项目需要的配置也就是ip, hostname, configuration files, 这些东东足已。Ansbile可以和CMDB结合工作,达成动态配置,这个“高级”用法就不在本文讨论范围。

  • 自动化框架(automation framework):注意不只是automation,Ansible把自动化的动作按它的方式编写和组织,这样才能完成其它的事情如编排(orchestration:把几个事情串在一起执行),包括重用,和共享,这就比脚本和脚本库高一个档次。使用者也不必纠缠于命令行的参数,选项等等各种细节(当然Ansbile允许你直接调用命令行),Ansbile采用类似SQL“声明式”的理念,而不是一大段具体的实现细节,关注点在“What”上而不是“How”,例如:

- name: set up & configure web servers
hosts: webservers
vars:
http_port: 80
max_clients: 200
tasks:
- name: ensure apache is at the latest version
yum:
name: httpd
state: latest
- name: write the apache config file
template:
src: /srv/httpd.j2
dest: /etc/httpd.conf
notify:
- restart apache
- name: ensure apache is running
service:
name: httpd
state: started
handlers:
- name: restart apache
service:
name: httpd
state: restarted

该例子完成三件事:

  1. 通过yum安装httpd;
  2. 通过template修改httpd.conf(httpd的配置文件),如有任何修改重启apache;
  3. 通过service确定httpd服务已启动

Ansible是自动化配置管理的顶级三大利器之一,另两个是Puppet和Chef,Puppet老字号,应该是最成熟的,Chef也很普遍,但我选择Ansible的原因是:

  • 简单:no databases,no daemons,no agents,Puppet和Chef需要在每台机器上安装agent并持续monitor而Ansible无agent设计,通过发SSH命令到各台机器上执行,而非监管机器,其依赖于Python 2.4+(Python 3目前还木有支持)(Ansible是Python编写的)和SSH,这在Linux机器上基本上就是默认的,所以安装使用Ansible及其简单,对目标机器无任何影响和副作用;

  • 易用,易扩展:Ansible的开发就是写YAML文件,无编程要求,无脚本,非常易读和理解,Ansbile没有花里花俏的概念,提供抽象的同时,没有强行你穿两条内裤,你照样能做任何原来想做的事情,这是好工具的哲学。

Ansible的工作原理

Ansible由一台机器(control server)来配置其它机器,基本原理可以用下面这张图概述:

Ansible原理

图中包含了Ansible的几个基本概念/术语:

inventory

inventory以YAML文本文件形式存在,定义了机器的基本信息,包含ip等。上面hosts中指定的webservers就定义在inventory文件里。通常都要给Ansible指定一个inventory。

task or task module(任务模块)

这个就是具体干活的家伙,Ansible把各种最基本的自动化动作(task)以Python module的形式实现,拿来就可以用了,是Ansible的最基本动作单元,例如上面webservers例子中的yum,template, service就是Ansible提供的任务模块。Ansible提供的任务模块超过400个,运维最基本的东西都包括了,具体见Ansbile Module文档。自己也可以通过Python扩充任务模块,这个不在本文讨论范畴(我感觉用不到的)。

称之为module/模块的原因是因为Ansible是用Python开发的吧,不叫component,lib,package,等等。

role(任务角色)

由Ansible和community分享提供,或自己编写。一个role就是一组task,相当于一个迷你playbook,这样相同的role,在不同的地方就不需要重复写task,例如,上面的例子中可以把yum,template,service合为一个apache的role:

- name: set up & configure web servers
hosts: webservers
tasks:
- {role: apache, http_port: 80, max_clients: 200}

role把任务的执行,重用,扩展的提高了一个层次。

playbook

playbook也是yaml文本文件,相当于演戏的脚本,把角色(role),任务(task),inventory串起来,例如:

- name: set up & configure web servers
hosts: myapp-webs
become: true
tasks:
- {role: apache, http_port: 80, max_clients: 200}
- name: set up & configure app servers
hosts: myapp-apps
become: true
- {role: tomcat, http_port: 8080}
- name: set up & configure db servers
hosts: myapp-dbs
become: true
tasks:
- {role: mysql, max_connection: 200}

playbook把任务的执行,重用,扩展的又提高了一个层次,达到系统级别,例如上面的例子就完成了整个系统包括web server, app server, db server的配置。

在运行时,Ansbile把playbook结合inventory和module/roles编译成Python程序,control server通过SSH将其发送至目标机器,然后在目标机器上执行。 Ansible把系统/目标机器看成是一个状态机,每做一个task,状态会发生改变,通过若干task后,目标机器就从原始状态达到你想要的状态,实现“变化管理”。如果目标机器已经处于目标状态中,该task就不会被执行。每一步变化是“等幂”的 - 可重复,可验证。例如上面的这个yum任务:

- name: set up & configure web servers
hosts: myapp-webs
vars:
http_port: 80
max_clients: 200
tasks:
- name: ensure apache is at the latest version
yum:
name: httpd
state: latest

如果httpd最新版本已经安装,上面这段yum任务将不会执行。Ansiblez在执行任务时具有一定的状态控制和逻辑。

Ansible的基本操作

安装

Ansible只安装在control server上,安装Ansible通过Linux包安装工具如RedHat的yum,Suse的zypper,等,很容易搞定(官方安装文档),如果offline安装,手动安装所需要的rpm。Ansible的目标机器可以是Linux或Windows,但目前control server必须是Linux,虽然有各种hack,但强烈不建议Windows作为control server。

设置

Ansible的设置文件可以按顺序下面找到:

  • $ANSIBLE_CONFIG
  • ./ansible.cfg
  • ~/.ansible.cfg
  • /etc/ansible/ansible.cfg

他们不会相互覆盖,遇到第一个设置文件就停止。下面是ansible.cfg例子:

[defaults]
forks = 5
host_key_checking = False
gathering = smart
roles_path = /usr/local/repository/ansible/roles
[priviledge_escalation]
become_method = sudo
[ssh_connection]
ssh_args = -o ForwardAgent=yes
scp_if_ssh = True
pipelining = True

编写

如何编写和组织inventory,role,playbook是Ansible的主要工作内容。Ansible采用YAML文档格式,其语法简单,编写前先了解一下,参考阮一峰的YAML 语言教程。注意Ansible很多动作是自动进行的,类似编程,默认以main.yml为入口(convention over configuration)。

必读:Ansible Best Practices

Ansible Best Practices官方网页,非常清楚了。 我改动的地方是group_vars和host_vars,接下来会解释。

Ansible

inventory

下面inventory的例子定义了dev环境中的机器:

localhost ansible_connection=local
myapp-db1 ansible_ssh_host=172.16.0.62
myapp-db2 ansible_ssh_host=172.16.0.63
myapp-app1 ansible_ssh_host=172.16.0.60
myapp-app2 ansible_ssh_host=172.16.0.61
myapp-web1 ansible_ssh_host=172.16.0.64
myapp-web2 ansible_ssh_host=172.16.0.66
[dev:children]
myapp-dbs
myapp-apps
myapp-webs
[myapp-dbs]
myapp-db1
myapp-db2
[myapp-apps]
myapp-app1
myapp-app2
[myapp-webs]
myapp-web1
myapp-web2
[dev:vars]
ansible_ssh_user=ansible
ansible_ssh_private_key_file="/home/ansible/.ssh/id_rsa"

myapp-db1 ansible_ssh_host=172.16.0.62

定义了myapp-db1这台机器host,ansible_ssh_host是系统变量名,用来定义此台机器的IP地址(其实是隶属于myapp-db1的一个key),自己可以加入自定义的变量。

[dev:children]

children这个关键字说明dev的成员myapp-dbs和myapp-apps不是单个host,而是一个组,[myapp-dbs]和[myapp-apps]则分别定义了他们的组成员

[dev:vars]

vars这个关键字说明里面的变量将作用在所有的dev成员上(dev的通用变量),ansible_ssh_user和ansible_ssh_private_key_file也都是系统变量名。

role

role由几部分组成:

  • defaults: 定义该role用到默认变量,该role运行时自动加入这里定义的变量,但其定义的变量优先级别最低,若已在其他地方定义,可被覆盖。
  • vars: 该role运行时自动加入这里定义的变量,通常可把需要传入的变量定义在这里,vars和defaults很类似,通常把必须用到变量的放在defaults里,可变的放在vars里,例如tasks里针对不同的OS,版本用到的变量,需要到的时候可以引入,include_vars可自动在此目录中寻找变量文件(相对路径):
  • files:存放各种文件,该role需要执行相关file tasks时,file module可自动在此目录中寻找文件(相对路径)
  • templates:该role需要执行template task时,可自动在此目录中寻找template文件(相对路径),template这个task专门用来修改配置文件。
  • tasks:该role需要执行的tasks,类似playbook,include可在此目录中寻找tasks文件(相对路径)
  • handlers:定义该role需要的回调task,在tasks里可以调用

可以参考一下别人是如何编写role的,这是一个安装和配置mysql的role

playbook

playbook定义了一系列的tasks,每一段包含下面的字段:

  • name: 任务组名称或描述
  • hosts: 目标机器
  • become & become_user: 指定登录执行任务的用户,目标机器和执行任务的用户可能不同,例如需要root权限
  • tags: 任务标签
  • tasks: 一组具体任务,可以调用某个或多个任务模块(task module)、某个或多个角色(task role)

tags和vars对使用好Ansible非常重要,下面会介绍。

这是provision-dbs.yml的内容:

#!/usr/bin/env ansible-playbook
- name: set fact-related variables (always run)
hosts:
- myapp-dbs
gather_facts: true
tags: always
tasks:
- include: vars.yml
- name: db server common
hosts:
- myapp-dbs
become: true
roles:
- {role: server-common, tags: ["common"]}
- name: install & config mongodb
hosts:
- myapp-db1
become: true
roles:
- {role: mongodb, tags: ["mongodb"]}
- name: install & config mysql
hosts:
- myapp-db2
become: true
roles:
- {role: mysql, tags: ["mysql"]}

执行

Ansible的运行有两种主要模式,一种是playbook模式,另一种是随机模式。

playbook例子,一个命令把整个系统搞起:

> ansible-play site.yml

随机例子1,执行某个命令行”service status-all”:

> ansible db1 -a 'service --status-all' -u ansible -b

随机例子2,执行某个task module(shell),通过-a输入该task module附加参数,效果和上面一样:

> ansible db1 -m shell -a 'service --status-all' -u ansible

当然playbook是主要模式。

Ansible的项目实践

Ansible比较新,版本间的差别比较大,有些问题具体运行才知道。下面是运用Ansible的几个重点和技巧:

重点:部署lifecycle

部署基本上遵循下面四步:

部署四步

3,4由Ansible完成,所以通常我会有两个playbook - provision.yml和site.yml:

#!/usr/bin/env ansible-playbook
# provision.yml
- name: display environment name
hosts: localhost
tags: always
tasks:
- debug:
msg: "MyApp environment :::> {{ app_env }}"
- name: display environment name
hosts: localhost
tags: always
tasks:
- debug:
msg: "MyApp OS :::> {{ ansible_os_family }}"
- include: provision-dbs.yml
- include: provision-apps.yml
- include: provision-webs.yml
#!/usr/bin/env ansible-playbook
# site.yml
- name: display environment name
hosts: localhost
tags: always
tasks:
- debug:
msg: "MyApp environment :::> {{ app_env }}"
- name: display environment name
hosts: localhost
tags: always
tasks:
- debug:
msg: "MyApp OS :::> {{ ansible_os_family }}"
- include: site-cots.yml
- include: site-apps.yml
- include: site-misc.yml

重点:变量vars

运维的复杂性和灵活性由变量vars来体现的,Ansible的variable可在多处定义,并有优先顺序。不过不用那么复杂,关键是搞清楚变量应用的范围:

  • 全局变量 vars/main:不受部署环境影响,到处都要用到,全局变量的例子比如project_name, app_user,db_port, 等等。

  • 环境变量 env_xxx/group_vars/all/xxx:项目开发中通常有多个环境xxx:local, dev, staging, uat, production,有些变量随环境改变而改变,但相当于该环境下的全局变量,除了inventory/机器变量外,其它的变量例子如host file, resource_dir,repo_url,等等。

Ansible没有提出全局变量和环境变量的使用和区别,只有机器变量(group_vars & host_vars),Ansible Best Practices里把它们当做了全局变量,但结构上是支持的,也就是每个环境下都可以分别有inventory和group_vars & host_vars,而all就是默认的机器组,所以env_xxx/group_vars/all/下所有的变量文件都会被识别为机器全局变量,而且all的机器变量引用时不需要前缀指定,特定的group_vars & host_vars变量xxx必须指明是哪个group()或host(),“groups”和“hostvars”是Ansible内置变量。为了支持全局变量(各个环境都使用),可以通过file link的方法让每个env_xxx/group_vars/all/main指向同一个文件,上图中的env_dev/group_vars/all/main指向vars/main。

  • 支持多OS,多基础设施的变量:这种变量主要是支撑产品级的软件项目,通常是动态的,通过任务执行来设定,例如上面的dbservers.yml里引用的vars.yml:
- name: define os family (suse)
set_fact:
os_family: "suse"
when: ansible_os_family == "SuSe"
- name: define os family (redhat)
set_fact:
os_family: "redhat"
when: ansible_os_family == "RedHat"
- name: define os family (debian)
set_fact:
os_family: "debian"
when: ansible_os_family == "Debian"
- name: check os_family
fail:
msg: "ERROR - can not detect supported os_family"
when: os_family is not defined

变量os_family在以后的tasks和roles中都可能用到。

  • role变量:这些变量应用范畴只是该role,是传统意义上各软件component的参数变量,在role里定义和使用,但playbook调用时可以进行指定或覆盖默认值,例如一个Tomcat的war部署时传人war_name:
- name: deploy my app1
hosts:
- myapp-apps
become: true
become_user: "{{ app_user }}"
roles:
- {role: tomcat-deploy, war_name: "app1", tags: ['app1']}
  • Ansible流程和控制变量:上面的变量主要用来支持配置的,还有一些属于控制或者辅助控制Ansible流程的,这些通过Ansible提供的功能来指定,例如从命令行输入:
- name: create app user name & password
hosts: all
become: true
become_user: root
vars_prompt:
- name: app_user prompt: "app user name -> "
- name: app_password prompt: "app password -> "
- name: app_group prompt: "app group -> "
user:
name: "{{ app_user }}"
password: "{{ app_password }}"
group: "{{ app_group }}"
append: yes
generate_ssh_key: yes
shell: /bin/bash
home: "/home/{{ app_user }}"
createhome: yes
state: present

流程控制的例子见下面“技巧:有条件执行 conditional execution”。

  • Ansible默认/内置变量:例如获得当前运行的playbook路径, 则是当前role的路径,这里有个参考

重点:标签 tags

在调试和部署的时候,有些任务需要重复多次的,而playbook包含从头到尾,所以需要指定特定的任务(过滤掉其它的),这时候标签tags发挥作用。Ansible命令行支持两种标签指定方式:

  • -t or –tags
  • –skip-tags

怎么打标签关键是看需要做什么样的动作,例如provision-dbs.yml里:

- name: db server common
hosts:
- myapp-dbs
become: true
roles:
- {role: server-common, tags: ["common"]}

如果指定标签common,就可以执行所有common的任务。而在common这个role的tasks里:

- include: repo.yml
tags: repo
- include: hostfile.yml
tags: hostfile
- include: misc.yml

如果我在命令行里指定标签hostfile,这样就能执行部分特定的common任务。通过不同标签的组合也可以完成一定的任务,例如用于升级系统的tomcat:

> play appservers.yml -t tomcat,app1

标签tag最终是落在每一个task上的(透过playbook,role),但直接给每个task打标签就很麻烦,为保持role的可移植,我基本不在role里打标签。我遇到了playbook的include打上标签,但不起作用(Ansible bug or defeit?)。

有的时候tags还是不好用,例如mysql里要不要做clustering,这时可以通过变量来控制,参考下面的例子“技巧:有条件执行 conditional execution”。

技巧:gathering facts

在ansible.cfg中加入:

gathering = smart
fact_caching = jsonfile
fact_caching_connection = /app/intellisurf/deployment
fact_caching_timeout = 86400

技巧:host

除了tags,可以通过-l来限定目标机器,Ansible支持比较复杂的机器组合方式:

  • OR关系: h1:h2
  • NOT关系: !h1
  • Wildcard: web*.app.com
  • Regx: (~web[0-9]+)

通过-t和-l就能指定在某个目标机器某个动作,例如:

> play dbservers.yml -t mysql -l db1

技巧:SSH Keys

通过key免去登录密码输入,有几种选项:

  • 一个key登录所有机器,通过默认的key实现,在ansible.cfg文件里的[defaults]定义:
private_key_file=/etc/ansible/keys/access.pem
  • 一个key登录一组机器,例如在上面的inventory文件里:
[dev:vars]
ansible_ssh_user=ansible
ansible_ssh_private_key_file="/home/ansible/.ssh/id_rsa"

一个key登录一台机器, 也是在在上面的inventory文件里定义:

myapp-db1 ansible_ssh_host=172.16.0.62 ansible_ssh_private_key_file=/etc/ansible/keys/db1.pem
myapp-db2 ansible_ssh_host=172.16.0.63 ansible_ssh_private_key_file=/etc/ansible/keys/db2.pem
myapp-app1 ansible_ssh_host=172.16.0.60 ansible_ssh_private_key_file=/etc/ansible/keys/app1.pem
myapp-app2 ansible_ssh_host=172.16.0.61 ansible_ssh_private_key_file=/etc/ansible/keys/app2.pem
  • 让SSH/SSH-Agent自己去解决, 有空我会具体介绍一下SSH。

技巧:有条件执行 conditional execution

register, when, changed_when, failed_when 例子:

- name: install jdk rpm (suse)
shell: zypper -n in {{ java_rpm }}
when: os_family == "suse"
register: install_java
changed_when: install_java.rc == 0 and "already installed" not in install_java.stdout
- name: validate java home
shell: . /etc/profile && echo $JAVA_HOME
register: java_home_result
failed_when: java_home_result.stdout is not defined or java_home_result.stdout

有些配置只需要做一次,例如数据库初始化,虽然Ansible提供了run_once,但感觉不好用,还是直接用变量做开关来控制。

技巧:循环执行

下面的例子循环构建所有的docker image:

- name: build all service docker images
host: app-build
become: true
tasks:
- name: build a docker image
include_role:
name: docker-build
vars:
- image_name: "{{ item.name }}"
- image_tags: "{{ item.tags }}"
with_items: "{{ service_images }}"
when: (incldes is not defined) or (item.name in includes)

Ansible通过with_items, with_dict等达到循环的目的,上面的例子还加入命令行动态变量includes来控制指定的image:

# play docker-build.yml -e "includes=[service1, service2]"

Ansible支持多重循环,

- name: build all service docker images
host: app-build
become: true
tasks:
- name: build a docker image
include_role:
name: docker-build
vars:
- module_name: "{{ item.0.name }}"
- image_name: "{{ item.1.name }}"
- image_tags: "{{ item.1.tags }}"
with_subelements::
- “{{ modules }}”
- service_images
when: (incldes is not defined) or (item.0.name in includes)

item.0和item.1对应循环体的循环变量。

技巧:ansible-vault存储敏感信息

密码等属于敏感信息,可以把包含敏感信息的vars文件通过ansible-vault加密,当直接运行包含这些加密vars文件的playbook时,会出错(加密后的vars已经不可读了),必须ask-vault-pass:

$ ansible-playbook site.yml --ask-vault-pass

详细使用方法见http://docs.ansible.com/ansible/playbooks_vault.html

技巧:debug

通过环境变量:

$ export ANSIBLE_STRATEGY=debug

或者直接在playbook:

- hosts: localhost
strategy: debug
tasks:
-

当该任务出错时,Ansible暂停执行,调用debugger,这时可以任意调整变量:

(debug) task.args

详细文档:https://docs.ansible.com/ansible/playbooks_debugger.html

另一个常用的方法时debug module,拿来输出信息:

- name: display environment name
hosts: localhost
tasks:
- debug:
msg: "environment :::> {{ app_env }}"
verbosity:3
- name: display OS name
hosts: localhost
tasks:
- debug:
msg: "OS :::> {{ ansible_os_family }}"
verbosity:3

注意verbosity,Ansible命令行用 -v,-vv, -vvv, -vvvv等来控制debug输出的verbosity程度,当verbosity: 3时,-v,-vv时将看不到debug的输出。

几个运行技巧

把ansible-playbook匿名成play,下面是几个有用的选项:

  • play site.yml –list-tasks:列出所有的tasks
  • play site.yml –list-tags:列出所有的tags
  • play site.yml –syntax-check:做语法检查
  • play site.yml –check:“虚假”运行,可告知那些tasks会产生改变,如果带上 -D or –diff,如果相关module支持,如template,会显示前后改变的具体内容
  • play site.yml –step:“单步”执行

容器架构对Devops的影响

Docker/Container带来了颠覆性革命,大大简化了DevOps,提倡 :

Build Once, Run Anywhere

注意,这里木有install(安装),木有config(配置)。复杂软件的安装和配置是个麻烦的事情,自动化完成提高了很大一步,但能不能完全省去呢?我们开发的服务器软件能不能像绿色软件那样,双击即可运行?能不能像绿色软件那样,双击即可运行?能不能像绿色软件那样,双击即可运行?接下来我会介绍近年来很火的Docker/Container。

福利