Ansible 是一个配置管理工具,当然同类的产品还有 Puppet、Chef、SaltStack 等,不过上述的三者都是基于服务端+客户端的模式,而 Ansible 相对来说要简单的多,无需安装服务端和客户端,只需要有 ssh 即可,而且使用简单。
简介
Ansible 是基于 Python 的配置管理和应用部署工具,这个非常简单,只需要 sshd 服务就可以,不需要客户端,官方称之为 “Ansible is Simple IT Automation”。
Ansible 采用模块化设计,基于 Python 实现,其底层调用 Paramiko 库实现并发连接 ssh 主机,另外还包括了两个基本库:PyYAML 和 Jinja2,分别用于配置文件以及模板化。
Ansible 的配置文件保存在 /etc/ansible/
目录下,包括了主配置文件 ansible.cfg
,以及管控主机 Host Inventory 的配置文件 hosts ;如果没有可以直接新建。
安装、测试
在 CentOS 中可以通过如下方式安装。
----- 直接通过yum安装,不过有可能版本会比较老
# yum --enablerepo=epel install ansible
----- 通过pip安装
# pip install ansible
----- 安装完之后可以通过如下命令查看版本号
# ansible --version
----- 安装启动 sshd 服务
# systemctl start sshd
----- 首先设置SSH免密钥登录
$ ssh-keygen -t rsa -P '' # 生成公钥/私钥
# cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys # 写入信任文件,所有服务器都需要执行
# chmod 600 /root/.ssh/authorized_keys
----- 测试能否登陆
$ ssh foobar@127.1
测试 ansible 时,需要将 /etc/ansible/hosts
文件配置为 127.1
的 IP 地址。
$ cat /etc/ansible/hosts
127.1
$ ansible all -m ping -u foobar
127.1 | SUCCESS => {
"changed": false,
"ping": "pong"
}
----- 也可以直接修改配值文件的内容
$ cat /etc/ansible/hosts
foobar ansible_connection=local ansible_ssh_host=127.1 ansible_ssh_user=foobar
$ ansible all -u foobar -m command -a "date" # 执行下测试命令
$ ansible all -u foobar -m shell -a "date" # 同上
$ ansible all -m setup -u foobar # 查看配置
其中 -m
指定模块,-u
指定用户名。
主机的系统变量 (facts)
ansible 会通过 module setup 来收集主机的系统信息,这些收集到的系统信息叫做 facts,这些 facts 信息可以直接以变量的形式使用。可以在命令行上调用 setup module 命令,查看那些变量可以使用。
$ ansible all -m setup -u foobar -a "filter=ansible_os_family"
然后可以在 playbook 中直接使用 facts 变量。
---
- hosts: all
user: root
tasks:
- name: echo system
shell: echo { { ansible_os_family } }
- name install ntp on Debian linux
apt: name=git state=installed
when: ansible_os_family == "Debian"
- name install ntp on redhat linux
yum: name=git state=present
when: ansible_os_family == "RedHat"
对于比较复杂的属性,可以通过如下的两种方式访问。
----- 使用中括号
{ { ansible_ens3["ipv4"]["address"] } }
----- 使用点号
{ { ansible_ens3.ipv4.address } }
在 Playbook 中,可以通过 gather_facts 控制是否收集远程系统的信息,如果不需要直接如下填写即可。
- hosts: whatever
gather_facts: no
自定义 facts
可以在 /etc/ansible/facts.d
目录下添加文件自定义 facts 。
$ cat /etc/ansible/facts.d/foobar.fact
[general]
foobar = "Just For Test"
$ ansible all -m setup -u foobar -a "filter=ansible_local"
Inventory
Ansible 中的 Inventory 文件定义了服务器的列表,默认情况下是 /etc/ansible/hosts
。
# 主机
192.168.9.10 # IP地址
mail.foobar.com # 使用FQDN表示,包括主机名+域名
jumper ansible_ssh_port=5555 ansible_ssh_host=192.168.9.50 # 将某些静态IP设置为别名
localhost ansible_connection=local # 指定选项
[webservers] # 分组
web1.foobar.com
web2.foobar.com
web[10:50].foobar.com # 表示从web10到web50,共计49台主机
web[a:f].foobar.com # 表示从weba到webf
web.foobar.com ansible_connection=ssh ansible_ssh_user=light # 指定选项
其中常用的选项包括了:
ansible_ssh_host # 指定ssh连接的IP或者FQDN
ansible_ssh_port # ssh连接端口
ansible_ssh_user # 默认ssh连接用户
ansible_ssh_pass # ssh连接的密码,这种方式不安全,可以用--ask-pass,或公钥
ansible_sudo_pass # sudo用户的密码
ansible_connection # ssh连接的类型,包括local、ssh、paramiko
ansible_ssh_private_key_file # ssh连接的公钥文件
ansible_shell_type # 主机使用的shell解释器,默认sh,可以设置为csh、fish等
ansible_python_interpreter # 用来指定python解释器的路径
PlayBook
首先看个很简单的示例。
$ cat foobar.yml
---
- hosts: all
remote_user: root
tasks:
- name: test connection
ping:
remote_user: foobar # 可以用不同的用户
$ ansible-playbook foobar.yml
playbook 是由一个或多个 “play” 组成的列表,其包括了如下的组件:
hosts
指定要执行任务的主机,可以是一个或多个由冒号分割主机组。user
通过remote_user指定远程主机上的执行任务的用户。tasks
play主体部分是各个任务按次序逐个在hosts中指定的所有主机上执行。action
任务执行过程。handlers
用于当前关注的资源发生变化时采取一定指定的操作。
执行结果通过三种颜色表示:绿色代表执行成功,系统保持原样;黄色代表系统代表系统状态发生改变;红色代表执行失败,显示错误输出。
# cat web.yml
- hosts: test # 选择执行命令的主机组,在/etc/ansible/hosts定义
vars: # 设置的变量
http_port: 80
max_clients: 200
remote_user: root # 远端执行任务的用户,也可以使用sudo
tasks: # 任务
- name: install httpd # 任务描述
command: yum -y install httpd # 调用ansible的command模块安装httpd
when: ansible_distribution == "CentOS" # 设置执行条件
- name: provide httpd.conf # 任务描述
copy: src="/root/httpd.conf" dest="/etc/httpd/conf/httpd.conf"
tags: conf # 打标记用于单独执行标记任务,用ansible-playbook -C执行
notify: # 文件内容变更通知
- server restart # 通知到指定的任务
- name: server start # 任务描述
service: name=httpd state=started enabled=true
handlers: # 定义接受关注的资源变化后执行的动作
- name: server restart # 任务描述
service: name=httpd state=restarted # 当关注的资源发生变化后调用service模块
在执行时,首先会收集远端主机的元数据信息,也即 facts,包括了 IP 地址、CPU 核数、系统架构、主机名等信息,然后这些元数据可以作为变量供后面的 task 使用。
$ ansible host -m setup -i inventory_file
当然可以通过 gather_facts: no
选项关闭,经常被用在条件语句和模板当中,可以使用条件判断语句关闭指定发行版的主机。
Notify + Handler
ansible 还支持设置 handlers,也就是是在执行 tasks 之后可供调用的处理过程,使用起来如下,同样是本地执行:
$ cat /etc/ansible/hosts
localhost ansible_connection=local ansible_ssh_host=127.1 ansible_ssh_user=foobar
$ echo "Hello World, Ansible" > /tmp/foobar
$ cat playbook.yml
---
- hosts: localhost # hosts中指定
tasks:
- name: whoami
copy: src=/tmp/foobar dest=/tmp/foobar.copy # 本地拷贝到远端
notify: # 如果复制前发现foobar.copy文件改变了,则执行如下的handler
- clear copy
handlers:
- name: clear copy
shell: 'mv /tmp/foobar.copy /tmp/foobar.del' # 假装删除
上述判断 foobar.copy 改变暂时还不太清楚如何做到的,无论是缺少,还是内容被修改,都会执行。
调试
对于 Playbook 可以通过如下方式调试。
$ ANSIBLE_KEEP_REMOTE_FILES=1 ansible-playbook -i production site.yml \
--tags git --step --start-at-task='git | setup' -vvvv
其中各个参数如下:
- ANSIBLE_KEEP_REMOTE_FILES=1 阻止删除远端的临时文件,可以用来查看。
- 通过
--tags
和--start-at-task
设置执行那些任务,或者从那些任务开始。 --step
表示单步执行,每执行完一个任务都会暂停。- 使用多个
-v
参数,输出详细的调试信息。
另外,可以使用 debug 模块,方式如下:
- name: debug this
debug: var=result
还可以在配置文件中通过 log_path 指定日志文件路径。
输入密码
通过 ansible 连接远程主机时,默认是需要输入远程主机的密码的,这样会很不方便,可以有以下三种方式处理:
1、通过 -k 提示输入密码
实际上该参数是 --ask-pass
的简写形式,不过这种方式依赖于 sshpass 命令。
# yum --enablerepo=epel install sshpass
$ ansible all -m setup -k -u foobar -a "filter=ansible_os_family"
也可以在 playbook 文件中指定提示输入密码。
hosts: all
remote_user: root
vars_prompt:
- name: 'https_passphrase' # 存储数据的变量名
prompt: 'Key Passphrase' # 手工输入数据
private: yes # 当该值为yes,则用户的输入不会被打印
2、 将用户名密码保存在 inventory 文件中
一般 inventory 文件保存在 /etc/ansible/hosts
文件里,书写格式如下。
foobar ansible_ssh_host=127.1 ansible_ssh_user=root ansible_ssh_pass=yourpassword
3. 使用SSH免秘钥登录
实际上就是通过 SSH 的公私钥验证方式。
其它技巧
为了灵活的控制 playbook,ansible 提供了 ansible-playbook 命令行工具,如下是一些常用的示例,更过用法可以参考 ansible-playbook -h 。
指定开始 tasks
查看当前任务,然后指定从哪里开始执行任务。
$ ansible-playbook --list-tasks foobar.yml
$ ansible-playbook --start-at-task="foo bar" foobar.yml
只在某主机执行任务
可以通过如下方式执行,或者跳过某些任务。
$ ansible-playbook --limit=host1 --tags=foobar foobar.yml
$ ansible-playbook --limit=host1 --skip-tags=foobar foobar.yml
Vault
当使用 Ansible 自动化地维护服务器时,不可避免需要接触一些密码或其它敏感数据,我们可以使用三方的密码管理工具,如 HashiCorp 的 Vault、Square 的 Keywhiz、亚马逊的 Key Management Service 和微软 Azure 的 Key Vault。
当然也可以使用 Ansible 自带的 Vault 加密功能,可以将经过加密的密码和敏感数据同 Playbook 存储在一起,详细的内容可以查看
Python API
详细的可以参考官方文档 Python API,在 2.0 之前的版本,代码非常简单,如下:
import ansible.runner
runner = ansible.runner.Runner(
module_name='ping',
module_args='',
pattern='web*',
forks=10
)
datastructure = runner.run()
如下是 2.0 的接口使用示例。
#!/usr/bin/env python
#-*- coding: UTF-8 -*-
import json
from collections import namedtuple
from ansible.parsing.dataloader import DataLoader
from ansible.vars import VariableManager
from ansible.inventory import Inventory
from ansible.playbook.play import Play
from ansible.executor.task_queue_manager import TaskQueueManager
from ansible.plugins.callback import CallbackBase
class ResultCallback(CallbackBase):
"""A sample callback plugin used for performing an action as results come in
If you want to collect all results into a single object for processing at
the end of the execution, look into utilizing the ``json`` callback plugin
or writing your own custom callback plugin
"""
def v2_runner_on_ok(self, result, **kwargs):
"""Print a json representation of the result
This method could store the result in an instance attribute for retrieval later
"""
host = result._host
print json.dumps({host.name: result._result}, indent=4)
# 指定选项,可以在启动时指定,或者在/etc/ansible/ansible.cfg中指定
Options = namedtuple('Options', ['connection', 'module_path', 'forks', 'become',
'become_method', 'become_user', 'check'])
options = Options(connection='local', module_path='/path/to/mymodules', forks=100,
become=None, become_method=None, become_user=None, check=False)
# initialize needed objects
variable_manager = VariableManager()
loader = DataLoader()
passwords = dict(vault_pass='secret')
# Instantiate our ResultCallback for handling results as they come in
results_callback = ResultCallback()
# create inventory and pass to var manager
inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list='/etc/ansible/hosts')
variable_manager.set_inventory(inventory)
# create play with tasks
play_source = dict(
name = "Ansible Play",
hosts = 'localhost',
gather_facts = 'no',
tasks = [
dict(action=dict(module='shell', args='ls'), register='shell_out'),
dict(action=dict(module='debug', args=dict(msg='{{shell_out.stdout}}')))
]
)
play = Play().load(play_source, variable_manager=variable_manager, loader=loader)
# actually run it
tqm = None
try:
tqm = TaskQueueManager(
inventory=inventory,
variable_manager=variable_manager,
loader=loader,
options=options,
passwords=passwords,
stdout_callback=results_callback, # Use custom callback instead of the 'default'
)
result = tqm.run(play)
finally:
if tqm is not None:
tqm.cleanup()
源码解析
实际上 ansible 安装后,ansible-XXX 都是指向 ansible 的可执行文件,不过调用的时候会在 ansible 中进行判断,根据不同的命令调用 cli 目录下的文件。
参考
可以查看中文帮助 www.ansible.com.cn,源码参考 github - ansible。
详细的文档可以参考 Ansible Documentation 。