自动化运维第一步, 走起.
由于这是第一篇学习自动化运维工具的文章, 所以我们先来瞎扯扯..
自动化运维相关 我们的运维工作, 假设从系统的安装开始, 这里就已经有很多说的点了. 比如我们可以从设备厂商协调好, 安装指定版本的系统, 这样拿过来的机器就已经是有系统了. 另外, 还可以使用我们之前说的PXE技术进行网络引导, 只要接通电源开机就可以自动进行安装了. 这是物理机的情况. 如果是虚拟机就更简单了, 我们可以直接生成一个模板, 模板中都携带了需要配置的各个参数, 但是这个时候, 我们可能需要在安装的时候有工具能够动态的去插入一些私有的信息, 例如MAC地址. 接着安装完系统, 我们就进入程序包的安装, 配置和服务启动, 想象一个十几台机器的场景, 难道一个一个手动的配置? 显然不可能, 这里我们就需要有能够统一集中部署的方法, 也就是批量的操作. 再往后, 我们就需要进行服务的发布了, 这个一般就是比较重要的一环了, 我们需要进行平滑的, 滚动的更新和版本回滚. 再往后, 就是监控的工作. 例如:告警, 自动修复等等.
再说说我们的程序发布问题, 首先在程序的研发阶段完成了之后, 显然是需要进行测试的. 这一步叫做预发布验证 , 新版本的代码先发布到测试环境中, 测试环境可能和线上的环境配置相同, 只不过没有接入调度器罢了. 在我们的程序发布中, 有一些原则性的问题需要保证:
不能影响用户体验
系统不能停机
不能导致系统故障或者系统不可用
这样我们就需要一个发布模型, 常见的一种叫做灰度模型, 灰度发布 , 一般我们可以使用符号链接的方式进行版本间切换. 接着 按照我们说过的 只要路径相同, 或者说都是标准的. 我们就可以通过应用程序或者脚本进行自动的切换.
Ansible入门 现在就来说说主角了, Ansible是一个使用Python语言的自动化运维工具, 功能挺强大的. 不过不管怎么说, 先来想个这样的问题, 我们怎么在远端执行命令呢? 我们知道所有的操作都是需要用户的权限来执行的. 这个时候还记得之前在说keepalived和heartbeat的时候使用的:
吗?
是的, 我们的ansible就是使用ssh协议进行的远端调用. 这就说明, 我们只需要一个ansible服务端就行了, 被调用的, 或者说被管理的节点不需要特地安装其他的程序. 这就是agentless
的运维工具. 而另外一种 就是agent
的, 这就是运维工具的一个小分类.
除了ansible, fabric也是基于ssh的. 另外, puppet , func 这些就是典型的需要agent的类型了.
当然, 是否需要agent各有各的好处和缺点.
截止目前(2017-10-25 13:51), Ansible的github仓库star数是26w, 3420个仍旧open的issue和1.1万个已经closed的.
Ansible能够实现统一配置, 统一部署, 批量执行, 多层次多用户并行等功能. 来看一张架构图:
左上角的Host Inventory就是字面意思, 存放管理的主机清单. Ansible通过才能够下面定制的Playbooks从Inventory中抓取主机清单, 接着通过右上角的连接插件进行连接和管理操作. 而且还可以使用插件和其他语言建立自定义的模块.
Ansible依靠Python中的Paramiko, PyYAML, Jinja2等库和模块构建.
直接安装看看, ansible被收录在epel源中, 配置好yum源就可以直接yum install了.
安装完成后, 我们可以看到, 几个配置文件:
1 2 3 4 5 6 [root@VM-node1 ~] /etc/ansible /etc/ansible/ansible.cfg /etc/ansible/hosts /etc/ansible/roles ..(omitted)
主配置文件就是那个cfg了, 额hosts就是我们的inventory. 但是这个清单文件也不是随随便便就可以写的, 里面要指明Web服务器, 存储服务器等等包括其他的一些信息.
由于使用ssh的方式, 所以不妨还是先把基于密钥的认证做一下, 当然这不是必须的, 我们也可以在配置文件中指明主机的账号和密码. 我已经配好了:
1 2 3 4 [root@VM-node1 ~] Thu Oct 26 05:40:41 CST 2017 [root@VM-node1 ~] Wed Oct 25 21:40:44 CST 2017
接下来我们就来看一下ansible基本命令是怎么使用的吧.
ansible基于模块的设计, 所以我们使用ansible, 其实就是使用它的模块, ansible的默认模块是一个叫做command
的模块. 总体的命令格式如下:
1 ansible <host-pattern> [options]
这里的host-pattern就是可以指定的主机, 默认使用的配置文件就是/etc/ansible/hosts
, 我们先来看一下这个文件:
1 2 3 4 5 6 [testservers] 192.168.206.9 192.168.206.10 192.168.206.22 www[001:006].example.com
通过这样的组定义, 我们可以一个服务器组, 也支持这样的, ansible可以自动展开.
定义了主机之后 ,我们就可以使用ansible了, 示例:
1 2 3 4 5 6 7 8 9 [root@VM-node1 ~] 192.168.206.22 | SUCCESS | rc=0 >> 2.6.32-220.el6.x86_64 192.168.206.10 | SUCCESS | rc=0 >> 3.10.0-693.2.2.el7.x86_64 192.168.206.9 | SUCCESS | rc=0 >> 3.10.0-693.2.2.el7.x86_64
来解释一下, 首先我们指明对哪些主机执行操作, 这里就使用的是当时在文件中定义的组名, 接着我们使用-m指明使用的模块command, 后面的参数就是对于每一个模块而言的了. 每一个模块有自己的参数.
那么我该怎么样获取每个模块的用法呢? ansible-doc就是为了这个存在的, 我们可以使用下面来查看所有的模块和每个模块的用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [root@VM-node1 ~] a10_server Manage A10 Networks AX/SoftAX/Thunder/vThunder devices' server object. a10_server_axapi3 Manage A10 Networks AX/SoftAX/Thunder/vThunder devices a10_service_group Manage A10 Networks AX/SoftAX/Thunder/vThunder devices' service groups . a10_virtual_server Manage A10 Networks AX/SoftAX/Thunder/vThunder devices' virtual servers. accelerate Enable accelerated mode on remote node aci_aep Manage attachable Access Entity Profile (AEP) on Cisco ACI fabrics (infra:AttEntityP) aci_ap Manage top level Application Profile (AP) objects on Cisco ACI fabrics (fv:Ap) aci_bd Manage Bridge Domains (BD) on Cisco ACI Fabrics (fv:BD) aci_bd_subnet Manage Subnets on Cisco ACI fabrics (fv:Subnet) aci_bd_to_l3out Bind Bridge Domain to L3 Out on Cisco ACI fabrics (fv:RsBDToOut) aci_config_rollback Provides rollback and rollback preview functionality for Cisco ACI fabrics (config:ImportP) aci_config_snapshot Manage Config Snapshots on Cisco ACI fabrics (config:Snapshot, config:ExportP) ...(omitted)
太多了, 我们看看一共有多少的模块支持:
1 2 3 4 5 6 7 8 9 10 11 12 [root@VM-node1 ~] 1375 [root@VM-node1 ~] > COMMAND (/usr/lib/python2.7/site-packages/ansible/modules/commands/command.py) The `command ' module takes the command name followed by a list of space-delimited arguments. The given command will be executed on all selected nodes. It will not be processed through the shell, so variables like `$HOME' and operations like `"<" ', `">"' , `"|" ', `";"' and `"&" ' will not work (use the [shell] module if you need these features). For Windows targets, use the [win_command] module instead. OPTIONS (= is mandatory): ...(omitted)
1000+的模块, 但是我们使用的也是很有限的几个. 可以直接在后面加上模块的名字, 就可以获取到相关的说明和文档了.
另外, 由于command是默认模块, 所以也可以不用特定指明:
1 2 3 4 5 6 7 8 9 [root@VM-node1 ~] 192.168.206.22 | SUCCESS | rc=0 >> 2.6.32-220.el6.x86_64 192.168.206.10 | SUCCESS | rc=0 >> 3.10.0-693.2.2.el7.x86_64 192.168.206.9 | SUCCESS | rc=0 >> 3.10.0-693.2.2.el7.x86_64
使用all关键字, 就可以对整个文件中的主机发出指令.
接下来就来学习一下常见的几个模块吧.
cron 先来看一个示例:
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 [root@VM-node1 ~] 192.168.206.22 | SUCCESS => { "changed" : true , "envs" : [], "failed" : false , "jobs" : [ "test of Ansible cron module" ] } 192.168.206.10 | SUCCESS => { "changed" : true , "envs" : [], "failed" : false , "jobs" : [ "test of Ansible cron module" ] } 192.168.206.9 | SUCCESS => { "changed" : true , "envs" : [], "failed" : false , "jobs" : [ "test of Ansible cron module" ] }
接着我们来稍微确认一下:
1 2 3 4 5 6 7 8 9 10 11 12 [root@VM-node1 ~] 192.168.206.22 | SUCCESS | rc=0 >> */10 * * * * /bin/echo Hello 192.168.206.10 | SUCCESS | rc=0 >> */10 * * * * /bin/echo Hello 192.168.206.9 | SUCCESS | rc=0 >> */10 * * * * /bin/echo Hello
已经添加好了.
我们来说说一些相关的参数吧, 首先肯定要提供的就是我们的时间间隔和任务是什么, 对应minute, hour, day, weekday, 如果不指明就是默认的*
. 接着我们需要指明添加的名字是什么, 这样Ansible才可以对他们进行操作. 如果不指定就是None, 这样造成的后果是很严重的, 相当于每一次操作就会改变全体. 接着最后我们写了一个state, 指明是添加还是去除, 默认是添加.
比如我们现在把之前添加的删除掉:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 [root@VM-node1 ~] 192.168.206.22 | SUCCESS => { "changed" : true , "envs" : [], "failed" : false , "jobs" : [] } 192.168.206.10 | SUCCESS => { "changed" : true , "envs" : [], "failed" : false , "jobs" : [] } 192.168.206.9 | SUCCESS => { "changed" : true , "envs" : [], "failed" : false , "jobs" : [] }
这样就没有了.
user user模块听名字也就知道功能是什么了吧哈哈. 对就是进行用户管理的, 有很多属性和指令, 必须要给的一个就是name用户名了:
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 [root@VM-node1 ~] 192.168.206.22 | SUCCESS => { "changed" : true , "comment" : "" , "createhome" : true , "failed" : false , "group" : 500, "home" : "/home/user1" , "name" : "user1" , "shell" : "/bin/bash" , "state" : "present" , "system" : false , "uid" : 500 } 192.168.206.10 | SUCCESS => { "changed" : true , "comment" : "" , "createhome" : true , "failed" : false , "group" : 1000, "home" : "/home/user1" , "name" : "user1" , "shell" : "/bin/bash" , "state" : "present" , "system" : false , "uid" : 1000 } 192.168.206.9 | SUCCESS => { "changed" : true , "comment" : "" , "createhome" : true , "failed" : false , "group" : 1001, "home" : "/home/user1" , "name" : "user1" , "shell" : "/bin/bash" , "state" : "present" , "system" : false , "uid" : 1001 } [root@VM-node1 ~] 192.168.206.22 | SUCCESS => { "changed" : true , "comment" : "" , "createhome" : true , "failed" : false , "group" : 500, "home" : "/home/user1" , "name" : "user1" , "shell" : "/bin/bash" , "state" : "present" , "system" : false , "uid" : 500 } 192.168.206.10 | SUCCESS => { "changed" : true , "comment" : "" , "createhome" : true , "failed" : false , "group" : 1000, "home" : "/home/user1" , "name" : "user1" , "shell" : "/bin/bash" , "state" : "present" , "system" : false , "uid" : 1000 } 192.168.206.9 | SUCCESS => { "changed" : true , "comment" : "" , "createhome" : true , "failed" : false , "group" : 1001, "home" : "/home/user1" , "name" : "user1" , "shell" : "/bin/bash" , "state" : "present" , "system" : false , "uid" : 1001 }
接着我们也可以确认一下:
1 2 3 4 5 6 7 8 9 [root@VM-node1 ~] 192.168.206.22 | SUCCESS | rc=0 >> user1:x:500:500::/home/user1:/bin/bash 192.168.206.10 | SUCCESS | rc=0 >> user1:x:1000:1000::/home/user1:/bin/bash 192.168.206.9 | SUCCESS | rc=0 >> user1:x:1001:1001::/home/user1:/bin/bash
要删除也很简单了, 直接state=absent就行了. 其他的功能通过读文档也能很快的OK.
group 就好比我们的groupadd命令的参数极其的少一样, 这个模块也很简单:
1 2 3 4 5 6 7 [root@VM-node1 ~] - name: Add or remove groups group: gid: name: state: system:
设置gid, 组名, 状态, 是否为系统组.完了. 就是这么简洁.
copy 主要用来实现文件复制, 先来实际操作一次:
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 [root@VM-node1 ~] 192.168.206.22 | SUCCESS => { "changed" : true , "checksum" : "bcc5409249b05584582f46eb8cc41d6f93df01f8" , "dest" : "/tmp/fstab.ansible" , "failed" : false , "gid" : 0, "group" : "root" , "md5sum" : "4fba41ce7527a0977db246e220f94855" , "mode" : "0640" , "owner" : "root" , "size" : 465, "src" : "/root/.ansible/tmp/ansible-tmp-1509005226.85-229350602804565/source" , "state" : "file" , "uid" : 0 } 192.168.206.10 | SUCCESS => { "changed" : true , "checksum" : "bcc5409249b05584582f46eb8cc41d6f93df01f8" , "dest" : "/tmp/fstab.ansible" , "failed" : false , "gid" : 0, "group" : "root" , "md5sum" : "4fba41ce7527a0977db246e220f94855" , "mode" : "0640" , "owner" : "root" , "secontext" : "unconfined_u:object_r:admin_home_t:s0" , "size" : 465, "src" : "/root/.ansible/tmp/ansible-tmp-1509005226.81-171401749560627/source" , "state" : "file" , "uid" : 0 } 192.168.206.9 | SUCCESS => { "changed" : true , "checksum" : "bcc5409249b05584582f46eb8cc41d6f93df01f8" , "dest" : "/tmp/fstab.ansible" , "failed" : false , "gid" : 0, "group" : "root" , "md5sum" : "4fba41ce7527a0977db246e220f94855" , "mode" : "0640" , "owner" : "root" , "secontext" : "unconfined_u:object_r:admin_home_t:s0" , "size" : 465, "src" : "/root/.ansible/tmp/ansible-tmp-1509005226.8-44103016607587/source" , "state" : "file" , "uid" : 0 }
接着简单的验证一下:
1 2 3 4 5 6 7 8 9 [root@VM-node1 ~] 192.168.206.22 | SUCCESS | rc=0 >> -rw-r----- 1 root root 465 Oct 26 16:07 /tmp/fstab.ansible 192.168.206.10 | SUCCESS | rc=0 >> -rw-r-----. 1 root root 465 Oct 26 16:51 /tmp/fstab.ansible 192.168.206.9 | SUCCESS | rc=0 >> -rw-r-----. 1 root root 465 Oct 26 16:07 /tmp/fstab.ansible
确实也存在了.
其实我们会用到的参数也不是很多, 就那一些. 目的地址是必须要指明的,而且需要使用绝对地址 . 另外, 对于目录, 如果出现了不存在的父目录, 会报错并执行失败.
使用owner/group可以来指定属主属组, 接着mode用来指定权限, 都是很好理解的. 有意思的是, src并不是必须的. 我们也可以使用content来指明文件的内容, 例如:
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 [root@VM-node1 ~] 192.168.206.22 | SUCCESS => { "changed" : true , "checksum" : "b9169ba8226473029923516a19a73c4c68f10daa" , "dest" : "/tmp/test.ansible" , "failed" : false , "gid" : 0, "group" : "root" , "md5sum" : "0bb9e5050a1820c59d836bf9ef6a5080" , "mode" : "0644" , "owner" : "root" , "size" : 26, "src" : "/root/.ansible/tmp/ansible-tmp-1509008122.2-242725231441981/source" , "state" : "file" , "uid" : 0 } ...(omitted) [root@VM-node1 ~] 192.168.206.22 | SUCCESS | rc=0 >> Hello World Hello Ansible 192.168.206.10 | SUCCESS | rc=0 >> Hello World Hello Ansible 192.168.206.9 | SUCCESS | rc=0 >> Hello World Hello Ansible
虽然, 该模块也可以提供对文件的操作, 但是更多的时候, 我们使用file模块
file 对于我们刚刚复制过去的那两个文件, 我们来修改一下属组属主, 修改一下权限, 并且来做个符号链接吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [root@VM-node1 ~] 192.168.206.22 | SUCCESS => { "changed" : true , "failed" : false , "gid" : 500, "group" : "user1" , "mode" : "0600" , "owner" : "user1" , "path" : "/tmp/fstab.ansible" , "size" : 465, "state" : "file" , "uid" : 500 } ...(omitted)
还是..来验证一下吧:
1 2 3 4 [root@VM-node1 ~] 192.168.206.22 | SUCCESS | rc=0 >> -rw------- 1 user1 user1 465 Oct 26 16:07 /tmp/fstab.ansible ...(omitted)
接着, 我们在做一个符号链接指向它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [root@VM-node1 ~] 192.168.206.22 | SUCCESS => { "changed" : true , "dest" : "/tmp/fstab.link" , "failed" : false , "gid" : 0, "group" : "root" , "mode" : "0777" , "owner" : "root" , "size" : 18, "src" : "/tmp/fstab.ansible" , "state" : "link" , "uid" : 0 } ...(omitted)
也来…验证一下吧:
1 2 3 4 [root@VM-node1 ~] 192.168.206.22 | SUCCESS | rc=0 >> lrwxrwxrwx 1 root root 18 Oct 26 17:03 /tmp/fstab.link -> /tmp/fstab.ansible ...(omitted)
注意在我们进行链接的时候要指明state=link才行. 否则会报错的. 另外, 如果创建的是硬链接, 状态就是hard了.
ping 这是一个超级简单的模块哈哈, 这样用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [root@VM-node1 ~] 192.168.206.22 | SUCCESS => { "changed" : false , "failed" : false , "ping" : "pong" } 192.168.206.10 | SUCCESS => { "changed" : false , "failed" : false , "ping" : "pong" } 192.168.206.9 | SUCCESS => { "changed" : false , "failed" : false , "ping" : "pong" }
如果主机可以ping通, 就会显示pong!
service 看名字就可以意识到这是一个超级重要的模块了吧哈哈. 我们来看一下吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [root@VM-node1 ~] [WARNING]: Consider using service module rather than running service 192.168.206.22 | FAILED | rc=3 >> httpd is stoppednon-zero return code 192.168.206.10 | FAILED | rc=3 >> ● httpd.service - The Apache HTTP Server Loaded: loaded (/usr/lib/systemd/system/httpd.service; disabled; vendor preset: disabled) Active: inactive (dead) Docs: man:httpd(8) man:apachectl(8) Oct 25 03:50:01 VM-node2 systemd[1]: Unit httpd.service cannot be reloaded because it is inactive.Redirecting to /bin/systemctl status httpd.servicenon-zero return code
由于我们的后端两个主机不是一个系统, 这就造成输出的信息并不一致的情况了, 接着我们启动并且设置成开机自动启动httpd服务:
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 [root@VM-node1 ~] 192.168.206.22 | SUCCESS => { "changed" : true , "enabled" : true , "failed" : false , "name" : "httpd" , "state" : "started" } 192.168.206.10 | SUCCESS => { "changed" : true , "enabled" : true , "failed" : false , "name" : "httpd" , "state" : "started" , "status" : { "ActiveEnterTimestampMonotonic" : "0" , "ActiveExitTimestampMonotonic" : "0" , ...(omitted) "UnitFileState" : "disabled" , "Wants" : "system.slice" , "WatchdogTimestampMonotonic" : "0" , "WatchdogUSec" : "0" } }
( CentOS7输出的信息真可怕…
接着我们确认一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 [root@VM-node1 ~] [WARNING]: Consider using service module rather than running service 192.168.206.22 | SUCCESS | rc=0 >> httpd (pid 14688) is running... 192.168.206.10 | SUCCESS | rc=0 >> ● httpd.service - The Apache HTTP Server Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled) Active: active (running) since Thu 2017-10-26 18:19:33 CST; 2min 48s ago Docs: man:httpd(8) man:apachectl(8) Main PID: 9510 (httpd) Status: "Total requests: 0; Current requests/sec: 0; Current traffic: 0 B/sec" CGroup: /system.slice/httpd.service ├─9510 /usr/sbin/httpd -DFOREGROUND ├─9511 /usr/sbin/httpd -DFOREGROUND ├─9513 /usr/sbin/httpd -DFOREGROUND ├─9514 /usr/sbin/httpd -DFOREGROUND ├─9516 /usr/sbin/httpd -DFOREGROUND └─9517 /usr/sbin/httpd -DFOREGROUND Oct 26 18:19:33 VM-node2 systemd[1]: Starting The Apache HTTP Server... Oct 26 18:19:33 VM-node2 systemd[1]: Started The Apache HTTP Server.Redirecting to /bin/systemctl status httpd.service
确实已经启动, 而且, 开机自启动也已经OK了.
总结一下 service模块通过enabled来决定是否开机自动启动, 取值true或者false. name: 服务名称. state: 状态(stopped, started, restarted)
shell shell模块和command模块有点类似, 但是不同之处在于, command不提供变量支持. 我们来做个测试就知道了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [root@VM-node1 ~] 192.168.206.22 | SUCCESS | rc=0 >> user1:!!:17465:0:99999:7::: 192.168.206.10 | SUCCESS | rc=0 >> user1:!!:17465:0:99999:7::: [root@VM-node1 ~] 192.168.206.22 | SUCCESS | rc=0 >> password | passwd --stdin user1 192.168.206.10 | SUCCESS | rc=0 >> password | passwd --stdin user1 [root@VM-node1 ~] 192.168.206.22 | SUCCESS | rc=0 >> user1:!!:17465:0:99999:7::: 192.168.206.10 | SUCCESS | rc=0 >> user1:!!:17465:0:99999:7:::
没有设置成功, 原因是我们使用了管道符. 接下来换做shell试试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [root@VM-node1 ~] 192.168.206.22 | SUCCESS | rc=0 >> Changing password for user user1. passwd: all authentication tokens updated successfully. 192.168.206.10 | SUCCESS | rc=0 >> Changing password for user user1. passwd: all authentication tokens updated successfully. [root@VM-node1 ~] 192.168.206.22 | SUCCESS | rc=0 >> user1:$6$paTv //V6$UmwQlj0OinDI9RLkIcHUWFvQNHxZF9aacuH272zDaRyoKpn21jH568IVLEJv .Q8ShxPrtie9CHdfEuiVFgOwl0:17465:0:99999:7::: 192.168.206.10 | SUCCESS | rc=0 >> user1:$6$3DO .U5ry$uAXVYHoNz7R5IfdksZZZMvcYLnlXYKuIHebpg65obLoTupzZZmbAFvY4soylGBwepad33v6QJ6sPqbduAwMUX .:17465:0:99999:7:::
提示语也是不一样的. 密码也成功的更新了.
shell虽然支持这样的, 但是如果是要执行脚本, 我们就需要下一个模块了
script script模块用于脚本执行, 比较坑的一个地方是, 它不支持绝对路径, 也就是说仅仅只有当你使用相对路径才接受:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [root@VM-node1 ~] echo "Test" > /tmp/script.ansible[root@VM-node1 ~] [root@VM-node1 ~] 192.168.206.22 | SUCCESS => { "changed" : true , "failed" : false , "rc" : 0, "stderr" : "Shared connection to 192.168.206.22 closed.\r\n" , "stdout" : "" , "stdout_lines" : [] } 192.168.206.10 | SUCCESS => { "changed" : true , "failed" : false , "rc" : 0, "stderr" : "Shared connection to 192.168.206.10 closed.\r\n" , "stdout" : "" , "stdout_lines" : [] }
按照惯例, 验证一下:
1 2 3 4 5 6 [root@VM-node1 ~] 192.168.206.22 | SUCCESS | rc=0 >> Test 192.168.206.10 | SUCCESS | rc=0 >> Test
yum 见名知意啦, 为了yum安装软件包的一个模块, 使用起来也很简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [root@VM-node1 ~] 192.168.206.22 | SUCCESS => { "changed" : true , "failed" : false , "msg" : "" , "rc" : 0, "results" : [ "Loaded plugins: ...(omitted) ] } 192.168.206.10 | SUCCESS => { " changed": true, " failed": false, " msg": " ", " rc": 0, " results": [ ...(omitted) ] }
这样就安装完成了, 我们来验证一下咯:
1 2 3 4 5 6 7 8 [root@VM-node1 ~] [WARNING]: Consider using yum, dnf or zypper module rather than running rpm 192.168.206.22 | SUCCESS | rc=0 >> zsh-4.3.11-4.el6.centos.2.x86_64 192.168.206.10 | SUCCESS | rc=0 >> zsh-5.0.2-28.el7.x86_64
删除也是很简单的, 直接指明state=absent就行了:
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 [root@VM-node1 ~] 192.168.206.10 | SUCCESS => { "changed" : true , "failed" : false , "msg" : "" , "rc" : 0, "results" : [ ...(omitted) ] } 192.168.206.22 | SUCCESS => { "changed" : true , "failed" : false , "msg" : "" , "rc" : 0, "results" : [ ...(omitted) ] } [root@VM-node1 ~] [WARNING]: Consider using yum, dnf or zypper module rather than running rpm 192.168.206.22 | FAILED | rc=1 >> package zsh is not installednon-zero return code 192.168.206.10 | FAILED | rc=1 >> package zsh is not installednon-zero return code
这样就删除掉了.
setup 这个模块可以用来获取主机的fact信息. 什么是fact呢? 为了能够管理各个主机, ansible会获取每一个主机的一些信息, 包括系统信息, 内核版本, IP地址, BIOS日期等等大量信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 [root@VM-node1 ~] 192.168.206.22 | SUCCESS => { "ansible_facts" : { "ansible_all_ipv4_addresses" : [ "192.168.206.22" ], "ansible_all_ipv6_addresses" : [ "fe80::20c:29ff:feb0:b47f" ], "ansible_apparmor" : { "status" : "disabled" }, "ansible_architecture" : "x86_64" , "ansible_bios_date" : "07/02/2015" , "ansible_bios_version" : "6.00" , "ansible_cmdline" : { "KEYBOARDTYPE" : "pc" , "KEYTABLE" : "us" , "LANG" : "en_US.UTF-8" , "SYSFONT" : "latarcyrheb-sun16" , "quiet" : true , ...(omitted)
YAML 说了一些模块的使用, 接下来我们就可以开始尝试写一下playbook了. 但在此之前, 我们还是要了解一下playbook的格式 – YAML
如果你使用过hexo, 并且乐于配置的话. 相信对YAML挺熟悉的了.不过我们在说说这个东西吧.
YAML也是一个递归缩写, 即YAML Ain’t Markup Language.
YAML通过缩进和键值对来表示数据结构. 使用一个-
来表示列表, 使用一个:
来表示键值对, 一个示例:
1 2 3 4 5 6 7 8 9 name: Justin age: 20 gender: Male attr: isAlive: True isHappy: True skills: - Linux - Windows
很简单了..
Playbook 想要写一个playbook, 我们先要了解到playbook涉及到那些东西, 请看下面:
Inventory
Modules
Playbooks
Tasks
Variable
Templates
Handlers
Roles
我们一个一个说, 首先还是我们的Inventory. 这个时候用来定义主机的对不上面说过了, 现在来补充一下.
首先我们可以把几个节点放在一起构成一个组:
1 2 3 4 5 [webservers] 192.168.207.100:8080 192.168.207.200 web1.justin.com web2.justin.com
如果主机遵循相同的命名规范, 我们还可以简写:
1 2 [webservers] web[1:10].justin.com
另外, 我们还可以在inventory中定义一些主机变量供playbook使用,这些变量仅仅属于这些主机.
1 2 3 [frontend] web1.scuec.com http_port=80 maxRequestPerChild=1000 web2.scuec.com http_port=8080 maxRequestPerChild=500
除了主机变量, 还有组变量, 也就是属于一个组的变量:
1 2 3 4 5 6 [webservers] www[1:10].justin.com [webservers:vars] nfs_server=nfs.justin.com ntp_server=ntp.justin.com
另外, Inventory还可以进行组嵌套:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [Group1] www.douban.com www.zhihu.com [Group2] www.sciencenet.cn www.weather.com.cn [target:children] Group1 Group2 [target:vars] var=XXX
还有一些属于Inventory的变量, 例如ansible_ssh_port, ansible_ssh_host, ansible_ssh_pass
等等.
现在我们来看一个playbook的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 - hosts: webnodes vars: http_port: 80 max_client: 256 remote_user: root tasks: - name: ensure apache is at the latest ver. yum: name=httpd state=latest - name: ensure apache is running service: name=httpd state=started Handlers: - name: restart apache service: name=httpd state=restarted
tasks就是任务了, 调用模块完成操作, vars就是声明变量的区域, handlers就是由某事件触发的一些处理器. 最后的roles就是角色之意. 我们继续往后.
先不说后面的处理器, 我们来写一个简单的playbook执行试试.
1 2 3 4 5 6 7 8 9 10 11 - hosts: testservers remote_user: root vars: ntp_server: edu.ntp.org.cn tasks: - name: install ntp client yum: name=ntp state=latest - name: update time immediately command : "ntpdate {{ ntp_server }}" - name: create cron cron: hour=*/12 job="ntpdate {{ ntp_server }}" name=ntp_sync state=present
很简单的一个剧本, 就是安装ntp接着进行时间同步, 定制定时任务以及显示当前的时间.
我们来执行一下看看:
执行playbook的时候, 我们使用ansible-playbook程序, 最直接的方法就是在后面跟上yml的playbook文件, 也可以使用-e(添加额外变量)等选项. 另外这个地方我把testservers的inventory做了一下小修改:
1 2 3 4 5 6 [testservers] VM-node[2:3] [webservers] 192.168.206.22 192.168.206.10
就是这样的效果, 很简单吧:
再来写一个和tinyproxy相关的playbook, 接着引出我们的handler,
web_tinyproxy.yml
内容如下:
1 2 3 4 5 6 7 8 9 - hosts: testservers remote_user: root tasks: - name: Install tinyproxy service yum: name=tinyproxy state=latest - name: Copy configuration file copy: src=/etc/tinyproxy/tinyproxy.conf dest=/etc/tinyproxy/tinyproxy.conf - name: Start tinyproxy service service: name=tinyproxy state=started
执行效果:
有意思的是, 现在的配置文件监听端口是2333
, 我们来验证一下:
1 2 3 4 5 6 [root@VM-node1 ~] VM-node3 | SUCCESS | rc=0 >> 0 128 *:2333 *:* VM-node2 | SUCCESS | rc=0 >> LISTEN 0 128 *:2333 *:*
接着我们修改一下本地的配置, 将端口换成23333
:
再次执行一次playbook:
从转型结果我们可以看出来, 服务并没有得到重启. 尽管我们的配置文件是已经重新复制了( 因为文件被修改了 ). 这个时候就需要我们的Handler来根据task的执行情况来触发特定的task.
说来也简单, 当我们的配置文件被修改的时候, 显然是应该重启服务的, 所以我们应该通知 一下, 从而去执行特定的操作, handler和我们的tasks同级别, 如果没有人**通知(notify)**它. 就不会执行里面的任务, 通知的方式就是通过notify关键字, 指明哪一个handler, 直接看我们的实例吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 - hosts: testservers remote_user: root tasks: - name: Install tinyproxy service yum: name=tinyproxy state=latest - name: Copy configuration file copy: src=/etc/tinyproxy/tinyproxy.conf dest=/etc/tinyproxy/tinyproxy.conf notify: - restart tinyproxy - name: Start tinyproxy service service: name=tinyproxy state=started handlers: - name: restart tinyproxy service: name=tinyproxy state=restarted
这个时候, 我们还需要修改一下配置文件, 比如随便再把端口换一换, 9990吧. 接着再次执行一次:
多了一个RUNNING HANDLER
吧. 这个时候看一下, 端口也确实是改变了.
另外, 我们的playbook中还可以使用条件测试, 通过when关键字可以根据具体的一些情况来选择性的执行, 来看这样的一个playbook
1 2 3 4 5 6 7 [root@VM-node1 ~ ] - hosts: testservers remote_user: root tasks: - name: make a test copy: content="This is CentOS7" dest=/tmp/version.ansible when: ansible_distribution_major_version == "7"
执行之后就是这样的了:
其中ansible_distribution_major_version
是fact中的变量, 我们可以直接进行引用.
从结果看出来了, 因为我们的两个节点一个是CentOS7, 一个是 CentOS6, 所以仅仅执行了一个.
除了条件测试 , playbook还可以使用迭代, 就像这样:
1 2 3 4 5 6 7 8 - hosts: testservers remote_user: root tasks: - name: add users user: name={{ item.name }} groups={{ item.groups }} state=present with-items: - { name: 'test1' , groups: 'wheel' } - { name: 'test2' , groups: 'testgroup' }
其中, item是一个固定的变量, 会从with-items中定义的变量中取值. 不仅如此, 我们的变量可以通过字典的形式来取值.
上面的迭代展开, 其实就像是两个差不多的task. 这样写就更加简洁.
接下来, 我们再来了解一下Jinja2的模板语言. 由于Ansible使用的就是Jinja2, 所以支持Jinja2的各种语法. 不过说真的, 用的地方用的不是很多, 我们使用最多的地方应该就是变量替换了吧.
来看看一个配置文件好了:
1 2 Listen {{ port }} ServerName {{ servername }}
还记得我们说过的Inventory中的变量吗, 在这里就可以派上用场了:
1 2 3 [webservers] 192.168.206.22 port=80 servername=VM-node3 192.168.206.10 port=8080 servername=VM-node2
这样我们就可以通过模板这个模块来展开变量, 从而达到不同配置的需求.
接着我们看一下Tag功能, 如果只想运行一个playbook中的一个单独的或者特定的task, 我们可以在task中写上tags关键字:
1 2 3 4 5 6 7 8 9 - hosts: webservers remote_user: root tasks: - name: Install httpd service yum: name=httpd state=latest - name: Copy configuration file template: src=/root/templates/httpd.j2 dest=/etc/httpd/conf/httpd.conf - name: Ensure apache service is running service: name=httpd state=restarted
假设说每次执行这个playbook他都会从头执行到尾, 但是假设我现在就指向执行其中的复制和重启, 而不像执行安装的话, 怎么办呢? 我们在template和service下加上tags关键字:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 - hosts: testservers remote_user: root tasks: - name: Install tinyproxy service yum: name=tinyproxy state=latest - name: Copy configuration file copy: src=/etc/tinyproxy/tinyproxy.conf dest=/etc/tinyproxy/tinyproxy.conf notify: - restart tinyproxy tags: - skip-install - name: Start tinyproxy service service: name=tinyproxy state=started handlers: - name: restart tinyproxy service: name=tinyproxy state=restarted
我们在复制文件的那个地方加入了tags, 接着我们执行:
看, 只有加上tag的那一个任务才会执行.