一个 purge 参数引发的惨案 —— 从线上 hbase 数据被删事故说起
在写这篇 blog 前,我的心情久久不能平静,虽然明白运维工作如履薄冰,但没有料到这么一个细小的疏漏会带来如此严重的灾难。这是一起其他公司误用 puppet 参数引发的事故,但是这个参数我也曾被“坑过”。
先说说这起事故,在周二下午,安静了一天的某个技术交流群,突然有个惊慌失措的同学在群里说,他拿第三方的 puppet hbase module 来管理线上 hbase 集群,结果这个模块在管理数据文件夹时,使用了一个 purge 参数把几乎所有的线上数据都删完了。
他已经和领导汇报了情 况,那边正在紧急讨论处理方案。他在做好打包走人的准备的同时,仍抱有一丝希望来询问我们有没有办法恢复数据,大家纷纷为他献计献策... 我就想起两年前,我第一次尝试使用 puppet-apache 模块管理 apache 服务, apache::init
类中默认设置了 purge_configs 参数为 True,导致我把 apache 目录下的所有 vhost 文件删掉了,万幸的是我是在开发环境发现了这个问题。 那么,我们来看看这个“邪恶”的 purge 参数是什么样子的:
file {'/var/lib/data_directory': xxxx => xxx, ...... recurse => true, purge => true }
file 是 puppet 的默认 resource type,用于管理文件或者文件夹。在管理文件夹时,只有当设置了 recurse 为 true 的情况下,purge 参数才会生效。这段逻辑的意思是要清空 /var/lib/data_directory 目录下所有非 puppet 管理的文件, purge 参数 通常的目的是清理管理目录以及防止被他人恶意添加本不该存在的文件。但也因为这段逻辑,就把 hbase 数据目录下的文件全部清空了。 从这次事故中,我们可以看到很多问题:
- 部署代码的上线居然没有通过开发和测试环境的验证
- 使用第三方模块时,竟然不阅读源码或者 README 文件,也没有运行测试
- 上线没有审批流程,上线负责人的失职
首先,有一个观念需要矫正,有些人认为部署逻辑不属于开发范畴,往往编写后就直接上线,其实只要涉及到代码的变更,无论是业务逻辑还是部署逻辑,都需要通过开发环境和测试环境的验证。那么如何做好部署逻辑的验证工作? 对于编写 puppet 来实现部署逻辑的工程师来说:少一个花括号或者分号,就可能导致代码无法运行;遗漏某个 class 或者某个参数就会使节点 无法到达期望的状态;错误的执行顺序,甚至可能会导致系统崩溃或者网络不可达。为如何保证所编写的 manifests 符合你的预期?
1. 语法检查
和其他的编程语言一样,语法检查是基本步骤,因此使用 puppet 解析器做语法检查是最基础也是必不可少的验证工作。你可以使用 puppet parser validate 命令来检查某个 manifest 文件: 例如,我在 logserver.pp 中的$eth0_netmask 变量后面漏掉了逗号:
puppet parser validate logserver.pp Error: Could not parse for environment production: Syntax error at 'eth0_netmask' at sunfire/manifests/logserver.pp:6:3
对于 erb template,你可以使用 erb -P -x -T '-' $1 | ruby -c 命令来做检查。我在 route-eth.erb 中漏掉了 if 判断语句的结束标记,此时执行语法检测会发生以下提示:
route-eth.erb:1: syntax error, unexpected '<' <%= @internal_network %> via <%= @internal_gateway %>
2. 代码风格检查
每个语言都有一套规范的语法风格指南,puppet 也不例外: https://docs.puppetlabs.com/guides/style_guide.htm 我们可以使用 Github 的 Tim sharpe 所开发的 puppet-lint 工具来分析你所写的 manifests 文件。 例如检查一个 manifests 文件:
puppet-lint manifests/init.pp WARNING: class inheriting from params class on line 339 WARNING: line has more than 80 characters on line 47 WARNING: line has more than 80 characters on line 167
如果你希望检查整个 puppet mainifest 目录,你需要添加: require 'puppet-lint/tasks/puppet-lint' 到 Rakefile 里,然后运行 rake lint 即可。 在某些情况下,你不得不关闭某些检查,例如,想要关闭 80 character check,你可以如下运行:
puppet-lint --no-80chars-check /path/to/my/manifest.pp
需要注意的是,puppet-lint 仅作代码风格的检查,不能替代语法检查。
3. 模块测试
你可以使用 rspec 来确保所有的模块符合预期。举一个例子,你希望编写一个测试来确保当使用 puppet-keystone 模块时,keystone 包被正确地安装,系统中添加了 keystone 用户和组,可以编写一个 keystone_spec.rb 文件来做测试:
it { should contain_package('keystone').with( 'ensure' => param_hash['package_ensure'] ) } it { should contain_group('keystone').with( 'ensure' => 'present', 'system' => true ) } it { should contain_user('keystone').with( 'ensure' => 'present', 'gid' => 'keystone', 'system' => true ) }
随后执行 rspec 来验证这些测试能否通过。
如果希望集成到 rake 命令中去,我们可以在 Rakefile 里添加:
require 'puppetlabs_spec_helper/rake_tasks'
随后使用以下命令来完成相应的 spec 执行模块测试:
rake spec # Run spec tests in a clean fixtures directory rake spec_clean # Clean up the fixtures directory rake spec_prep # Create the fixtures directory rake spec_standalone # Run spec tests on an existing fixtures directory
因此,在使用从 github 或者 puppetforge 下载的 module 时,阅读 README 和测试用例是非常重要的,如果我当时仔细阅读了 apache::init 的测试用例,也不会出现所谓被坑的问题,因为人家明明在 apache_spec.rb 里写有对/etc/apache /sites-enabled 目录的测试:
it { should contain_file("/etc/httpd/conf.d").with( 'ensure' => 'directory', 'recurse' => 'true', 'purge' => 'true', 'notify' => 'Class[Apache::Service]', 'require' => 'Package[httpd]' ) }
4. 开发环境和测试环境的验证
最终部署逻辑能否上线到生产环境,还需要在开发环境和测试环境进行验证。可以使用目前流行的 vagrant,Openstack 等工具搭建一个测试 平台,调用 API 创建符合生产环境的集群,通过 puppet 做软件安装和配置,验证部署逻辑是否符合预期。开发环境和测试环境的不同点在于,测试环境的所 有变更与线上环境完全一致,不允许有任何的人工干预。
至此,一个通过验证的 puppet 部署逻辑可以 release 了,打上 tag,可以准备发布到线上了。当然不能少了线上变更流程,写下在此次线上变更的详细操作以及回滚机制。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论