[toc]
这样理解Ansible更容易
这篇文章是 Jenkins 2.x实践指南
作者翟志军博客中的一篇内容,看完之后觉得写的非常好,所以收藏一下
滚滚长江东逝水,浪花淘尽英雄。是非成败转头空。青山依旧在,几度夕阳红。—— 《临江仙》
电脑店
从前,有一家电脑店。原来你即是老板,又是店员时,拿到清单,你就必须亲自动手采购,然后一个个零件组装。每天都做着即重复又辛苦的活。
虽说你的组装技术已经很娴熟了,但是偶尔还发生装错的情况(大概是那天和老板娘吵架了),把一个客人要求的 CPU i5 装成了 CPU i7。结果是你亏本或者赚得少了。
后来,你采购了一个自动组装电脑的机器人。你只要告诉它电脑的配置,并把零件放到指定的箱子中。接着启动这台机器,它就自动帮你组装好电脑了。它每天都干重复的活也不会叫辛苦。
最重要的是准确,它不会因为心情不好,而装错。因为它根本不会闹情绪。
这样,老板就可以从重复的工作解放出来。然后将多出来的时间花在与人的沟通上,为有不同需求的人设计更合适的电脑配置清单。毕竟游戏发烧友和办公小白领的需求是不一样的。
在运维领域,不少运维人都干着即是老板又是店员的工作。如果在运维领域也能有这样的“机器人”该多好。事实上,Ansible、Puppet、Check 就是这样的机器人。
为什么要从零设计一个运维机器人
本文并不想生硬地罗列 Ansible 的各个知识点。因为那样,大家不如直接看 Ansible 官方文档就好。
笔者采用从零设计一个运维机器人的方式来告诉你,为什么 Ansible 会是现在这个样子。当然,现实中的 Ansible 不会像本文所写的那样一步步设计。
为什么要这样呢?因为笔者觉得只有知道一个工具背后的设计原理,真正用这个工具才会得心应手。
运维机器人的最终模样
首先,需要确定一下实现这个运维机器人的目的是什么。我们并不是希望所有的运维工作都交给运维机器人,而是希望运维工作中重复的那部分尽可能的交给机器人,把创造性的工作全部交给人。如下图所示。
以终为始是一种非常有效的实现目标的思考模型。根据此思考模型,我们首先必须探讨运维机器人的最终模样。然后,再讨论可能的解决方案。
那么,什么样的运维机器人能帮助我们实现上述的自动化运维目标呢?想像一下。是不是只要我们对着运维机器人说一句:“我要部署一个 Nginx 到 192.168.12.11”。它就可以帮我们完成了?
但是它怎么知道如何连接到 192.168.12.11 呢?是使用用户名密码的方式,还是使用私钥?它又怎么知道 Nginx 需要什么样的配置呢?一问下来,其实,语音运维只适用于启动一些预定义的动作。就像汽车的一键启动。你不可能使用语音来对 Nginx 进行大量的配置。
而纯文本才是进行 大量配置的最好媒介。
所以,运维机器人的最终模样是:我们将部署的主机 IP、登录方式、Nginx 的配置放在一个文本文件中,然后运维机器人读取这个文本文件,然后根据配置进行部署。如果部署的是业务系统,我们还需要准备该业务系统的二进制包。如下图所示。
那么,我们在文本文件中使用何种语言描述我们的配置需求呢?可以分成两种。一种是利于人类学习的自然语言(如英语)。另一种是利于机器读取的结构化数据(如YAML、JSON)。
按当前的技术实现的可能性,不论是运维机器人,还是交给其它程序,都需要将自然语言转到结构化的数据。就像程序员,需要将业务知识翻译成编程语言;像编译器将编程语言翻译成机器真正能识别的二进制代码。
运维机器人的真正核心不是将自然语言转成结构化的数据。所以,文本文件中,我们直接写结构化数据。同时,我们决定使用 YAML 格式作为结构化数据的载体。因为它是非常流行的配置文件格式。降低人们写结构化数据的难度。
当然,好的设计不应该与具体配置文件格式耦合。
实现运维机器人要解决哪些问题
以上只是 确定了运维机器人的最终模样及使用方式,解决的是用户的问题。但是因为我们是运维机器人的设计者,我们必须考虑如何实现它。
回想一下,平时我们的运维人员是如何实现自动化的?是不是写好了 bash 脚本后,然后将脚本上传到目标机器,最后在目标机器上执行该脚本。
这个 bash 脚本其实也可以算是一种结构化的数据格式,而且是一种不需要再做编译,目标机器能直接运行的格式。
在平时的自动化方式基础上进行抽象。我们觉得要实现运维机器人要解决的关键问题有:
- 需要将 YAML 转成目标机器可执行的程序(或脚本)。
- 需要将可执行的程序上传到目标机器,并执行。
为什么一定要将 YAML 转成目标机器可执行的程序呢?直接写 bash 不就可以了?
因为运维机器人要解决的不止是 Linux 系统的自动化运维,还有 Windows,甚至路由器的。所以,我们需要使用一种独立于目标机器的语言来描述我们的运维需求。
问题 2 我们先放一放,因为上传代码到目标不是运维机器人的关键问题。
实现 YAML 转成 ?
虽然咱们希望运维机器人能运维所有类型的机器,但是本文重点不是要在一篇文章内实现所有类型机器的自动化运维。接下我们只针对 Linux 机器进行讲解。
现在我们遇到的问题是要将 YAML 转成什么程序以实现在目标机器上执行?
如果我们将 YAML 转成 Java 程序,那么目标机器就必须装有 JDK,这是不现实的。你不可能让所有的 Linux 机器都安装 JDK。所以,YAML 最好是能转成所有 Linux 都支持程序。 目前笔者能想到的就是:bash 和 Python。
P.S. 现实中,很多运维工具要求所有的目标机器必须安装特定的客户端的。而 Ansible 却不需要。如果在使用自动化运维工具前,你要为所有的机器安装特定的客户端,那么,你怎么自动化为所有的机器自动安装客户端呢?留着读者思考。
从 bash 和 Python 之间做选择,没有什么好讨论的,选择 Python。
那么,怎么将 YAML 转成 Python 代码呢?这要看我们怎么设计 YAML 内的描述语言了。其实就是设计自动化运维机器人的领域特定语言(DSL)。为方便讨论,我们称之为:OPL 语言。
OPL 语言1.0:基本要素
现在咱们在为一台机器部署一个 Nginx 作为切入点,来设计我们的 OPL 语言。以下就是1.0版本:
---
- host: "192.168.35.10"
ansible_ssh_user: vagrant
ansible_ssh_pass: vagrant
tasks:
- name: install nginx
yum:
name: nginx
action: install
- host:代表部署的目标机器。
- ansible_ssh_*:开头的 key 是指定 ssh 连接的用户名和密码。
- tasks:是一个数组,包含一系列任务。
- name:任务名称,方便人阅读。
- yum:要执行的任务的类型,也就是要执行 yum 操作。yum 任务下的 name 属性代表要安装的软件名称,action 属性代表执行的是安装操作。
1.0版的 OPL 语言包含了运维领域最基本要素:
- 目标主机的描述
- 连接目标主机的必要信息
- 任务描述
为方便沟通,我们暂将些 YAML 文件命名为 playbook.yml
。
OPL 语言2.0:采用声明式的任务描述方式
注意到 OPL 语言1.0中的任务描述方式很像现实中执行shell:yum install nginx
。我们称这种方式为脚本式的。脚本式的描述方式与传统的运维方式更像。
但是声明式的描述方式更适合 OPL 语言所要解决的问题。我们期望的是描述我们期望的结果,而不是换一种方式写脚本。
采用声明式的描述方式,还有一个 很重要的原因:幂等的。在概念上,声明式的描述被执行了多少次,结果都应该是我们所声明的。而第二次执行脚本式的代码时,你会问,结果还会和我第一次执行的一样吗?
所以,2.0中,任务描述方式改成声明式:
- name: Ensure nginx present
yum:
name: nginx
state: present
state
属性的值为 present
形容词。
P.S. 识别声明式与脚本式的简单办法是看它在描述目标状态时,是用动词,还是用形容词。
除了 yum 任务,接下来要实现的所有任务也将采用声明式。
OPL 语言3.0:主机清单
在 2.0 版本中,我们只实现了一台目标机器的部署。但是现实中,我们常常要针对多主机进行部署。我们需要一种更好的方式去描述目标主机。
主机清单的内容,也需要一个文件来存放。关于文件的格式,我们采用一种类 INI (INI-like)的格式。文件名暂命名为:inventory
。以下是 inventory 文件的内容:
[nginx]
192.168.35.10
192.168.35.11
192.168.35.12
[springboot]
192.168.35.20
192.168.35.21
192.168.35.22
[]
括号中是主机分组的名称,接下来是就是这个组内目标机器的 IP 列表。
而上一版本中的 playbook.yml 中的 host
key 是单数,所以不合适了。我们改成 hosts
复数,同时值变成了在主机清单中的组名,而不是具体某台主机的 IP。
playbook.yml 文件内容如下:
- hosts: "nginx"
ansible_ssh_user: vagrant
ansible_ssh_pass: vagrant
tasks:
- name: install nginx
yum:
name: nginx
state: present
细心的读者朋友应该发现了,目标机器的连接方式是各不相同的,有些用用户密码的方式,有些用密钥的方式。所以,我们再将 ansible_ssh_*
写在 playbook.yml 中已经不合适了。
笔者只能告诉你,这是剧情需要。OPL 语言的后续版本会解决此问题。
现在咱们为运维机器人准备的内容已经不再只是一个 playbook.yml 文件,它应该是一个目录了,目录内容如下:
├── inventory
└── playbook.yml
OPL 语言4.0:include 任务
在 OPL 1.0 版本,已经考虑到了一次部署将会包括很多子任务。所以使用了 tasks
这个复数词。
而现实中一次部署任务往往包含几十个子任务,playbook.yml 的文件内容一定会膨胀。这样的源代码非常难维护。所以,在 4.0 版本,我们决定为 OPL 增加一个 include_tasks 任务类型。用户可以通过 include_tasks 任务类型将另一个包含任务描述的文件(这里我们称为子任务集)引入到当前的 playbook.yml。
- hosts: "nginx"
tasks:
- name: config firewalld
include_tasks: firewalld.yml
- name: install and config nginx
include_tasks: nginx.yml
这时的目录结构如下:
├── firewalld.yml
├── inventory
├── nginx.yml
└── playbook.yml
所有的子任务文件都与 playbook.yml 同级,会显得很乱,不利于区分哪个文件是 OPL 的执行入口。我们是不是可以建了一个 tasks
目录来专门存放呢?事实上就应该这么做。
所以,经过重构,得到了4.1 版本。目录结构调整如下:
├── inventory
├── playbook.yml
└── tasks
├── firewalld.yml
└── nginx.yml
playbook.yml 中 include_tasks 任务的文件路径做相应的调整,改成:
- name: install and config nginx
include_tasks: tasks/nginx.yml
OPL 语言5.0:丰富任务类型
5.0 之前的版本,已经实现了一个基本框架。5.0 版本中我们希望加入更多的任务类型,以满足不同的运维需求。
copy 任务
部署过程中,常常需要将一些文件从本地 copy 到目标机器。copy 任务的代码样例如下:
- name: "ensure nginx package exists"
copy:
src: "./files/nginx.tar.gz"
dest: "/tmp"
为方便管理,我们将所有 copy 任务用到的文件放在 files 目录中。此时目录结构调整如下:
├── inventory
├── playbook.yml
└── tasks
├── files
│ └── nginx.tar.gz
├── firewalld.yml
└── nginx.yml