# Ansible学习教程
# 为什么使用Ansible
自动化运维为什么选择学习Ansible这个工具,而不选择其他的工具。
最主要的原因是我需要的工具必须要简单,包括安装简单,使用简单。
安装简单
这个工具安装简单(下面的章节会详细讲解安装方法),而且只要安装在一台机器上就可以了。被管理的机器不需要安装客户端。像SaltStack,Puppet不仅要安装服务端还要安装客户端,很麻烦。
使用简单
只需要一条命令就可以完成复杂的操作。安装完成以后,基本不用做任何配置。就可以直接使用。为什么说基本不用配置呢,因为被管理的机器还是要配置的(后面章节会详细讲解如何添加被控机器,不用担心,超级简单),要不然我怎么知道我要控制哪些机器呢?
下面是Ansible,Puppet, SaltStack 之间的对比
# 技术对比
名称 | Puppet | SaltStack | Ansible |
---|---|---|---|
开发语言 | Ruby | Python | Python |
客户端 | 有 | 有 | 无 |
二次开发 | 不支持 | 支持 | 支持 |
通信验证 | 是 | 是 | 是 |
通信加密 | 标准SSL协议 | AES加密 | OpenSSH |
平台支持 | AIX,BSD,HP-UX,Linux,MacOS, Solaris,Windows | BSD,Linux,MacOS, Solaris,Windows | AIX,BSD,HP-UX,Linux,MacOS, Solaris,Windows |
配置文件格式 | Ruby语法格式 | YAML | YAML |
Web UI | 提供 | 提供 | 提供(商用) |
命令执行 | 不支持(配置模块可实现) | 支持 | 支持 |
# 优劣势对比
名称 | 优势 | 劣势 | 成本 |
---|---|---|---|
Puppet | • 模块由Ruby或Ruby子集编写 • - push命令可以即可触发变更 • Web界面生成处理报表、资源清单、实时节点 管理 • 代理运行瑞进行详细、深入的报告和对节点进 行配置 | • 相对其他工具较复杂需要学习Puppet的DSL或Ruby • 安装过程缺少错误校验和产生错误报表 | • 开源免费 |
SaltStack | • 状态文件可以用简单YAML配置模块或复杂的Python/PyDSL脚本 • 与客户端可以基于SSH或在被管理节点安装代理 • Web界面可以看到运行的工作、minion状态、事件日志、可在客户端执行命令 • 扩展能力强 | • Web界面不完善 缺乏生成深度报告的能力 | • 开源免费 • SaltStack企业版收费 |
Ansible | • 模块可以用任何语言开发 • 备管节点不需要安装代理软件 • 有Web管理界面、可配置用户、组、资源清单 和执行Playbook | • 对备管理节点为Windows有待加强 • Web管理界面是内置的Ansible的一部 分 • 需导入资源清单 | • 开源版本免费 • Ansible Tower小于 10台被 管理节点免费 • 超过10台后每年每台需支付$100~$250的支持服务 费用 |
基础准备工作也就完成了,接下来就是就正式开启自动化运维学习之路。废话不多说,马上开始Ansible的学习。
# Ansible的安装
# CentOS 7安装Ansible
ansible在第三方源中,所以我们要安装第三方源
[root@localhost ~]# yum install -y epel-release
然后再安装ansible
[root@localhost ~]# yum install -y ansible
如果出现报错:Cannot retrieve metalink for repository: epel/x86_64. Please verify its path and try again
请修改epel源的配置文件
[root@localhost ~]# vi /etc/yum.repos.d/epel.repo
将第二行开头 “#” 号去掉,第三行开头加一个 “#” 号。保存退出。
然后如下执行
[root@localhost ~]# yum clean all
[root@localhost ~]# yum install -y ansible
2
3
等待安装完成即可。
安装完成后,查看版本。
[root@localhost ~]# ansible --version
ansible 2.9.16
config file = /etc/ansible/ansible.cfg
configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python2.7/site-packages/ansible
executable location = /usr/bin/ansible
python version = 2.7.5 (default, Jul 13 2018, 13:06:57) [GCC 4.8.5 20150623 (Red Hat 4.8.5-28)]
2
3
4
5
6
7
包含版本信息,工具配置文件路径,python版本等等。这说明已经安装成功
# Ubuntu安装Ansible
$ sudo apt update
$ sudo apt install software-properties-common
$ sudo apt-add-repository --yes --update ppa:ansible/ansible
$ sudo apt install ansible
2
3
4
# OpenEuler安装Ansible
dnf install ansible
其他发行版本请查看官方文档
https://docs.ansible.com/ansible/latest/index.html
# Ansible之初体验
用来管理的机器已经安装好了,被管理的机器还要安装什么吗? 郑重的告诉你,被管理的机器什么都不用安装,就是这么任性。
虽然被管理的机器什么都不用安装,但是我们要把被管理机器的 IP,用户名,密码 收集起来。 然后告诉管理机器。要不然管理机器不知道去管理谁。我们要怎么把被管理机器告诉告诉管理机器呢?
Ansible有个管理清单,只要把被管理的机器添加进去就可以了。
这个清单文件是:/etc/ansible/hosts 前面做对比介绍的时候说过, Ansible是通过ssh协议通信的,所以,我们要把被管理的机器IP,用户名,密码告诉管理机器。也就是把被管理机器的这些信息写进清单里面。
我们把 被管理的机器的IP ,用户名,密码按照下面格式填入/etc/ansible/hosts文件中
把下面的内容添加到/etc/ansible/hosts的最后一行,然后保存退出。
192.168.233.167 ansible_ssh_user=root ansible_ssh_pass=123456
说明: 为什么要添加到最后一行。里面其他的内容可以删掉么?里面原有的内容是可以删除的,但是不建议删除。因为清单有个分组的概念(后面会讲到分组的概念). 而原有的这些注释的内容,就是给我们的实例。我们按照这个实例根据需求配置我们自己的分组。
# 管理机与被管理机联通测试
被管理的主机已经添加到清单里面了,接下来就测试一下,管理机和被管理机是否联通。用一条命令即可,下面的命令在管理机操作
[root@localhost ~]# ansible 192.168.233.167 -m ping
192.168.233.167 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": false,
"ping": "pong"
}
2
3
4
5
6
7
8
说明管理机与被管理机已经可以正常通信了。返回的信息颜色是绿色的,执行的命令返回是正确的。 如果执行命令失败,显示是红色的。所以很容易就判断出执行的命令返回是正确的还是错误。
细心的同学可能发现了,我们在配置清单的时候,把被管理机的用户名和密码全部填写进去了。万一管理被入侵,其他的被管理机器一下全暴露了。
# 管理机与被管理机的免密码配置
这里我们就引入了密钥的概念。管理机和被管理机通信使用密钥代替密码,这样就提高了安全性。具体操作步骤如下:
步骤一: 在管理机器生成公钥
[root@localhost ~]# ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:IrRLAe0wt1pDfZjbzI16ss79M6Hp/O/7aLSXyGQtcBo root@localhost.localdomain
The key's randomart image is:
+---[RSA 2048]----+
| .. . o |
| o.+ + . |
| *o. * o |
| .=o. = E . |
| o+...S = . |
| .. oo.. o = . |
| . + o * + . |
| ..oo o =.o |
| .o.+oo*=+. |
+----[SHA256]-----+
[root@localhost ~]#
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
步骤二: 把公钥复制到被管理机器
[root@localhost ~]# ssh-copy-id root@192.168.233.167
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/root/.ssh/id_rsa.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
root@192.168.233.167's password:
Number of key(s) added: 1
Now try logging into the machine, with: "ssh 'root@192.168.233.167'"
and check to make sure that only the key(s) you wanted were added.
2
3
4
5
6
7
8
9
10
输入被管理机器密码即可。
聪明的同学可能马上就想到了,一台机器这样复制还行,被管理的机器少则几十台,多则几百几千台。一个一个的复制岂不是要累死了。几百几千台当然不可能这样操作了。我们用个小工具sshpass,写个小脚本来完成这个重复的工作。
其实前面在/etc/ansible/hosts配置user和password的时候,ansible就是调用sshpass来自动输入ssh密码的。
sshpas 需要单独安装,首先要下载:http://sourceforge.net/projects/sshpass/ (opens new window)
安装sshpass会用到gcc,所以要先安装gcc
[root@localhost ~]# yum install gcc -y
[root@localhost ~]# tar -xvf sshpass-1.06.tar.gz
[root@localhost ~]# cd sshpass-1.06/
[root@localhost ~]# ./configure
[root@localhost ~]# make && make install
2
3
4
5
6
7
8
9
接下来写脚本copysk.sh。
#!/bin/bash
#
password=123456
for ip in $(cat ip)
do
sshpass -p ${password} ssh-copy-id root@${ip}
done
2
3
4
5
6
7
脚本文件copysk.sh和ip文件在同一目录下。然后执行脚本即可
[root@localhost ~]# bash copysk.sh
如果不知道如何写脚本,这上面的复制修改password的值,再把ip文件中的IP改为你的IP即可。
密钥已经配置好了,管理机上的清单文件也要改一下。
打开清单文件如下:
[root@localhost ~]# vi /etc/ansible/hosts
192.168.1.1
192.168.1.2
2
3
把IP后面的ansible_ssh_user 和 ansible_ssh_pass删除,只保留前面的IP就行了。
# Ansible主机清单和分组管理
前面说到清单分组的概念,如果忘记也没关系。接下来说一下,为什么要分组,如何把被管理的机器分组。
如果你之前了解过Ansible,你可能找到更多的是Inventory。 其实这个文件保存的就是被控主机的信息。
首先说一下为什么要分组。
比如说我管理了100台机器。其中的10台做web服务器。5台做数据库服务器,剩下的全部用来做数据运算。 今天领导让我把10台web服务器更新一下,我如果把web服务器的IP一个一个单独的拿出来,单独的去操作。 这样做是不是特别傻。
所以ansible的开发者就想到了一个分组的概念。 把功能一类的机器分成一组,然后通过组名管理组内的若干台机器。
比如分一个web服务器组,包含主机192.168.1.10、192.168.1.11. 我们可以在清单里进行如下配置:
[web]
192.168.1.10
192.168.1.11
2
3
如此配置就可对web组进行管理,示例如下:
[root@localhost ~]# ansible web -m ping
192.168.1.10 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": false,
"ping": "pong"
}
192.168.1.11 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": false,
"ping": "pong"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
到此为止,ansible安装好了,该配置的也已经配置好了。具体的实际应用到现在一点都没有说到。 下面的内容将结合实际生产应用来教大家如何使用ansible
ansible是基于模块化设计的,所以有很多的模块可以供我们使用。
比如前面用到的命令:
ansible web -m ping #就是调用**ping**模块测试管理机与被管理机是否能ping通
ansible-doc -l #命令可以列出所有的模块
ansible-doc ping #就可查看ping模块的用法
ansible-doc copy #就可以查看copy模块的用法。
2
3
4
5
6
7
如果你英文还不错,完全可以查看此文档学习每个模块的用法。 英文不太好的话,也不用担心。
后面会把常用的模块都拿出来,一 一讲解,并给出一个或多个例子供参考。
虽然模块很多,但是用到的并不多,接下来就开始常用模块的学习。
# Ansible模块学习
# command 模块
command模块顾名思义,就是执行命令的模块。这模块可以帮助我们在被管理机器上执行命令
例如:
查看被管理机器root用户目录下的文件
[root@localhost ~]# ansible 192.168.146.129 -m command -a "ls /root"
这个模块很简单,就像本地执行命令一样,但是执行的命令中含有重定向、管道符等操作时,会显示红色报错信息。
比如**"<", ">", "|", ";" 和 "&" ** 这些符号。
如果必须用到怎么办呢?
当然有办法,接下来就来介绍Ansible模块学习之shell 模块
# shell 模块
shell模块同样是在远程机器上执行命令但是不同的是,shell 模块在远程主机中执行命令时,会经过远程主机上的/bin/sh程序处理。也因此shell模块支持更多的操作符,比如介绍command模块提到的重定向”<”, “>”, 管道符“|”。
例1: 我想查看远程机器var目录下面一共有多少个文件
[root@localhost ~]# ansible 192.168.146.129 -m shell -a "ls /var |wc -l"
返回22,说明远程机器/var目录下一共有22个文件
例2: 我想查看远程机器/var目录下面是否有test文件或文件夹
[root@localhost ~]# ansible 192.168.146.129 -m shell -a "ls /var |grep test"
返回红色失败信息,说明远程机器的/var目录下没有test文件或文件夹
上面的2个例子都是使用shell模块并用到了管道 “|”。如果使用command模块并使用管道符号会有什么样的效果呢?我们来看一下。
例3: 使用command模块查看远程机器上/var目录下一共有多少个文件
[root@localhost ~]# ansible 192.168.146.129 -m command -a "ls /var |wc -l"
结果是只执行了管道“|”前面的命令,最后一行显示执行管道后面的命令直接报错。
这时候你可能会想shell模块可以满足我使用一些复杂的了命令了。但是还满足不了我的需求,我要执行在远程机器上执行脚本怎么办呢?我要把脚本拷贝到远程机器上再执行吗?
虽然方法可以,但是这也太麻烦了。根本符合自动化理念。
其实开发者早就想到这个问题了。这也印证那句话:“我们现在考虑的问题,90%以上都是前人考虑过的。”因此开发者就开发了script模块来解决远程执行脚本的问题。接下来就讲解一下script模块。
# script模块
看到script就知道这个模块和脚本有关系。没错,script模块功能就是本地的脚本,可以在远程机器上执行。也就是说,脚本一直在本地,不用拷贝到远程机器上。我们做个小实验来介绍script模块的用法。
首先在管理机器上我们写个很简单的脚本test.sh,在/root目录下。脚本内容如下:
#!/bin/bash
touch /tmp/testfile
echo "123" > /tmp/testfile
2
3
脚本的内容很简单,就是在tmp目录下创建一个testfile文件,然后把”123“写入test file文件中。
接下来我们就使用script模块,把本地的脚本在远程机器上执行
[root@localhost ~]# ansible 192.168.146.129 -m script -a "/root/test.sh"
我们可以看到已经正常执行完成了。我们在使用shell或者command模块来查看远程机器是否有testfile文件,里面的内容是不是”123“.
[root@localhost ~]# ansible 192.168.146.129 -m command -a "cat /tmp/testfile"
可以看见远程机器上有对应的文件,文件中的内容也是对的,
上面的小实验就介绍了script用法。非常简单。
小贴士: 在本地执行脚本的时候要给脚本执行的权限才能执行。使用script模块时,脚本即使没有执行权限也可以在远程机器上执行的。
细心的同学可能发现了,并且产生疑问。
我们在本地执行命令的时候还会加个参数啥的,比如:ls -l ,rm -rf 等等。
为什么我们上面的使用各个模块的时候没有添加其他的参数呢?是没有参数吗?
这里要说明 一下。上面三个模块都有参数,但是基本上不会使用到。反正到现在,使用上面的三个模块我也没加过任何参数。所以我就直接省略掉。避免一些同学看到这么简单模块都要加参数。而丧失学习兴趣。
后面的模块讲解,我们也只会讲解常用的模块,不常用的模块简单提一下,或者直接就不讲。同样,模块的参数也是只讲常用的,不常用的就不讲,或一笔带过。所以你在看这本书的时候不要有这样的担心。我看了一堆的东西,结果没什么用。这是你不用担心的。我的目的就是写出有用的东西。不会写一些没用的东西来凑字数。也不会用一些高大上的词汇来显示自己很牛的样子。
我的目的是让你一点都不懂到入门。如果你入门了,我的目的就达到了。接下来的深入学习你就知道该如何去学习,去搜索资料。
# user模块
做运维的都知道,同一台机器可以有很多用户登陆。我们要创建用户,删除用户,给用户附加组等等。这些都会用到user模块。下面对user模块和参数的使用结合实例进行讲解
name参数: 这个是必须参数,指定要操作用户的名称
group参数: 指定用户所在的基本组
shell参数: 指定用户默认登陆shell
uid参数: 指定用户uid,一般不做设置
comment参数: 用作用户的注释信息,
state参数: 指定用户是否在远程机器,有2个可选值。默认是present,用户存在远程机器上,absent删除远程机器上的用户
remove参数: 当我们删除用户,也就是state参数值是absent的时候。默认是不会删除家目录的。也就是remove值默认是no,如果要删除家目录remove值yes
password参数: 用于设置用户密码 ,但是这个密码不是明文的,而是加密后的和/etc/shadow文件中密码字段一样
参数介绍就到这里,下面将用实例讲解各参数的用法
例1: 创建名称为test的用户
[root@localhost ~]# ansible 192.168.146.129 -m user -a "name=test"
可以从返回参数看出创建了用户test,uid是1000,groupid也是1000,家目录是/home/test 使用的shell 是/bin/bash
如果test用户已经存在了,则不做任何操作,如下:
[root@localhost ~]# ansible 192.168.146.129 -m user -a "name=test"
可以看到changed的值是false,说明没有做任何改变。
例2: 我要删除刚才创建的test用户。并且把它的家目录也一起删了
[root@localhost ~]# ansible 192.168.146.129 -m user -a "name=test state=absent remove=yes"
如果我不想删除用户的家目录怎么操作呢?直接把remove=yes去掉就可以保留用户的家目录了
如果当前用户是登陆状态,会删除失败,有这样的提示 "msg": "userdel: user test is currently used by process 2533\n"
例3: 安装apache要创建apache用户,但是这个用户是不需要登陆的,我们要如何做呢?这里就要指定登陆shell了,而指定的shell值是nologin。意思是不允许apache这个用户登陆
[root@localhost ~]# ansible 192.168.146.129 -m user -a "name=apache shell=nologin"
这样就创建apache用户了,而且无法用这个用户登陆系统。因为无法登陆系统所以是没有家目录的。
如果指定apache用户使用/bin/sh作为默认shell如下操作即可
[root@localhost ~]# ansible 192.168.146.129 -m user -a "name=apache shell=/bin/sh"
例4: 刚才创建了test用户,我需要做个说明,这是一个测试用户,如下操作:
[root@localhost ~]# ansible 192.168.146.129 -m user -a "user=test comment=Thistestuser"
然后我们查看一下远程机器上的/etc/passwd文件
[root@localhost ~]# ansible 192.168.146.129 -m shell -a "grep test /etc/passwd"
可以看到"Thistestuser"字段
用户创建好了,是时候给用户添加个密码了。这里用到了password参数,前面做参数说明的时候讲到了,这个密码不是明文的。是加密的,因此我们要把密码的明文经过加密处理才能使用。
比如:我想给test用户设置密码为123456,应该怎么做呢?下面跟我一步一步的操作即可。
首先进入到python命令行界面。用python的crypt模块对字符串进行加密,最后返回的字符串就是我们要用到的,注意字符串是用单引号引起来的,我们只要单引号里面的字符串
具体操作步骤如下:
$ python -c "import crypt; print(crypt.crypt('123456'))"
$6$xXHy1LuASc7e9tbO$38vx7.ZvpllcavRxvQ1z1GMBwGx5C3eTR2H8Xofbiq8/hwaj8cvWfM6D63agNMWeL6vu2kQ2lszum0JBQRejD/
2
加密后的密码已经生成了,我们直接拿来用就可以了。
例5: 给test用户设置密码为123456
ansible 192.168.146.129 -m user -a 'user=test password="$6$xXHy1LuASc7e9tbO$38vx7.ZvpllcavRxvQ1z1GMBwGx5C3eTR2H8Xofbiq8/hwaj8cvWfM6D63agNMWeL6vu2kQ2lszum0JBQRejD/" '
这里有个地方必须要注意的,执行的整条命令是单引号引起来的,密码内容是双引号引起来的。这样做的原因是,加密后的密码内容包含了一些特殊符号,必须要用双引号引起来,避免产生歧义。
user模块常用的参数和参数值的使用已经讲完了,接下来讲解file模块
# file模块
file模块一看就知道这个模块和文件操作相关。比如创建文件或文件夹,删除文件或文件夹,修改文件或文件夹的权限。
下面对file模块常用参数介绍一下,然后结合实例讲解用法。
**path参数:**这参数是必须的,它指明了被管理的文件或文件夹的路径
**state参数:**这个参数很灵活,不同的模块都会有state参数,模块不一样它的值也不一样,后面回结合实例说明state参数不同的值作用和它的作用。
src参数:做软链接或硬链接的时候指定链接的源文件
例1:在/tmp目录下创建文件mytest 权限位0644
[root@localhost ~]# ansible 192.168.233.167 -m file -a "path=/tmp/mytest state=touch mode=0644"
192.168.233.167 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": true,
"dest": "/tmp/mytest",
"gid": 0,
"group": "root",
"mode": "0644",
"owner": "root",
"size": 0,
"state": "file",
"uid": 0
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
例2: 删除刚才创建的mytest文件
[root@localhost ~]# ansible 192.168.233.167 -m file -a "path=/tmp/mytest state=absent"
192.168.233.167 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": true,
"path": "/tmp/mytest",
"state": "absent"
}
2
3
4
5
6
7
8
9
例3: 在/tmp目录下创建文件夹dirtest 权限是0755
[root@localhost ~]# ansible 192.168.233.167 -m file -a "path=/tmp/dirtest state=directory mode=0755"
192.168.233.167 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": true,
"gid": 0,
"group": "root",
"mode": "0755",
"owner": "root",
"path": "/tmp/dirtest",
"size": 4096,
"state": "directory",
"uid": 0
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
例4: 创建软连接相当Linux命令"ln -s"
[root@localhost ~]# ansible 192.168.233.167 -m file -a "src=/tmp/dirtest dest=/tmp/testlink state=link"
192.168.233.167 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": true,
"dest": "/tmp/testlink",
"gid": 0,
"group": "root",
"mode": "0777",
"owner": "root",
"size": 12,
"src": "/tmp/dirtest",
"state": "link",
"uid": 0
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
以上就是file模块常用的几种方式了。
主要就是state值的区别,下面总结一下:
**touch:**创建文件
**absent:**删除文件或目录
**directory:**创建目录
link:创建软链接。如果创建硬链接state的值就是hard
注意:mode值的前面的0不要省略掉,要写成"0755"或"0644"而不是"755"和"644"。
# copy模块
copy模块是把本机的文件拷贝到远程机器,跟scp命令有点类似
常用参数如下:
src参数: 本地文件路径。必须参数
dest参数: 远程机器路径。必须参数
backup参数: 是否备份,若值是yes表示备份,如果不需要备份省略该参数。省略该参数后,若远程机器有同名的文件,远程机器上的文件会直接覆盖
mode参数: 复制后的文件权限,比如:"0755","0644"。如果没有该参数则复制后的文件与源文件权限相同
**例1:**把本地拷贝到远程机器不做备份,
[root@localhost ~]# ansible 192.168.233.167 -m copy -a "src=/root/test dest=/tmp/test"
192.168.233.167 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": true,
"checksum": "4e1243bd22c66e76c2ba9eddc1f91394e57f9f83",
"dest": "/tmp/test",
"gid": 0,
"group": "root",
"md5sum": "d8e8fca2dc0f896fd7cb4cb0031ba249",
"mode": "0644",
"owner": "root",
"size": 5,
"src": "/root/.ansible/tmp/ansible-tmp-1611939995.84-4445-83659486471571/source",
"state": "file",
"uid": 0
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
例2: 把本地文件拷贝到远程机器并备份
[root@localhost ~]# ansible 192.168.233.167 -m copy -a "src=/root/test dest=/tmp/test backup=yes"
192.168.233.167 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"backup_file": "/tmp/test.2953.2021-01-29@17:08:14~",
"changed": true,
"checksum": "1b7d20fc0014599208b7ccea75f6f0c959af283b",
"dest": "/tmp/test",
"gid": 0,
"group": "root",
"md5sum": "80b9455d8672a7e9a2c5120462b2963d",
"mode": "0644",
"owner": "root",
"size": 10,
"src": "/root/.ansible/tmp/ansible-tmp-1611940092.92-4528-54138861592249/source",
"state": "file",
"uid": 0
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
此时,返回值会多一个"back_file"
如果本地文件内容和远程文件一样,则不会复制。返回内容也是深绿色的 ansible在复制文件的时候会判断文件内容是否相同
关于ansible执行返回的颜色简单说明一下:
绿色: 表示执行成功但是没做任何修改 黄色: 表示执行成功并做了修改 红色: 表示执行失败 浅绿色: 表示跳过此次操作
有一个参数需要拿出来单独说一下,用到的可能不多,但可能会用到。
**remote_src参数:**这个参数表示操作都是在远程机器上操作
此时的src表示的也是远程机器的路径,而不是本地的。dest也是远程机器的路径.
例1: 远程机器上的文件拷贝
[root@localhost ~]# ansible 192.168.233.167 -m copy -a "src=/tmp/dirtest dest=/root/dirtest backup=yes remote_src=yes"
192.168.233.167 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": true,
"checksum": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
"dest": "/root/dirtest",
"gid": 0,
"group": "root",
"md5sum": "d41d8cd98f00b204e9800998ecf8427e",
"mode": "0644",
"owner": "root",
"size": 0,
"src": "/tmp/dirtest",
"state": "file",
"uid": 0
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
上面示例因为remote_src=yes, 所以src=/tmp/dirtest 和dest=/root/dirtest的两个路径都是在远程机器上,
### Ansible模块学习之fetch模块
copy模块是把本机的文件拷贝到远程机器,而fetch模块是把远程机器的文件拉取到本地
fetch模块刚好和copy模块的操作是相反的。因此参数的含义也是相反的
src参数: 远程机器上的文件或文件夹
dest参数: 保存到本地的目录
**例1:**拷贝远程文件到本地
[root@localhost ~]# ansible 192.168.233.167 -m fetch -a "src=/etc/hosts dest=/home/"
192.168.233.167 | CHANGED => {
"changed": true,
"checksum": "68991e742192b6cc45ad7b95eb88ea289658f65c",
"dest": "/home/192.168.233.167/etc/hosts",
"md5sum": "32d544b36aefb5a7f800e75cca57ce8b",
"remote_checksum": "68991e742192b6cc45ad7b95eb88ea289658f65c",
"remote_md5sum": null
}
2
3
4
5
6
7
8
9
在本地/home目录下会生成远程机器IP的文件夹,拉取的文件就在该目录下。并且保留目录结构
[root@localhost ~]# tree /home/192.168.233.167/
/home/192.168.233.167/
└── etc
└── hosts
2
3
4
这个模块很简单,就不过多赘述。
# cron模块
Cron模块用来管理crontab的,包括添加、删除、更新操作系统的crontab任务计划
name参数: 计划任务名称
**job参数:**指定计划的任务中需要实际执行的命令或者脚本
**user参数:**指定计划任务属于哪个用户,默认是root用户
**state参数:**当计划任务有名称时,根据计划任务名称修改删除对应的任务,删除计划任务state值为absent
**backup参数:**对已有的任务修改或删除时,是否保存
**disabled参数: ** 当计划任务有名称时,根据计划任务名称关闭(注释)对应的计划任务
minute参数:设置计划任务中分钟设定位的值,取值范围(0-59,*, */2)
**hour参数:**设置计划任务中小时设定位的值,取值范围(0-23,*,*/2)
**day参数:设置计划任务中天(日)**设定位的值,取值范围(1-31,*,*/2)
month参数:设置计划任务中月份设定位的值,取值范围(1-12,*,*/2)
**weekday参数:**设置计划任务中周几设定位的值,取值范围(0-6 for Sunday-Saturday, *)
**例1:**创建名称为ntpdate的计划任务,每天凌晨1点5分同步时间
[root@localhost ~]# ansible 192.168.233.167 -m cron -a "name='ntpdate' minute=5 hour=1 job='ntpdate ntp.aliyun.com'"
192.168.233.167 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": true,
"envs": [],
"jobs": [
"ntpdate"
]
}
2
3
4
5
6
7
8
9
10
11
查看一下添加后效果
[root@localhost ~]# ansible 192.168.233.167 -m shell -a "crontab -l"
192.168.233.167 | CHANGED | rc=0 >>
#Ansible: ntpdate
5 1 * * * ntpdate ntp.aliyun.com
2
3
4
已经添加成功
**例2:**把刚才创建的计划给关闭掉
[root@localhost ~]# ansible 192.168.233.167 -m cron -a "name='ntpdate' minute=5 hour=1 job='ntpdate ntp.aliyun.com' disabled=yes"
192.168.233.167 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": true,
"envs": [],
"jobs": [
"ntpdate"
]
}
2
3
4
5
6
7
8
9
10
11
查看一下结果
[root@localhost ~]# ansible 192.168.233.167 -m shell -a "crontab -l"
192.168.233.167 | CHANGED | rc=0 >>
#Ansible: ntpdate
#5 1 * * * ntpdate ntp.aliyun.com
2
3
4
已经被注释掉了
注意:如果使用disabled参数没有设置时间参数,或时间参数设置错误时,计划任务会被注释掉的同时,时间也会被修改,如下:
[root@localhost ~]# ansible 192.168.233.167 -m shell -a "crontab -l"
192.168.233.167 | CHANGED | rc=0 >>
#Ansible: ntpdate
#* * * * * ntpdate ntp.aliyun.com
2
3
4
可以看到,时间全部变为 "*"了
例3:把刚才添加的ntpdate任务删除并备份
[root@localhost ~]# ansible 192.168.233.167 -m cron -a "name='ntpdate' state=absent backup=yes"
192.168.233.167 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"backup_file": "/tmp/crontabRvJZBw",
"changed": true,
"envs": [],
"jobs": []
}
2
3
4
5
6
7
8
9
10
显示已经把名称为"ntpdate"的计划任务保存到/tmp/crontabRvJZBw
我们先用 "crontab -l" 查看一下计划任务
[root@localhost ~]# ansible 192.168.233.167 -m shell -a "crontab -l"
192.168.233.167 | CHANGED | rc=0 >>
2
3
显示为空,说明计划任务已经被删除了。
我们再看一下备份文件
[root@localhost ~]# ansible 192.168.233.167 -m shell -a "cat /tmp/crontabRvJZBw"
192.168.233.167 | CHANGED | rc=0 >>
#Ansible: ntpdate
5 1 * * * ntpdate ntp.aliyun.com
2
3
4
确实是我们在例1中创建的计划任务
建议:删除和关闭计划任务的时候,把backup=yes一起加上,即使操作错了还有备份
还有一个参数这里提一下special_time参数。
special_time参数:
计划任务的时间设定格式为@reboot或者@hourly,@reboot表示重启时执行,@hourly表示每小时执行一次,相当于设置成"0 * * * *" ,这种@开头的时间设定格式则需要使用special_time参数进行设置.
special_time参数的可用值有:
reboot 重启后
yearly 每年
annually 每年,与yearly相同
monthly 每月
weekly 每周
daily 每天
hourly 每时
# yum&apt模块
yum模块是RHEL系的操作系统的软件包管理工具,但是这个模块只用在Python2.x中,Python3.x中要用dnf模块。这里不做讲解,和yum模块用法一样。
apt模块是Debian系的操作系统的软件包管理工具。
常用的参数如下,且用法一下,这里就不分开讲了。根据被管理机的系统自己选择使用相应的模块即可
**name参数:**软件包名称,比如:nginx
**state参数:**软件包状态,present表示安装,absent表示删除
**update_cache参数:**安装软件前是否更新缓存,如果update_cache=true表示安装前更新,默认是不更新
**例1:**安装nginx
[root@localhost ~]# ansible 192.168.233.167 -m apt -a "name=nginx state=present"
192.168.233.167 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"cache_update_time": 1612164115,
"cache_updated": false,
"changed": true,
"stderr": "",
"stderr_lines": [],
...
2
3
4
5
6
7
8
9
10
11
**例2:**删除刚才安装的nginx
[root@localhost ~]# ansible 192.168.233.167 -m apt -a "name=nginx state=absent"
192.168.233.167 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": true,
"stderr": "",
"stderr_lines": [],
"stdout": "Reading package lists...\nBuil
...
2
3
4
5
6
7
8
9
10
yum和apt模块就写这么多。
# service模块
service模块用来管理远程主机上的服务,支持的init系统包括BSD init、OpenRC、SysV、Solaris SMF、,
systemd,upstart。对于Windows目标,请改用**[win_service]**模块
**name参数:**服务名称,比如nginx
**state参数:**指定服务状态,可选值为:started、stopped、restarted、 reloaded
**enabled参数:**值为yes时,指定服务设置为开机启动,值为no时,指定服务不开机启动
**例1:**启动nginx服务
[root@localhost ~]# ansible 192.168.233.167 -m service -a "name=nginx state=started"
192.168.233.167 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": true,
"name": "nginx",
"state": "started",
"status": {
"ActiveEnterTimestamp": "Mon 2021-02-01 15:44:12 CST",
"ActiveEnterTimestampMonotonic": "344563040949",
"ActiveExitTimestamp": "Mon 2021-02-01 15:45:51 CST",
"ActiveExitTimestampMonotonic": "344661708600",
"ActiveState": "inactive",
"After": "network.target system.slice basic.target sysinit.target systemd-journald.socket",
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
**例2:**停止nginx服务
[root@localhost ~]# ansible 192.168.233.167 -m service -a "name=nginx state=stopped"
192.168.233.167 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": true,
"name": "nginx",
"state": "stopped",
"status": {
"ActiveEnterTimestamp": "Mon 2021-02-01 15:57:59 CST",
"ActiveEnterTimestampMonotonic": "345389576503",
"ActiveExitTimestamp": "Mon 2021-02-01 15:45:51 CST",
"ActiveExitTimestampMonotonic": "344661708600",
"ActiveState": "active",
"After": "network.target system.slice basic.target sysinit.target systemd-journald.socket",
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
**例3:**将nginx设置为开机启动
[root@localhost ~]# ansible 192.168.233.167 -m service -a "name=nginx enabled=yes"
192.168.233.167 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": true,
"enabled": true,
"name": "nginx",
"status": {
"ActiveEnterTimestampMonotonic": "0",
"ActiveExitTimestampMonotonic": "0",
"ActiveState": "inactive",
"After": "network.target basic.target sysinit.target system.slice systemd-journald.socket",
...
2
3
4
5
6
7
8
9
10
11
12
13
14
# Ansible 常用模块学习总结和思考
细心的同学会发现,前面几节常用模块的示例中每次都执行了单步操作也叫Ad-hoc,那么可不可以一次性执行多步操作呢?
学习过shell的同学都知道,shell执行多个命令,并添加一些逻辑判断。
Ansible当然也可以,那就是ansible-playbook。它会像剧本一样执行我们写好的yml文件。
前面几节的内容学习了常用的一些模块,在后面学习ansible-playbook的时候还会穿插着讲解其他的模块,接下来就进入ansible-playbook部分的学习,在实际应用中这一块用的是最多的,所以接下来的的学习很重要。
也希望学习的同学通过此次的学习,可以打牢基础,学习到更多的内容。
# 初时ansible-playbook
# 什么是playbook
playbook是由单个ad-hoc编排而成,并且加入一些逻辑判断,和变量引用。它非常适合部署复杂的应用程序。
playbook是以yaml格式表示,因此在写playbook的时候要遵守yaml语法格式。
之所以使用YAML,是因为与其他常见的数据格式(例如XML或JSON)相比,人类更容易读写。
学习playbook之前,我们先简单学习一下yaml语法。
# YAML语法
所有YAML文件(无论是否与Ansible有关)都可以选择以---
和开头...
。表示文档的开始和结束。
列表中的所有成员都是以相同的缩进级别开头的行,并以(破折号和空格)开头:"- "
---
# A list of fruits
- Apple
- Orange
- Strawberry
- Mango
2
3
4
5
6
创建的文件以yaml或yml为后缀。比如: tasks.yaml
字典以简单的形式表示(冒号后面必须有一个空格):key: value
---
#学生信息
student:
name: xiaoming
age: 18
sex: male
2
3
4
5
6
字典和列表结合使用
---
#学生信息
- student_1:
name: xiaoming
age: 18
sex: male
hobbies:
- listenMusic
- readingBook
- playGames
- student_2:
name: xiaohua
age: 16
sex: female
hobbies:
- running
- makeFriend
- sleeping
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
如果值有多行内容可以使用 |,使用这个 | 将忽略掉缩进,如下:
- name: add IP resolution
blockinfile:
path: /etc/hosts
block: |
192.168.1.1 node1
192.168.1.2 node2
2
3
4
5
6
blockinfile是ansible的一个模块,它的作用是把一段单行或多行文本添加到文件中。 别急!!!后面会讲到。
上面的yaml语句的意思是:把IP解析添加到/etc/hosts文件中
yaml语法特别简单,而且符合人类的读写习惯。其他的就不多说了
验证语法可以到 http://www.yamllint.com/
接下我们我们回到ansible中,在学习playbook的过程中,进一步熟悉yaml语法
# playbook组成
hosts: 远程主机或主机组
user: 执行任务的用户
sudo: 如果用户不是root需要sudo权限
gather_facts: 获取远程主机facts信息。默认是打开的(就是不写这个参数。关闭的时候这个必须写,且值是no
task: 任务开始
name: 任务名称,在屏幕上显示的信息
我们写一个test.yaml来感受一下。test.yaml内容如下
---
- hosts: 192.168.233.167 #远程机器的ip也可以是组名
remote_user: root #远程机器的用户
#sudo: yes
#gather_facts: no
tasks:
- name: copy file # 任务名称,在执行任务的时候显示的TASK
copy:
src: /root/testFile #本地文件
dest: /tmp/ # 远程机器目录
2
3
4
5
6
7
8
9
10
执行一下
[root@localhost ~]# ansible-playbook test.yaml
PLAY [192.168.233.167] ********************************************************************************************************
TASK [Gathering Facts] ********************************************************************************************************
ok: [192.168.233.167]
TASK [copy file] *********************************************************************************************************
changed: [192.168.233.167]
PLAY RECAP *********************************************************************************************************
192.168.233.167 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
2
3
4
5
6
7
8
9
10
11
12
一个最基本的playbook就完成了
# handlers 使用
handlers 和 notify 是结合使用的,那么,在什么时候会用到呢?
举一个例子,我们在修改nginx监听端口的时候要nginx -s reload
一下才能生效。
我们按住正常的流程是先修改 nginx端口,再重启nginx。示例如下:
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- name: modify nginx listen port
replace:
path: /etc/nginx/sites-enabled/default #ubuntu用apt安装的nginx端口配置文件位置
regexp: 'listen 80' #匹配需要修改的内容
replace: 'listen 880' #修改匹配到的内容
#backup: yes 这里不做备份,因为ubuntu用apt安装nginx配置有点特殊,会导致nginx无法重启
- name: restart nginx
service:
name: nginx
state: restarted
2
3
4
5
6
7
8
9
10
11
12
13
14
这样写是没问题的。执行一下看看:
[root@localhost ~]# ansible-playbook modify.yaml
PLAY [192.168.233.167] ********************************************************************************************************
TASK [Gathering Facts] ********************************************************************************************************
ok: [192.168.233.167]
TASK [modify nginx listen port] ***********************************************************************************************
changed: [192.168.233.167]
TASK [restart nginx] *********************************************************************************************************
changed: [192.168.233.167]
PLAY RECAP *********************************************************************************************************
192.168.233.167 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
执行也是ok的。
查看一下结果
[root@localhost ~]# ansible 192.168.233.167 -m shell -a "ss -tpln|grep 880"
192.168.233.167 | CHANGED | rc=0 >>
LISTEN 0 128 *:880 *:* users:(("nginx",pid=40284,fd=6),("nginx",pid=40283,fd=6),("nginx",pid=40282,fd=6))
2
3
可以看到端口已经改为880了。
那么用handlers 和notify应该怎么写playbook呢?我们写一个handlers.yaml文件,如下:
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- name: modify nginx listen port
replace:
path: /etc/nginx/sites-enabled/default #ubuntu用apt安装的nginx端口配置文件位置
regexp: 'listen 80' #匹配需要修改的内容
replace: 'listen 880' #修改匹配到的内容
#backup: yes 这里不做备份,因为ubuntu用apt安装nginx配置有点特殊,会导致nginx无法重启
notify: restart nginx #注意notify的层级和replace是同一层的
handlers: #handlers的层级是和"- name"同一层级的
- name: restart nginx
service:
name: nginx
state: restarted
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
注意: notify的 名称一定要和handlers的 name 相同
执行一下看看结果:
[root@localhost ~]# ansible-playbook handlers.yaml
PLAY [192.168.233.167] ********************************************************************************************************
TASK [Gathering Facts] ********************************************************************************************************
ok: [192.168.233.167]
TASK [modify nginx listen port] ***********************************************************************************************
changed: [192.168.233.167]
RUNNING HANDLER [restart nginx] ***********************************************************************************************
changed: [192.168.233.167]
PLAY RECAP *********************************************************************************************************
192.168.233.167 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
可以看到多了一个 RUNNING HANDLER ,这个就是handlers的用法。
有的同学就纳闷了,这个跟正常流程的没有区别啊。对,这样看来确实没有区别。
我们知道,ansible在执行时会进行判断,目标与想要修改的状态是否一致,如果不一致则修改,一致则不修改。这个也叫做幂等性 。
因此ansible的同一条命令是可以重复执行的。不过有一种操作例外,就是往文件里面添加字符,每次操作都会添加一次 。
这个跟handlers有什么关系呢?
handlers的触发机制是,只有notify的任务执行返回的状态是 changed 的时候才会触发handlers。否则不会触发handlers的。
如果按照正常流程写一个重启nginx的任务,那么就是无论端口是否修改它都会重启一次nginx,重启是为了使新的配置生效的。
配置文件都没有修改,还去重启的话,这样是没有必要的。这也就是为什么使用handlers。
重新运行刚才的handlers.yaml文件看看结果
[root@localhost ~]# ansible-playbook handlers.yaml
PLAY [192.168.233.167] *********************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [192.168.233.167]
TASK [modify nginx listen port] *********************************************************************************************************
ok: [192.168.233.167]
PLAY RECAP *********************************************************************************************************
192.168.233.167 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
2
3
4
5
6
7
8
9
10
11
12
可以看到最下面的changed的值是0,说明没有做修改,也没调用handlers重启nginx,没有RUNNING HANDLER
# lineinfile使用
在Ansible中,lineinfile
模块用于在文件中确保特定的行存在或者从文件中删除特定的行。这个模块非常适合用于配置管理任务,比如添加、更改或删除配置文件中的特定配置行。相当于sed
命令
lineinfile
常用的参数:
path: 文件的路径
line: 添加或修改的内容
state: 可选值present,添加指定行;absent 删除指定行
regexp: 匹配文件中的行
insertbefore: 插入到选中的行之前。
insertafter: 插入到选中的行之后。
backup: 把需要修改的文件进行备份,备份文件名包含有时间戳
下面是lineinfile
模块的一些基本用法:
例1: 向/etc/hosts文件中添加一行解析。内容为: "192.168.1.10 nfs.server.com"
state值为 present
playbook内容如下:
---
- hosts: 192.168.8.214
remote_user: root
become: no
gather_facts: no
tasks:
- name: append line
lineinfile:
path: /etc/hosts
line: '192.168.1.10 nfs.server.com'
state: present ##值为present表示添加
2
3
4
5
6
7
8
9
10
11
如果/etc/hosts
文件中不存在192.168.1.10 nfs.server.com
这一行,lineinfile
模块将会添加这一行。
如果已经存在则什么都不做。
例2: 删除/etc/hosts文件中 "192.168.1.10 nfs.server.com"这一行。
state值为 absent
playbook内容如下:
---
- hosts: 192.168.8.214
remote_user: root
become: no
gather_facts: no
tasks:
- name: delete line
lineinfile:
path: /etc/hosts
line: '192.168.1.10 nfs.server.com'
state: absent ##值为absent表示删除
2
3
4
5
6
7
8
9
10
11
这个例子将会从/etc/hosts
文件中删除包含192.168.1.100 myserver.example.com myserver
的行。
例3: 匹配内容并修改行内容
/etc/hosts文件中 把这一行"192.168.1.10 nfs.server.com"的IP更换成"192.168.1.235"
使用regexp匹配192.168.1.10开头的行
playbook内容如下:
---
- hosts: 192.168.8.214
remote_user: root
become: no
gather_facts: no
tasks:
- name: modify line
lineinfile:
path: /etc/hosts
regexp: '^192.168.1.10' #匹配192.168.1.10开头的行
line: '192.168.1.235 nfs.server.com'
2
3
4
5
6
7
8
9
10
11
在这个例子中,lineinfile
模块将会在/etc/hosts
文件中查找所有以192.168.1.10
开头的行,并用192.168.1.235 nfs.server.com
替换它们。
如果没有匹配到该行,或者匹配到的行和修改的内容相同,则什么都不做。
除了使用regexp进行正则匹配外,还可以用search_string进行精确匹配字符串。
search_string不支持正则。
可以把上面的playbook中的regexp 改成 search_string
playbook内容如下:
---
- hosts: 192.168.8.214
remote_user: root
become: no
gather_facts: no
tasks:
- name: modify line
lineinfile:
path: /etc/hosts
search_string: '192.168.1.10' #精确匹配192.168.1.10开头的行
line: '192.168.1.235 nfs.server.com'
2
3
4
5
6
7
8
9
10
11
注意:如果匹配到多行,无论使用regexp还是search_string,都只会修改匹配到的最后最后一行。
如果想在某一行前面添加一行要怎么办呢?
这时可以使用 insertbefore
例4: /etc/hosts
文件中 在192.168.1.235 nfs.server.com
行前面添加"#insertbefore"
playbook内容如下:
---
- hosts: 192.168.8.214
remote_user: root
become: no
gather_facts: no
tasks:
- name: append line
lineinfile:
path: /etc/hosts
insertbefore: "192.168.1.235 nfs.server.com"
line: "#insertbefore"
2
3
4
5
6
7
8
9
10
11
这个例子将会在/etc/hosts
文件中找到192.168.1.235 nfs.server.com
这行,并在这一行之前插入#insertbefore
这一行。
如果要在某一行后面添加一行就使用 insertafter
例5: /etc/hosts
文件中 在192.168.1.235 nfs.server.com
行后面添加"#insertafter"
playbook内容如下:
---
- hosts: 192.168.8.214
remote_user: root
become: no
gather_facts: no
tasks:
- name: append line
lineinfile:
path: /etc/hosts
insertafter: "192.168.1.235"
line: "#insertafter"
2
3
4
5
6
7
8
9
10
11
做备份是一个很好的习惯。
使用 lineinfile
修改文件也可以做备份的,只需要使用backup=yes
就行了。
例6: 修改文件前进行备份
playbook内容如下:
---
- hosts: 192.168.8.214
remote_user: root
become: no
gather_facts: no
tasks:
- name: lineinfile backup
lineinfile:
path: /etc/hosts
insertafter: "192.168.1.235"
line: "#backup this file"
backup: yes ##添加此行进行备份
2
3
4
5
6
7
8
9
10
11
12
使用backup=yes
参数后会创建一个带有时间戳的备份文件。
上面的playbook执行后,会在修改/etc/hosts
文件之前创建一个备份文件名为hosts.10811.2024-06-21@14\:30\:58~
# blockinfile使用
既然可以插入、修改、删除一行内容,当然也可以操作多行。
ansible提供了blockinfile
模块就可以对多行文本进行操作,它的操作和 lineinfile
差不多。
blockinfile
常用参数
path: 文件的路径
block: 添加或修改的内容
state: 可选值present,添加指定行;absent 删除指定行
marker: 添加注释
insertbefore: 插入到选中的行之前。
insertafter: 插入到选中的行之后。
backup: 把需要修改的文件进行备份,备份文件名包含有时间戳
下面是blockinfile
模块的一些基本用法:
例1: 在empty.txt
文件中添加一个说明
blockinfile.yaml
文件如下
---
- hosts: all
remote_user: root
become: no
gather_facts: no
tasks:
- name: append block
blockinfile:
path: /tmp/empty.txt
block: |
This is an empty file
2nd line
3rd line
4th line
2
3
4
5
6
7
8
9
10
11
12
13
14
执行后查看一下empty.txt文件
[root@node1 ~]# cat /tmp/empty.txt
# BEGIN ANSIBLE MANAGED BLOCK
This is an empty file
2nd line
3rd line
4th line
# END ANSIBLE MANAGED BLOCK
2
3
4
5
6
7
原本是空文件的empty.txt
,现在里面有6行内容。
但是在写playbook中我们只要添加4行内容,这里为什么会有6行呢?
从empty.txt文件内容可以看到,第一行和最后一行不是我们添加的,中间那4行才是我们添加的。
这里需要说明一下,blockinfile
有个marker
参数,这参数是默认执行的。
第一行和最后一行就是marker
参数添加的内容。
那能不能不要添加这个marker
内容呢?答案是可以的,我们把marker
参数加上,然后给个空值就行
例2: 去掉默认的marker
---
- hosts: all
remote_user: root
become: no
gather_facts: no
tasks:
- name: append block
blockinfile:
path: /tmp/empty.txt
marker: "" #此时marker值为空,添加内容时不再有marker内容。
block: |
This is an empty file
2nd line
3rd line
4th line
2
3
4
5
6
7
8
9
10
11
12
13
14
15
有的同学可能已经想到了,既然能添加或删除marker内容。那能不能自定义marker内容呢?
答案是肯定的。这时候就要用到另外2个参数marker_begin
和marker_end
。
其实marker参数就是这2个的合集,只不过开始和结束行是相同的。
下面将用一个例子展示如何自定义开始和结束标志
例3: 自定义marker 开始和结束内容
---
- hosts: all
remote_user: root
become: no
gather_facts: no
tasks:
- name: append block
blockinfile:
path: /tmp/empty.txt
marker_begin: "This is begin line"
marker_end: "This is end line"
block: |
This is an empty file
2nd line
3rd line
4th line
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
执行后查看一下empty.txt文件
[root@node1 ~]# cat /tmp/empty.txt
# This is begin line ANSIBLE MANAGED BLOCK
This is an empty file
2nd line
3rd line
4th line
# This is end line ANSIBLE MANAGED BLOCK
2
3
4
5
6
7
insertbefore、insertafter和backup用法与lineinfile
中相同,这里不再给出示例。
blockinfile
支持循环的。至于如何在playbook中使用循环,后面的章节会有讲解。
这里只给出一个示例
例4: 给/etc/hosts文件中添加多个映射
---
- hosts: all
remote_user: root
become: no
gather_facts: no
tasks:
- name: append block
blockinfile:
path: /etc/hosts
block: |
{{ item.ip }} {{ item.nodename }}
marker: "#==============="
backup: yes
loop:
- { ip: 192.168.1.12, nodename: node1 }
- { ip: 192.168.1.13, nodename: node2 }
- { ip: 192.168.1.14, nodename: node3 }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
注意:marker
参数不能省略,否则只能添加最后一行循环的内容
查看一下/etc/hosts文件内容
[root@node1 etc]# cat hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
#===============
192.168.1.12 node1
#===============
192.168.1.13 node2
#===============
192.168.1.14 node3
#===============
2
3
4
5
6
7
8
9
10
11
# tags的使用
看名字就知道是打标签了,给什么打标签呢?当然是给任务打标签了。
为什么要给任务打标签呢?
想象一下,我写了一个playbook里面包含了很多个任务(我自己就写过一个任务里面包含20多个任务),但是我只想执行其中的一个任务呢?
这时就用到了tags,我们先写个tags.yaml的demo看看。
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- name: task1
file:
path: /root/test_one
state: touch
tags: t1 # 注意tags标签的层级,是和file模块同一层级
- name: task2
file:
path: /root/test_two
state: touch
tags: t2
- name: task3
file:
path: /root/test_three
state: touch
tags: t3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
上面yaml有3个任务:task1 task2和task3 对应的tags是t1 t2 t3。
如果正常执行的话,3个任务会被依次执行,但是我现在只想执行第3个任务task3。
执行如下:
[root@localhost ~]# ansible-playbook --tag=t3 tags.yaml
PLAY [192.168.233.167] *********************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [192.168.233.167]
TASK [task3] *********************************************************************************************************
changed: [192.168.233.167]
PLAY RECAP *********************************************************************************************************
192.168.233.167 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
2
3
4
5
6
7
8
9
10
11
12
13
在执行ansible-playbook的时候添加参数--tag=t3
就只会执行tags=t3的任务。执行结果TASK [task3]也证明了这一点。
既然能选择执行哪个tags的任务,当然也能跳过某个tags的任务,命令如下:
[root@localhost ~]# ansible-playbook --skip-tag=t3 tags.yaml
列出所有的tags
[root@localhost ~]# ansible-playbook --list-tags tags.yaml
playbook: tags.yaml
play #1 (192.168.233.167): 192.168.233.167 TAGS: []
TASK TAGS: [t1, t2, t3]
2
3
4
5
6
7
共3个tags 分别是t1、t2、t3
# playbook中的变量
在写playbook中使用变量可以使我们的playbook更加灵活
自定义变量
变量名应该由字母、数字、下划线组成,变量名需要以字母开头,ansible内置的关键字不能作为变量名
定义变量的格式:变量名:变量值
可以用关键字 vars来定义变量
---
- hosts: 192.168.233.167
remote_user: root
vars:
testfile: mytestfile
tasks:
- name: create file
file:
path: /root/{{ testfile }} #引用变量名 在双花括号中,且变量名和花括号直接加一个空格
state: touch
2
3
4
5
6
7
8
9
10
多个变量写法
vars:
testfile1: mytestfile
testfile2: youtestfile
2
3
或者
vars:
- testfile1: mytestfile
- testfile2: youtestfile
2
3
两种写法都可以,根据你自己喜好。
除了在playbook中定义变量外,还可以在单独一个文件中定义变量
比如:写一个名为my_vars.yaml的变量文件如下:
nginx_port: "listen 880" #变量中间有空格的用双引号,没有空格的可以不用引号# nginx_conf_path: /etc/nginx/nginx.conf
每个变量占一行
引用方法如下:
---
- hosts: 192.168.233.167
remote_user: root
vars:
testfile: mytestfile
var_files: #用var_files关键字引入变量文件
- /root/my_vars.yaml # 引入变量文件的路径
tasks:
- name: create file
file:
path: /root/{{ testfile }} #引用变量名 在双花括号中,且变量名和花括号直接加一个空格
state: touch
- name: modify nginx listen port
replace:
path: /etc/nginx/sites-enabled/default #ubuntu用apt安装的nginx端口配置文件位置
regexp: 'listen 80' #匹配需要修改的内容
replace: {{ nginx_port }} #引用变量文件中的nginx_port值
#backup: yes 这里不做备份,因为ubuntu用apt安装nginx配置有点特殊,会导致nginx无法重启
notify: restart nginx #注意notify的层级和replace是同一层的
- name: fetch file
fetch:
src: "{{ nginx_conf_path }}" #调用了my_vars变量文件中的nginx_conf_path变量
dest: /root/
handlers: #handlers的层级是和"- name"同一层级的
- name: restart nginx
service:
name: nginx
state: restarted
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
vars和vars_files可以同时使用,但是建议只使用其中一种方式
如果两种方式都是用,且引用变量名相同时,会优先使用vars_files引用的文件中定义的变量
# ansible本身的变量
上面所讲的都是我们自己定义的变量,在ansible中除了自定义变量外,它自身也有自己的变量
通过setup模块我们查看一下
[root@localhost ~]# ansible 192.168.233.167 -m setup
192.168.233.167 | SUCCESS => {
"ansible_facts": {
"ansible_all_ipv4_addresses": [
"192.168.233.167"
],
"ansible_all_ipv6_addresses": [
"fe80::20c:29ff:fe31:441"
],
"ansible_apparmor": {
"status": "enabled"
},
"ansible_architecture": "x86_64",
"ansible_bios_date": "07/29/2019",
"ansible_bios_version": "6.00",
"ansible_cmdline": {
"BOOT_IMAGE": "/vmlinuz-4.4.0-87-generic",
"ro": true,
"root": "/dev/mapper/ubuntu--vg-root"
},
"ansible_date_time": {
"date": "2021-02-03",
"day": "03",
"epoch": "1612322512",
"hour": "11",
"iso8601": "2021-02-03T03:21:52Z",
"iso8601_basic": "20210203T112152183883",
"iso8601_basic_short": "20210203T112152",
"iso8601_micro": "2021-02-03T03:21:52.183883Z",
"minute": "21",
"month": "02",
"second": "52",
"time": "11:21:52",
"tz": "CST",
"tz_offset": "+0800",
"weekday": "Wednesday",
"weekday_number": "3",
"weeknumber": "05",
"year": "2021"
},
"ansible_default_ipv4": {
"address": "192.168.233.167",
"alias": "ens33",
"broadcast": "192.168.233.255",
"gateway": "192.168.233.2",
"interface": "ens33",
"macaddress": "00:0c:29:31:04:41",
"mtu": 1500,
"netmask": "255.255.255.0",
"network": "192.168.233.0",
"type": "ether"
},
"ansible_default_ipv6": {},
"ansible_device_links": {
"ids": {
"dm-0": [
"dm-name-ubuntu--vg-root",
"dm-uuid-LVM-2wodXdFENDdoEyQu26BNJoBxQ7Vi3OpA8J1ctCb9mfzmBcg6fkMoq4qNzWbXc24U"
],
"dm-1": [
"dm-name-ubuntu--vg-swap_1",
"dm-uuid-LVM-2wodXdFENDdoEyQu26BNJoBxQ7Vi3OpA0SBFA2ppfr2CudF072rlk5JqgxEt6VZA"
],
"sda5": [
"lvm-pv-uuid-e0VDvn-DMEP-FQ7u-cuFZ-8RQu-y76e-UdUbFX"
],
"sr0": [
"ata-VMware_Virtual_SATA_CDRW_Drive_01000000000000000001"
]
},
"labels": {
"sr0": [
"Ubuntu-Server\\x2016.04.3\\x20LTS\\x20amd64"
]
},
"masters": {
"sda5": [
"dm-0",
"dm-1"
]
},
"uuids": {
"dm-0": [
"4f69b277-6b7e-4bd8-80f0-4f85a04eadda"
],
"dm-1": [
"e1b06976-3417-4a0c-8d43-9cf1481e43ae"
],
"sda1": [
"64a1b32e-3914-493a-9353-03afe9700551"
],
"sr0": [
"2017-08-01-11-30-13-00"
]
}
},
"ansible_devices": {
"dm-0": {
"holders": [],
"host": "",
"links": {
"ids": [
"dm-name-ubuntu--vg-root",
"dm-uuid-LVM-2wodXdFENDdoEyQu26BNJoBxQ7Vi3OpA8J1ctCb9mfzmBcg6fkMoq4qNzWbXc24U"
],
"labels": [],
"masters": [],
"uuids": [
"4f69b277-6b7e-4bd8-80f0-4f85a04eadda"
]
},
"model": null,
"partitions": {},
"removable": "0",
"rotational": "1",
"sas_address": null,
"sas_device_handle": null,
"scheduler_mode": "",
"sectors": "36741120",
"sectorsize": "512",
"size": "17.52 GB",
"support_discard": "0",
"vendor": null,
"virtual": 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
信息较多,我们只截取部分,有兴趣的同学可以自己研究一下。
在playbook执行过程中我们首先看到[Gathering Facts]任务,这个任务会自动执行setup模块收集远程机器的信息。
在我们写playbook的时候默认都会执行[Gathering Facts]任务,如果不想或不需要收集远程机器的信息我们可以把这个任务关闭,写法如下:
---
- hosts: 192.168.233.167
remote_user: root
gather_facts: no
tasks:
- name: task1
file:
path: /root/test_one
state: touch
tags: t1 # 注意tags标签的层级,是和file模块同一层级
- name: task2
file:
path: /root/test_two
state: touch
tags: t2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
加上gather_facts: no 在执行playbook中就不会在执行[Gathering Facts]任务,这样也就提高了playbook的执行速度。
这里列出几个常用的变量名称
ansible_os_family: 获取远程机器属于哪个家族的,比如RedHat、Debian
ansible_distribution_version: 远程机器发行版本
ansible_default_ipv4: 获取远程机器ip
我们写一个playbook看看上面三个变量对应的值
---
- hosts: 192.168.233.167
remote_user: root
vars:
ip: "{{ ansible_default_ipv4['address'] }}"
family: "{{ ansible_os_family }}"
vers: "{{ ansible_distribution_version }}"
tasks:
- name: show ansible vars
debug:
msg: "{{ item }}"
loop:
- "{{ ip }}"
- "{{ family }}"
- "{{ vers }}"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
再看一下执行结果
[root@localhost ~]# ansible-playbook ansible_vars.yml
PLAY [192.168.233.167] *********************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [192.168.233.167]
TASK [show ansible vars] *********************************************************************************************************
ok: [192.168.233.167] => (item=192.168.233.167) => {
"msg": "192.168.233.167"
}
ok: [192.168.233.167] => (item=Debian) => {
"msg": "Debian"
}
ok: [192.168.233.167] => (item=16.04) => {
"msg": "16.04"
}
PLAY RECAP *********************************************************************************************************
192.168.233.167 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
用debug模块分别显示了上面三个变量的值,远程机器不同,可能显示的结果也不同。
# 注册变量
其实ansible每次执行后都会有返回值,执行Ad-hoc(就是临时执行的命令)的时候可以看到,但是执行playbook的时候是看不到的。
在我们写playbook的时候如果某一步操作想引用前一步执行后的返回值怎么办呢?
这时候我们可以借助ansible的关键字register ,把返回的内容注册的一个自定义的变量中,
先写一个register.yaml,内容如下:
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- name: free -m
shell: free -m
register: free #返回内容注册到变量free
- name: show register vars
debug:
var: free #显示变量free的内容
2
3
4
5
6
7
8
9
10
这个yaml文件的意思是第一个任务使用shell模块执行free -m命令,再把返回的结果注册到变量free。
第二个任务用debug模块通过关键字var 把刚才注册到free变量的内容显示出来
看一下执行结果:
[root@localhost ~]# ansible-playbook register.yml
PLAY [192.168.233.167] *********************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [192.168.233.167]
TASK [free -m] *********************************************************************************************************
changed: [192.168.233.167]
TASK [show register vars] *********************************************************************************************************
ok: [192.168.233.167] => {
"free": {
"changed": true,
"cmd": "free -m",
"delta": "0:00:00.006214",
"end": "2021-02-03 14:32:45.998207",
"failed": false,
"rc": 0,
"start": "2021-02-03 14:32:45.991993",
"stderr": "",
"stderr_lines": [],
"stdout": " total used free shared buff/cache available\nMem: 1982 326 232 9 1423 1414\nSwap: 2047 34 2013",
"stdout_lines": [
" total used free shared buff/cache available",
"Mem: 1982 326 232 9 1423 1414",
"Swap: 2047 34 2013"
]
}
}
PLAY RECAP *********************************************************************************************************
192.168.233.167 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
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
可以看到变量free的内容,"stdout_lines"的内容就是执行free -m的结果。
# 交互式变量
之前我们写的playbook都是直接执行完成,中间没有任何交互的,但是在执行playbook的时候想要临时输入一下变量需要怎么做呢?只要通过关键字prompt即可实现。我把这种变量叫做交互式变量 (官方没有这个叫法),要先通过vars_prompt定义好哪些变量需要交互实现
先看一个例子
---
- hosts: 192.168.233.167
remote_user: root
vars_prompt:
- name: uname
prompt: "Input uname"
private: no #关闭private
- name: passwd
prompt: "Password"
tasks:
- name: out prompt
debug:
msg: you uname is {{ uname }} password is {{ passwd }}
2
3
4
5
6
7
8
9
10
11
12
13
参数 private默认是打开的,该参数打开后输入内容是不可见的,关闭后输入的内容可以在屏幕显示
执行结果如下:
[root@localhost ~]# ansible-playbook 123.yml
Input uname: xiaoming #关闭private,显示输入内容
Password: #默认打开private,不显示输入内容
PLAY [192.168.233.167] *********************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [192.168.233.167]
TASK [out prompt] *********************************************************************************************************
ok: [192.168.233.167] => {
"msg": "you uname is xiaoming password is 123456"
}
PLAY RECAP *********************************************************************************************************
192.168.233.167 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
交互式变量就讲这么多。
当然还有其他设置变量的方式,个人觉得掌握以上几种足够工作中使用了。感兴趣的同学,可以网上去找其他设置变量的方式。这里就不全列出了
# playbook的循环
# 使用关键字loop进行循环
学过编程的同学知道在编程过程中经常会用到for 循环、while循环来处理数据。
在ansible-playbook中也有循环,虽然没有那么复杂,但是用起来很方便。
细心的同学马上想起在讲变量的用到过循环。不记得也没关系,我们再来看一下
---
- hosts: 192.168.233.167
remote_user: root
vars:
ip: "{{ ansible_default_ipv4['address'] }}"
family: "{{ ansible_os_family }}"
vers: "{{ ansible_distribution_version }}"
tasks:
- name: show ansible vars
debug:
msg: "{{ item }}"
loop:
- "{{ ip }}"
- "{{ family }}"
- "{{ vers }}"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
通过关键字loop 对 ip、family、vers三个变量进行循环,除了loop还可以使用with_items达到同样的循环效果。只要把上的loop改成with_items。
返回的结果也是分别显示出来
[root@localhost ~]# ansible-playbook loop.yaml
PLAY [192.168.233.167] *********************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [192.168.233.167]
TASK [show ansible vars] *********************************************************************************************************
ok: [192.168.233.167] => (item=192.168.233.167) => {
"msg": "192.168.233.167"
}
ok: [192.168.233.167] => (item=Debian) => {
"msg": "Debian"
}
ok: [192.168.233.167] => (item=16.04) => {
"msg": "16.04"
}
PLAY RECAP *********************************************************************************************************
192.168.233.167 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 使用关键字with_items进行循环
再来一个应用场景,需要删除多个文件
如果不使用循环,那么删除3个文件就要写3个任务,使用循环的话一个任务就可以了
示例如下:
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- name: remove install file
file:
path: "{{ item.filepath }}"
state: absent
with_items:
- { filepath: "/root/test_one" }
- { filepath: "/root/test_two" }
2
3
4
5
6
7
8
9
10
11
再来一个更复杂一点的场景,需要拷贝多个文件,但是文件在本地路径不一样,拷到远程机器的目录也不一样。
比如:本地文件/root/A 拷贝到远程机器/home目录下,本地文件/var/B拷贝到远程机器的/tmp目录下,yaml文件示例如下:
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- name: copy file
copy:
src: "{{ item.src }}"
dest: "{{ item.dest}}"
with_items:
- { src: "/root/A", dest: "/home/A" }
- { src: "/var/B", dest: "/tmp/B" }
2
3
4
5
6
7
8
9
10
11
# 使用关键字with_together进行循环
上面的playbook还可以用另一个关键字with_together来实现,写法有所区别,
示例如下:
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- name: copy file
copy:
src: "{{ item.0 }}"
dest: "{{ item.1}}"
with_together:
- ["/root/A", "/var/B" ]
- ["/home/A", "/tmp/B" ]
2
3
4
5
6
7
8
9
10
11
以上两种方法都可以,根据自己喜好即可。
还有一种虽然使用了循环,但是没有用到loop或with_items关键字,我们先来看一下
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- name: remove install file
apt:
name: ['vim', 'lrzsz']
state: present
2
3
4
5
6
7
8
虽然没有循环的关键字,但是name的值是一个列表,ansible在执行的时候会自动循环
# playbook的条件判断与when
在编程的过程经常会看到用if做条件判断,但是在ansible-playbook中用关键字when来做判断,而它的用法也很简单,先看一个小例子
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- name: yum install nginx
yum:
name: nginx
state: present
when: ansible_distribution == "CentOS"
- name: apt install nginx
apt:
name: nginx
state: present
when: ansible_distribution == "Ubuntu"
2
3
4
5
6
7
8
9
10
11
12
13
14
上面yaml的意思很简单,当远程机器是CentOS的时候使用yum安装nginx,当远程机器是Ubuntu使用apt安装nginx
执行结果如下:
[root@localhost ~]# ansible-playbook when.yaml
PLAY [192.168.233.167] ***********************************************************************************************************
TASK [Gathering Facts] ************************************************************************************************************
ok: [192.168.233.167]
TASK [yum install nginx] ************************************************************************************************************
skipping: [192.168.233.167]
TASK [apt install nginx] ************************************************************************************************************
ok: [192.168.233.167]
PLAY RECAP ************************************************************************************************************
192.168.233.167 : ok=2 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
2
3
4
5
6
7
8
9
10
11
12
13
14
可以看到TASK [yum install nginx]状态的skipping,因为远程机器的系统是Ubuntu的。
这个是最简单的用法用来区分远程机器的不同发行版本。
如果远程机器是CentOS6和CentOS7或者Ubuntu14,16,18的版本我们就需要组合条件来判断了
示例如下:
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- name: apt install nginx
apt:
name: nginx
state: present
when: ansible_distribution == "Ubuntu" && ansible_distribution_major_version == "16"
2
3
4
5
6
7
8
9
或者换成列表的方式书写条件
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- name: apt install nginx
apt:
name: nginx
state: present
when:
- ansible_distribution == "Ubuntu"
- ansible_distribution_major_version == "16"
2
3
4
5
6
7
8
9
10
11
以上两种方式都可以
条件判断常用的就这些,当然还有其他的。感兴趣的同学可以去网上找找
# playbook的条件判断和block
试想,如果每个任务都写一个when去判断,这种做法是不是很麻烦呢。一个两个任务还行,十个八个呢?
先不管写那么多的when判断语句累不累,一下子写那么多判断语句,自己估计看着的都傻了吧。
还好ansible的开发者已经帮忙我们想好解决方案了,就是用block关键字把符合条件的任务当作**"块"**,
只要条件符合,**"块"**内的所有任务都会执行,而不需要每个任务都去写个判断。
且看示例:
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- block:
- name: yum install nginx
yum:
name: nginx
state: present
- name: yum install vim
yum:
name: vim
state: present
when: ansible_distribution == "CentOS" #注意when的层级,是和block同一层级
- block:
- name: apt install nginx
apt:
name: nginx
state: present
- name: apt install vim
apt:
name: vim
state: present
when: ansible_distribution == "Ubuntu" #注意when的层级,是和block同一层级
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
这个playbook相信你可以很轻松的看明白是什么意思。
唯一需要注意的就是when和block的层级,他们是相同层级的
# 忽略错误ignore_errors
我们在执行playbook的是按照任务顺序一步步执行的,如果某一步执行错误,playbook就会自动停止,后面的任务不会再继续执行。
有时候我们希望某一步任务执行错误,但是后面的任务还要继续执行。
这个时候要用到关键字ignore_errors,一看就知道它的作用是忽略错误。
默认是关闭的,需要忽略的时候要打开,示例如下:
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- name: copy
shell: cp 123 #这个命令执行会报错
ignore_errors: true
- name: show df -h
shell: df -h
2
3
4
5
6
7
8
9
看一下执行结果
[root@localhost ~]# ansible-playbook ig.yaml
PLAY [192.168.233.167] ************************************************************************************************************
TASK [Gathering Facts] ************************************************************************************************************
ok: [192.168.233.167]
TASK [copy] ************************************************************************************************************
fatal: [192.168.233.167]: FAILED! => {"changed": true, "cmd": "cp 123", "delta": "0:00:00.003605", "end": "2021-02-04 11:35:22.464331", "msg": "non-zero return code", "rc": 1, "start": "2021-02-04 11:35:22.460726", "stderr": "cp: missing destination file operand after '123'\nTry 'cp --help' for more information.", "stderr_lines": ["cp: missing destination file operand after '123'", "Try 'cp --help' for more information."], "stdout": "", "stdout_lines": []}
...ignoring
TASK [show df -h]
changed: [192.168.233.167]
PLAY RECAP ************************************************************************************************************
192.168.233.167 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
可以看到ignored=1 后面的df -h的任务正常执行了
ignore_errors使用很简单,不做多说。
# include的使用
编程的时候经常会调用其他代码模块的函数,当前比较火的Python 就是用关键字 import调用(或者叫做导入)模块。
在写shell脚本的时候我们也会用[. /path/test.sh] 的语法调用其他的shell脚本
在ansible-playbook中也有相应的调用,它是用关键字include来实现的
我们先写2个playbook,一个安装LAMP一个安装LNMP.
LAMP.yaml
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- name: apt install LAMP
apt:
name: "{{ item }}"
state: present
loop:
- apache2
- mysql-server
- php-fpm
2
3
4
5
6
7
8
9
10
11
12
LNMP.yaml
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- name: apt install LNMP
apt:
name: "{{ item }}"
state: present
loop:
- nginx
- mysql-server
- php-fpm
2
3
4
5
6
7
8
9
10
11
12
可以看到无论在LAMP或LNMP中都会安装mysql-server和php-fpm,因此我们把这2个任务放到一个单独的yaml文件中,再写一个install_mysql_php.yaml文件,内容如下:
- apt:
name: "{{ item }}"
state: present
loop:
- mysql-server
- php-fpm
2
3
4
5
6
或者你不用循环,直接写单独的2个任务,如下:
- apt:
name: mysql-server
state: present
- apt:
name: php-fpm
state: present
2
3
4
5
6
以上2种写法都可以,但是要注意一点,这个yaml文件只包含任务,可以理解为任务列表。
而我们之前写的LAMP.yaml和LNMP.yaml文件也要做相应的修改,修改后如下:
LAMP.yaml
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- name: apt install LAMP
apt:
name: apache2
state: present
- include: install_mysql_php.yaml
2
3
4
5
6
7
8
9
LNMP.yaml
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- name: apt install LNMP
apt:
name: nginx
state: present
- include: install_mysql_php.yaml
2
3
4
5
6
7
8
9
include和when结合是经常使用的,因为经常远程机器的系统都有不同发行版,有的是CentOS,有的是Ubuntu,有的是SUSE,而针对不同的发行版我们就要写不同的playbook,写法如下:
---
- hosts: 192.168.233.167
remote_user: root
tasks:
- include: centos.yml
when: ansible_os_family == "RedHat"
- include: ubuntu.yml
when: ansible_os_family == "Debian"
2
3
4
5
6
7
8
其实还有include_tasks、import_tasks也可以达到相同的调用效果,个人感觉一个include就够用了
好奇的同学可以到网上找相关资料研究一下。
# template模块
为什么使用template ?
在实际的工作中由于每台服务器的环境配置都可能不同,
但是往往很多服务的配置文件都需要根据服务器环境进行不同的配置,比如Nginx最大进程数、Redis最大内存等。
为了解决这个问题可以使用Ansible的template模块,该模块和copy模块作用基本一样,都是把管理端的文件复制到客户端主机上,但是区别在于template模块可以通过变量来获取配置值,支持多种判断、循环、逻辑运算等,而copy只能原封不动的把文件内容复制过去
直接来个示例吧,本地创建模板文件template.j2(因为ansible用的jinja2引擎,所以用j2后缀)
template.j2
发行版: {{ distribution }}
系统版本: {{ distribution_version }}
主机名: {{ hostname }}
内核版本: {{ kernel }}
IP地址: {{ ip }}
2
3
4
5
写个playbook调用该模板
---
- hosts: 192.168.233.167
remote_user: root
vars:
- distribution: "{{ ansible_distribution }}"
- distribution_version: "{{ ansible_distribution_version }}"
- hostname: "{{ ansible_hostname }}"
- kernel: "{{ ansible_kernel }}"
- ip: "{{ ansible_default_ipv4['address'] }}"
tasks:
- name: use template module
template:
src: /root/template.j2
dest: /tmp/hostinfo
2
3
4
5
6
7
8
9
10
11
12
13
14
说明:playbook中""中是ansible gather_facter获取到的变量值。
template.j2文件中""中的变量是playbook中定义的,不要搞混淆了
jinja2 模板中的变量是调用playbook中定义的变量。
查看一下远程机器的hostinfo文件
[root@localhost ~]# ansible 192.168.233.167 -m shell -a "cat /tmp/hostinfo"
192.168.233.167 | CHANGED | rc=0 >>
发行版: Ubuntu
系统版本: 16.04
主机名: ubuntu
内核版本: 4.4.0-87-generic
IP地址: 192.168.233.167
2
3
4
5
6
7
贴士: Jinja是一种现代且设计友好的Python模板语言而ansible 是使用python开发的,所以在ansible中无论是正常使用变量,还是template模块使用变量都是使用jinja模板。
学过flask的应该很熟悉,jinja的使用也非常灵活,可以做逻辑运算,做for循环,做if判断。在这里就不做多讲解有兴趣的同学可以网上查找资料
再写个使用template模块使用的例子。也是简单的例子。但是会加上简单的运算、循环和逻辑判断
现在,我们需要部署一台nginx服务器,部署需求是:
1、nginx的worker_processes个数是cpu核数 x 2
2、部署2个vhosts,一个监听80端口,另一个监听8080端口
3、一个server_name是web1.test.com 另一个server_name是web2.test.com
4、2个vhosts的root路径分别是/var/www/web1和/var/www/web2
这个可能有点复杂,但是也不难。先写playbook
---
- hosts: 192.168.233.167
remote_user: root
vars:
cpu_cores: "{{ ansible_processor_cores }}"
nginx_vhosts:
- web1:
listen_port: 80
server_name: "web1.test.com"
root: "/var/www/web1"
- web2:
listen_port: 8080
server_name: "web2.test.com"
root: "/var/www.web2"
tasks:
- name: set nginx config from template
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
backup: yes
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
再写nginx.conf.j2
worker_processes {{ cpu_cores * 2}} #cpu核心数x2
{% for vhost in nginx_vhosts %} #循环playbook中nginx_vhosts中的变量
server {
listen {{ vhost.listen_port }}
{% if vhost.server_name is defined %} #判断playbook中是否定义server_name变量
server_name {{ vhost.server_name }}
{% endif %} #if语句结束
root {{ vhost.root }}
}
{% endfor %} #for语句结束
2
3
4
5
6
7
8
9
10
说明: 上面模板是nginx配置文件的部分内容,测试使用。正确的做法是把原来的nginx.conf文件修改成nginx.conf.j2文件
看一下结果
[root@localhost ~]# ansible 192.168.233.167 -m shell -a "cat /etc/nginx/nginx.conf"
192.168.233.167 | CHANGED | rc=0 >>
worker_processes 2 #用的是虚拟机只给一个核这里x2,所以值为2
server {
listen 80
server_name web1.test.com
root /var/www/web1
}
server {
listen 8080
server_name web2.test.com
root /var/www.web2
}
2
3
4
5
6
7
8
9
10
11
12
13
template就说这么多吧
# role的使用
roles是根据已知文件结构自动加载某些vars_files,任务和处理程序的方法。按角色对内容进行分组还可以轻松与其他用户共享角色。
项目结构示例:
site.yml
webservers.yml
fooservers.yml
roles/
common/
tasks/
handlers/
files/
templates/
vars/
defaults/
meta/
webservers/
tasks/
defaults/
meta/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
roles必须至少包含这些目录,使用时,每个目录必须包含一个main.yml文件
- tasks: 存放角色要执行的任务的文件。
- handlers: 调用handlers的文件放在该文件夹。
- defaults: 色的默认变量。
- vars: 角色的其他变量。
- files: 存放文件。比如要copy的文件,并且在此目录下的文件,再写copy任务时, src 只要写文件名即可,不用写全路径。
- templates: 包含可以通过此角色部署的模板。
- meta: 为此角色定义一些元数据。
roles 的存放路径默认是/etc/ansible/roles
写完roles后可以用**ansible-playbook -C roles_name.yaml
**检查语法是否有误
下面是我自己写的一个roles小demo功能非常简单,可以去看下了解roles具体该如何写。
Demo地址:
Gitee:https://gitee.com/wsl12105/playbook_demo.git
Github:https://github.com/wsl12105/playbook_demo.git
# ansible-vault的使用
前面学习了这么多,可能你在疑惑一个问题,我的playbook中包含了密码。这些是需要保密的,不能随便给别人看。那该怎么办?
作为ansible的开发者已经帮我们解决了这个问题。
就是使用ansible-vault
给我们的现有的playbook加密,当然也可以直接创建一个加密的文件
先来看看ansible-vault有哪些操作
create: 创建新的vault加密文件
decrypt: 解密vault加密文件。(前提是知道vault密码)
edit: 编辑vault加密文件 。(前提是知道vault密码)
view: 预览vault加密文件。(前提是知道vault密码)
encrypt: 给未加密的文件进行vault加密
encrypt_string: 加密一个字符串
rekey: 更换vault密码 。(前提是知道原来的vault密码)
下面将用几个例子进行讲解。
例1: 使用ansible-vault create
创建一个新的名为 vaulttest.yaml的vault 加密文件
[root@localhost ~]# ansible-vault create vaulttest.yaml
New Vault password:
Confirm New Vault password:
2
3
需要输入2次vault密码,然后才能进入编辑界面。正常编辑完成保存即可。
vaulttest.yaml明文内容为:
---
- hosts: 192.168.8.214
become: no
gather_facts: no
tasks:
- name: list root dir
shell: ls /root
2
3
4
5
6
7
直接用cat
查看加密后的文件
[root@localhost ~]# cat vaulttest.yaml
$ANSIBLE_VAULT;1.1;AES256
62376364376634663163636262323362646563646237623032336636393663363966623239396364
3739393064393330636237306263396664646533383866620a393436363030613637613663326161
61383231363133326364323261356161383836386332373262313337343064393633313261303233
3364636237356534650a363937613736323962663833636232323735666232323937356461643630
64336137383835346266363533333062353732623965393638656334646464346239356362313662
63616662336432353337363066313136396335646233613133663138653465303762316561313832
61386363313239623364663132663064303734313534646132316462613864633365613061626238
30636131383863326537346463626462313862323932343964623538643466306133643738366630
31373032383438623165623237363435303831376636623535383134613439643563
2
3
4
5
6
7
8
9
10
11
可以看到文件内容全变成了数字,因此起到了加密效果
例2: 使用ansible-vault decrypt
解密加密后的 vaulttest.yaml文件
[root@localhost ~]# ansible-vault decrypt vaulttest.yaml
Vault password:
Decryption successful
2
3
输入正确的密码后即可解密成功。使用cat
命令可以正常查看文件内容
例3: 使用ansible-vault edit
编辑vaulttest.yaml文件
[root@localhost ~]# ansible-vault edit vaulttest.yaml
Vault password:
2
正确输入密码后,直接进入编辑界面
例4: 使用ansible-vault view
编辑vaulttest.yaml文件
[root@localhost ~]# ansible-vault view vaulttest.yaml
Vault password:
---
- hosts: 192.168.8.214
become: no
gather_facts: no
tasks:
- name: list root dir
shell: ls /root
2
3
4
5
6
7
8
9
正确输入密码后,直接以明文方式显示文件内容
例5: 使用ansible-vault encrypt
加密vaulttest.yaml文件
[root@localhost ~]# ansible-vault encrypt vaulttest.yaml
New Vault password:
Confirm New Vault password:
2
3
需要输入2次密码。
如果该文件已经进行加密,再次加密会报错: ERROR! input is already encrypted
其实,加密文件更多的时候是想加密某个字符串,比如:密码。
加密字符串会就要用到 encrypt_string 这个时候就要用到encrypt_string
例6: 使用ansible-vault encrypt_string
加密字符串 123456
[root@localhost ~]# ansible-vault encrypt_string 123456
New Vault password:
Confirm New Vault password:
Encryption successful
!vault |
$ANSIBLE_VAULT;1.1;AES256
65613939623930646565316335626130316138396637323639646664633339376265646536343632
6336653531333539383363663637303166396661316164380a376364306430623830646436646631
31323730396430386535366535646365303562363539653732366266346564383134306331393238
3264643535323639350a323366663733346363626231346335346135333733626238663133326532
6230
2
3
4
5
6
7
8
9
10
11
加密字符串也同样要输入2次密码。我们要用到!vault |
以及下面的几行内容。
但是加密后的字符串要怎么使用呢?
请看下面的完整示例:
encrypt_string.yaml
---
- hosts: 192.168.8.214
remote_user: root
become: no
gather_facts: no
vars:
passwd: !vault |
$ANSIBLE_VAULT;1.1;AES256
65613939623930646565316335626130316138396637323639646664633339376265646536343632
6336653531333539383363663637303166396661316164380a376364306430623830646436646631
31323730396430386535366535646365303562363539653732366266346564383134306331393238
3264643535323639350a323366663733346363626231346335346135333733626238663133326532
6230
tasks:
- name: decrypt passwd
debug:
msg: "{{ passwd }}"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
示例中,定义了变量passwd和 这个变量的值。只不过值是经过加密的。
同时,创建了一个任务,这个任务用来显示passwd的值。
运行一下这个playbook
[root@localhost ~]# ansible-playbook --ask-vault-pass encrypt_string.yaml
Vault password:
PLAY [192.168.8.214] ************************************************************************************************************
TASK [decrypt passwd] ************************************************************************************************************
ok: [192.168.8.214] => {
"msg": "123456"
}
PLAY RECAP ************************************************************************************************************
192.168.8.214 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
2
3
4
5
6
7
8
9
10
11
12
可以看到msg的内容就是加密的字符串。
例7: 使用ansible-vault rekey
更换vault密码
[root@localhost ~]# ansible-vault rekey vaulttest.yaml
Vault password:
New Vault password:
Confirm New Vault password:
Rekey successful
2
3
4
5
第一次输入的原来的密码。
# 运行加密后的playbook
前面学习playbook的时候我们直接使用ansible-playbook
命令运行编写好的未加密的playbook文件。
这里我们也用同样的方式运行加密后的playbook文件vaulttest.yaml
[root@localhost ~]# ansible-playbook vaulttest.yaml
ERROR! Attempting to decrypt but no vault secrets found
2
直接报错 ERROR! Attempting to decrypt but no vault secrets found
那要怎么运行加密后的playbook文件呢?
返回去看一下例6的运行命令。
没错就是要添加一个参数--ask-vault-pass
添加这个参数才能正常运行加密后的playbook。
例8: 运行加密后的playbook文件vaulttest.yaml
[root@localhost ~]# ansible-playbook --ask-vault-pass vaulttest.yaml
Vault password:
PLAY [192.168.8.214] ************************************************************************************************************
TASK [Gathering Facts] ************************************************************************************************************
ok: [192.168.8.214]
TASK [list root dir] ************************************************************************************************************
changed: [192.168.8.214]
PLAY RECAP ************************************************************************************************************
192.168.8.214 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
2
3
4
5
6
7
8
9
10
11
12
需要输入密码才能正常运行playbook。
如果密码输入错误会报出如下错误
[root@localhost ~]# ansible-playbook --ask-vault-pass vaulttest.yaml
Vault password:
ERROR! Decryption failed (no vault secrets were found that could decrypt) on /root/vaulttest.yaml
2
3
报错内容:ERROR! Decryption failed (no vault secrets were found that could decrypt) on /root/vaulttest.yaml
除了使用--ask-vault-pass
参数外,还可以把密码保存到文件中,然后直接使用。
从文件中读取密码要用到--vault-password-file
参数。
用法:
ansible-playbook --vault-password-file <password file> <playbook>
下面给个例子加以说明。
首先创建passwd
文件内容是123。
[root@localhost ~]# cat passwd
123
2
然后运行带有--vault-password-file
参数的命令
[root@localhost ~]# ansible-playbook --vault-password-file passwd vaulttest.yaml
PLAY [192.168.8.214] ************************************************************************************************************
TASK [Gathering Facts] ************************************************************************************************************
ok: [192.168.8.214]
TASK [list root dir] ************************************************************************************************************
changed: [192.168.8.214]
PLAY RECAP ************************************************************************************************************
192.168.8.214 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
2
3
4
5
6
7
8
9
10
11
12
使用--vault-password-file
参数就不用再输入密码了