在寫這篇blog前,我的心情久久不能平靜,雖然明白運維工作如履薄冰,但沒有料到這么一個細小的疏漏會帶來如此嚴重的災難。這是一起其他公司誤用puppet參數引發的事故,而且這個參數我也曾被“坑過”。
0. 一個purge參數引發的事故
故事要從周二下午說起,安靜了一天的某技術交流群,突然有個驚慌失措的同學在群里說,他直接使用了第三方的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數據目錄下的文件全部清空了。
從這次事故中,我們可以看到很多問題:
1. 部署代碼的上線居然沒有通過開發和測試環境的驗證
2. 使用第三方模塊時,竟然不閱讀源碼或者README文件,也沒有運行測試
3. 上線沒有審批流程,上線負責人的失職
4. .....
首先,有一個觀念需要矯正,有些人認為部署邏輯不屬於開發范疇,往往編寫后就直接上線,其實只要涉及到代碼的變更,無論是業務邏輯還是部署邏輯,都需要通過開發環境和測試環境的驗證。那么如何做好部署邏輯的驗證工作?
對於編寫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,可以准備發布到線上了。當然不能少了線上變更流程,寫下在此次線上變更的詳細操作以及回滾機制。
尾聲
故事的尾聲,我想告訴大家不幸中的萬幸,那個可憐的同學,最終找回了大部分的數據。