詳解:從靜態(tài)清單到動(dòng)態(tài)Inventory的自動(dòng)化運(yùn)維實(shí)踐)
作為一個(gè)常年跟服務(wù)器打交道的運(yùn)維我對(duì)Ansible的態(tài)度一直是“能自動(dòng)化的絕不手工”。而在所有Ansible命令里-i參數(shù)可能是你最早接觸、卻又最容易用錯(cuò)的那一個(gè)。別小看它——ansible -i /path/to/inventory這種寫法表面上是指定一個(gè)清單文件實(shí)際上牽扯到一組主機(jī)的組織方式、環(huán)境隔離策略、批量的可重復(fù)性甚至是整個(gè)自動(dòng)化運(yùn)維體系能不能上線的關(guān)鍵。這篇東西不打算寫成手冊(cè)式的列舉而是想把我踩過(guò)的坑、驗(yàn)證過(guò)的用法、以及排查思路都攤開(kāi)聊一聊。無(wú)論你是剛接觸Ansible劇本的初學(xué)者還是已經(jīng)在自動(dòng)化運(yùn)維里摸爬滾打的老人這篇文章都會(huì)圍繞-i參數(shù)的幾種典型用法結(jié)合真實(shí)場(chǎng)景把原理、取舍和實(shí)操細(xì)節(jié)講清楚。看完之后你至少能明白什么時(shí)候該用靜態(tài)inventory什么時(shí)候該換動(dòng)態(tài)inventory-i和--limit到底怎么配合以及為什么說(shuō)“指定清單”這件小事往往是自動(dòng)化最值得投入精力的地方。1. 從inventory說(shuō)起-i參數(shù)到底在干什么1.1 最基礎(chǔ)也最容易忽略的概念講-i參數(shù)之前繞不開(kāi)inventory。Ansible本身沒(méi)有任何“服務(wù)器列表”的固化概念它執(zhí)行命令、跑劇本都依賴于一份或多份主機(jī)清單。默認(rèn)情況下Ansible會(huì)去找/etc/ansible/hosts但這在生產(chǎn)環(huán)境里幾乎不可用——你不可能讓所有項(xiàng)目都搶同一個(gè)文件更不可能在多人協(xié)作的時(shí)候?qū)χ粋€(gè)全局文件反復(fù)修改。所以真正的做法是用-i明確指定清單文件的路徑。-i的全稱是--inventory后面跟的既可以是一個(gè)普通文件也可以是一個(gè)目錄甚至可以是逗號(hào)分隔的主機(jī)列表。三種形式解決三種不同的問(wèn)題文件形式ansible -i hosts.ini all -m ping適合靜態(tài)環(huán)境簡(jiǎn)單直觀。目錄形式ansible -i inventory/dev/ all -m ping適合多環(huán)境管理目錄里可以放多個(gè)清單文件。逗號(hào)列表形式ansible -i web01,web02, all -m ping完全不依賴文件臨時(shí)排查、快速測(cè)試時(shí)極其方便。我第一次意識(shí)到-i的重要性是在同事留下的一堆混亂腳本里。當(dāng)時(shí)所有命令都依賴默認(rèn)的/etc/ansible/hosts一旦有人改了那個(gè)文件全公司跑腳本的人都受影響。后來(lái)我把清單全部改成顯式指定配合目錄隔離整個(gè)運(yùn)維腳本的穩(wěn)定性立刻上了一個(gè)臺(tái)階。這里想強(qiáng)調(diào)一個(gè)觀念顯式優(yōu)于隱式。-i把“我的命令控制哪些主機(jī)”這件事變成了一條明線而不是藏在某個(gè)全局文件里暗流涌動(dòng)。1.2 為什么不用默認(rèn)路徑有讀者可能會(huì)問(wèn)既然系統(tǒng)有默認(rèn)的/etc/ansible/hosts為什么非要折騰-i原因是實(shí)際場(chǎng)景里默認(rèn)路徑暴露了三個(gè)致命問(wèn)題。第一權(quán)限和隔離。生產(chǎn)、測(cè)試、開(kāi)發(fā)環(huán)境的清單如果擠在一個(gè)文件里任何誤操作都可能同時(shí)影響多個(gè)環(huán)境用-i配合獨(dú)立目錄每個(gè)環(huán)境自成體系就算有人改錯(cuò)了影響范圍也可控。第二可重復(fù)性。自動(dòng)化運(yùn)維的核心目標(biāo)之一是“同樣的腳本、同樣的參數(shù)在任何時(shí)間執(zhí)行結(jié)果一致”默認(rèn)路徑依賴機(jī)器本地的文件狀態(tài)換一臺(tái)機(jī)器跑結(jié)果就不同而把-i寫進(jìn)腳本里清單和腳本就成了一體的交付物。第三多項(xiàng)目共存。一個(gè)服務(wù)器上可能跑著多個(gè)業(yè)務(wù)線A項(xiàng)目的清單和B項(xiàng)目的清單本來(lái)就應(yīng)該互相隔離全局文件設(shè)計(jì)上就不義。還有一個(gè)細(xì)節(jié)值得注意-i指定的文件不要求固定的擴(kuò)展名。很多人習(xí)慣叫hosts或者inventory但Ansible只認(rèn)內(nèi)部格式不認(rèn)后綴名。你隨便命名prod、dev.ini、inventory.yaml都可以只要內(nèi)容符合格式要求。這給了我們?cè)谖募M織上極大的自由度后續(xù)在多環(huán)境管理那一節(jié)我會(huì)給出具體的目錄結(jié)構(gòu)。2. 實(shí)戰(zhàn)場(chǎng)景多環(huán)境、動(dòng)態(tài)清單、批量部署2.1 場(chǎng)景一多環(huán)境管理目錄化拆解實(shí)際工作中最常見(jiàn)的需求就是區(qū)分開(kāi)發(fā)、測(cè)試、生產(chǎn)環(huán)境。如果你把所有環(huán)境的主機(jī)都放在一個(gè)大文件里那每次執(zhí)行都要小心翼翼地加--limit去圈定范圍既煩瑣又危險(xiǎn)。我更推薦把-i指向一個(gè)目錄目錄里每個(gè)子目錄或文件對(duì)應(yīng)一個(gè)環(huán)境。舉個(gè)例子我的常見(jiàn)目錄結(jié)構(gòu)是這樣inventory/ ├── dev/ │ ├── hosts.ini │ └── group_vars/ ├── test/ │ ├── hosts.ini │ └── group_vars/ └── prod/ ├── hosts.ini └── group_vars/執(zhí)行時(shí)就這樣寫ansible-playbook -i inventory/prod/ deploy.yml為什么目錄化更好因?yàn)锳nsible讀取一個(gè)目錄時(shí)會(huì)自動(dòng)合并目錄下所有清單內(nèi)容和group_vars、host_vars目錄里的變量定義。這樣一來(lái)清單文件、組變量、主機(jī)變量就形成了完整的環(huán)境配置包。你交付的不再是“一個(gè)主機(jī)列表”而是一整套環(huán)境定義。更關(guān)鍵的是-i inventory/prod/和-i inventory/test/兩個(gè)命令在邏輯上強(qiáng)制隔離了環(huán)境。哪怕你在劇本里寫錯(cuò)了變量它的影響范圍也僅限于該命令指定的環(huán)境。我見(jiàn)過(guò)不少事故都是因?yàn)槟硞€(gè)文件被復(fù)用、變量串環(huán)境導(dǎo)致的目錄化之后這類問(wèn)題基本絕跡。2.2 場(chǎng)景二動(dòng)態(tài)inventory讓清單自己長(zhǎng)出來(lái)靜態(tài)文件在多環(huán)境場(chǎng)景下夠用但一旦上了云主機(jī)隨時(shí)彈性伸縮手工維護(hù)靜態(tài)清單就完全不現(xiàn)實(shí)了。AWS、阿里云、騰訊云的主機(jī)IP天天變你不能每次擴(kuò)縮容就去改一次hosts文件。這時(shí)候就該上動(dòng)態(tài)inventory。動(dòng)態(tài)inventory本質(zhì)上是一個(gè)可執(zhí)行腳本Ansible運(yùn)行-i script.py時(shí)會(huì)執(zhí)行這個(gè)腳本并解析它輸出到標(biāo)準(zhǔn)輸出的JSON數(shù)據(jù)。腳本根據(jù)云平臺(tái)API查詢實(shí)例狀態(tài)動(dòng)態(tài)生成主機(jī)列表和組信息。常見(jiàn)的實(shí)現(xiàn)方式有兩種使用官方或社區(qū)提供的動(dòng)態(tài)inventory插件。比如amazon.aws.aws_ec2、azure.azure_rm、google.cloud.gcp_compute只要在ansible.cfg里啟用對(duì)應(yīng)插件配上訪問(wèn)密鑰和區(qū)域信息就能直接以插件方式動(dòng)態(tài)獲取主機(jī)列表。自己寫一個(gè)腳本封裝API調(diào)用。適合定制化需求比如只篩選特定標(biāo)簽、特定VPC內(nèi)的實(shí)例。用一句話總結(jié)動(dòng)態(tài)inventory的價(jià)值-i不再指向一個(gè)文件而是指向一個(gè)“生成器”。它讓Ansible的主機(jī)清單和云上真實(shí)實(shí)例狀態(tài)始終保持一致消除手工同步帶來(lái)的滯后和誤差。我自己維護(hù)過(guò)一個(gè)內(nèi)部工具腳本大概邏輯是讀取配置文件里的云廠商AK/SK調(diào)用查詢接口拿回所有實(shí)例ID和IP再按實(shí)例名稱的前綴歸類到不同組。然后我只需要執(zhí)行ansible -i /opt/scripts/dynamic_inventory.py --list就能看到完整的JSON輸出驗(yàn)證腳本返回的數(shù)據(jù)結(jié)構(gòu)對(duì)不對(duì)。確認(rèn)無(wú)誤后再把它接到ansible-playbook里使用效果立竿見(jiàn)影。以前處理一批新擴(kuò)容機(jī)器先要把IP人工加進(jìn)文件再跑劇本現(xiàn)在擴(kuò)容完成直接跑劇本新節(jié)點(diǎn)自動(dòng)進(jìn)入目標(biāo)組。這里要特別提醒一個(gè)動(dòng)態(tài)inventory的細(xì)節(jié)腳本必須有可執(zhí)行權(quán)限而且輸出必須是合法的JSON。這是新手最容易栽的地方。Ansible執(zhí)行動(dòng)態(tài)清單腳本時(shí)會(huì)先運(yùn)行它再解析標(biāo)準(zhǔn)輸出任何一句額外的日志打印或者Python的print調(diào)試信息都會(huì)污染輸出導(dǎo)致解析失敗。我常用的排查方法就是先在命令行手動(dòng)運(yùn)行一次腳本看輸出的JSON是否完整再做調(diào)試。2.3 場(chǎng)景三臨時(shí)指定主機(jī)列表一條命令解決排查需求不是所有場(chǎng)景都需要文件和目錄。有時(shí)你只是想確認(rèn)某幾臺(tái)機(jī)器上某個(gè)服務(wù)是否啟動(dòng)或者快速批量執(zhí)行一個(gè)命令這時(shí)候最方便的反而是直接傳逗號(hào)分隔列表。ansible -i 192.168.1.21,192.168.1.22, all -m command -a systemctl status nginx注意寫法末尾有個(gè)逗號(hào)這是為了告訴Ansible“這是一個(gè)列表而不是一個(gè)文件名”。如果不加末尾逗號(hào)Ansible會(huì)把整段字符串當(dāng)作路徑去解析然后報(bào)錯(cuò)找不到文件。這個(gè)細(xì)節(jié)非常隱蔽我見(jiàn)過(guò)不止一個(gè)同事在這上面卡了半小時(shí)。這種臨時(shí)列表形式最適合配合-m模塊和-a參數(shù)做快速操作例如批量ping、查uptime、分發(fā)公鑰ansible -i node01,node02, all -m authorized_key -a userroot key{{ lookup(\file\, \/home/user/.ssh/id_rsa.pub\) }}它的優(yōu)勢(shì)就是零依賴不需要造文件、不需要考慮目錄結(jié)構(gòu)適合臨時(shí)搭建的環(huán)境或者故障排查。缺點(diǎn)也很明顯主機(jī)信息沒(méi)有持久化組變量、主機(jī)變量完全沒(méi)法用而且容易手滑漏掉某臺(tái)機(jī)器。所以我的建議是把這種形式限制在“一次性小操作”任何需要重復(fù)執(zhí)行的任務(wù)都應(yīng)該至少落一個(gè)靜態(tài)清單文件。3. 參數(shù)變體與核心細(xì)節(jié)解析3.1-i和--limit的辨析很多初學(xué)者會(huì)把-i和--limit混為一談或者用起來(lái)沒(méi)有章法。實(shí)際上這兩者解決的問(wèn)題完全不同。-i決定的是“候選主機(jī)池”也就是Ansible能從哪些主機(jī)里挑機(jī)器--limit決定的是“在這個(gè)池子里執(zhí)行哪些主機(jī)”。用白話講-i是劃定范圍--limit是細(xì)化到子集。生產(chǎn)中的正確姿勢(shì)是把環(huán)境級(jí)別的邊界全部交給-i避免意外觸碰環(huán)境外主機(jī)把單次執(zhí)行的目標(biāo)圈定交給--limit實(shí)現(xiàn)“整個(gè)生產(chǎn)池我只動(dòng)其中某臺(tái)”。舉例ansible-playbook -i inventory/prod/ upgrade.yml --limit db-01這條命令的含義是所有生產(chǎn)主機(jī)都是可操作范圍但我這次只對(duì)db-01執(zhí)行升級(jí)。好處是由于-i明確指向生產(chǎn)環(huán)境清單那么即使劇本里出現(xiàn)了組名、變量引用錯(cuò)誤也不會(huì)跑到測(cè)試環(huán)境去而--limit讓精確操作變得安全可控。相反如果你只用了--limit卻不指定-i那么主機(jī)池默認(rèn)是/etc/ansible/hosts里面混著哪些機(jī)器就有風(fēng)險(xiǎn)了。這也是我一直堅(jiān)持“命令里必須帶顯式-i”的原因。3.2 清單文件里的格式要點(diǎn)-i指向的清單文件不是隨便填幾個(gè)IP就行它有一套約定俗成的結(jié)構(gòu)。以INI格式為例[web] web01 ansible_host192.168.1.21 ansible_userroot web02 ansible_host192.168.1.22 [db] db01 ansible_host192.168.1.31 [prod:children] web db這個(gè)文件里包含了幾個(gè)信息組web、組db、父組prod以及每臺(tái)主機(jī)對(duì)應(yīng)的連接參數(shù)。-i指向它之后你既可以用-i hosts.ini web來(lái)操作web組也可以用-i hosts.ini prod來(lái)同時(shí)操作兩個(gè)子組。還有一個(gè)重要的隱式組all和ungrouped。-i hosts.ini all匹配清單里的所有主機(jī)-i hosts.ini ungrouped匹配沒(méi)有放進(jìn)任何組的主機(jī)。這決定了你如果不小心把某臺(tái)機(jī)器漏寫在組里它會(huì)不會(huì)被all誤執(zhí)行到。我強(qiáng)烈建議在靜態(tài)清單里給每個(gè)環(huán)境都建立一個(gè)明確的[env:children]父組例如[prod:children]、[test:children]。這樣可以極大地方便腳本里引用也讓-i inventory/prod/ web這樣的命令更直觀。3.3 兩種格式INI、YAML怎么選從Ansible 2.4開(kāi)始YAML格式的inventory也可以直接用但很多人沒(méi)意識(shí)到兩種格式在表達(dá)復(fù)雜變量時(shí)的差異。INI格式寫簡(jiǎn)單分組很方便一個(gè)文件幾行就完事YAML格式則結(jié)構(gòu)更清晰適合需要大量主機(jī)變量的場(chǎng)景。YAML版清單長(zhǎng)這樣all: children: web: hosts: web01: ansible_host: 192.168.1.21 web02: ansible_host: 192.168.1.22 db: hosts: db01: ansible_host: 192.168.1.31實(shí)際用下來(lái)我的判斷標(biāo)準(zhǔn)是如果只是幾十臺(tái)機(jī)器組結(jié)構(gòu)不復(fù)雜INI綽綽有余如果清單要交給多個(gè)團(tuán)隊(duì)維護(hù)、變量復(fù)用頻繁YAML更合適。-i對(duì)兩者都支持得很好所以選型不必糾結(jié)重點(diǎn)是整個(gè)團(tuán)隊(duì)保持一致。4. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄4.1 高頻報(bào)錯(cuò)速查表我整理了這幾年被問(wèn)到最多、也最典型的五個(gè)問(wèn)題全部跟-i和inventory直接相關(guān)。問(wèn)題現(xiàn)象常見(jiàn)原因解決方案No hosts matched指定的組名不存在或清單中實(shí)際沒(méi)有該組先執(zhí)行ansible -i 清單文件 --list-hosts 組名確認(rèn)匹配情況ERROR! Specified inventory hosts dont exist-i路徑寫錯(cuò)或逗號(hào)列表忘加末尾逗號(hào)用ls確認(rèn)文件存在列表形式檢查末尾逗號(hào)解析清單時(shí)報(bào)語(yǔ)法錯(cuò)誤INI里縮進(jìn)混用、ansible_host值寫了中文或異常字符逐個(gè)檢查主機(jī)行刪除隱藏字符用ansible-inventory -i 文件 --list驗(yàn)證動(dòng)態(tài)清單腳本無(wú)輸出或JSON解析失敗腳本沒(méi)有執(zhí)行權(quán)限或腳本里有額外print手動(dòng)運(yùn)行腳本清理非JSON輸出給腳本加x權(quán)限執(zhí)行到了不該執(zhí)行的主機(jī)-i指向了錯(cuò)誤的環(huán)境目錄命令中固定使用環(huán)境全路徑使用前先跑--list-hosts確認(rèn)范圍這里最推薦的一個(gè)習(xí)慣是在跑任何playbook之前先跑一次--list-hosts看匹配范圍。這個(gè)命令不會(huì)改變系統(tǒng)狀態(tài)純粹打印這次-i加主機(jī)模式匹配到了哪些機(jī)器。你只需在真正的playbook命令前面加一個(gè)--list-hosts參數(shù)就能避免絕大部分“誤操作到不該碰的機(jī)器”的慘劇。4.2 權(quán)限與連接問(wèn)題使用-i指定清單后每個(gè)主機(jī)的連接方式取決于清單里的連接參數(shù)。最常見(jiàn)的問(wèn)題是ansible_user沒(méi)有正確指定或者SSH端口不是默認(rèn)22。排查思路很直接ansible -i hosts.ini web -m ping -vvv-vvv參數(shù)會(huì)輸出完整的SSH連接日志你會(huì)看到它走了哪個(gè)ansible_user、嘗試了哪個(gè)端口、最終停在什么錯(cuò)誤上。我見(jiàn)過(guò)很多“ping不通”的假象最后都是因?yàn)榍鍐卫锏腶nsible_host寫成了內(nèi)網(wǎng)保留地址而執(zhí)行機(jī)根本訪問(wèn)不到。所以排查連通性時(shí)先確認(rèn)執(zhí)行機(jī)到目標(biāo)機(jī)的網(wǎng)絡(luò)路徑再確認(rèn)清單參數(shù)最后才考慮公鑰和密碼認(rèn)證的問(wèn)題。另外一個(gè)經(jīng)驗(yàn)是清單里連接參數(shù)如果和命令行的-u、--private-key產(chǎn)生沖突Ansible會(huì)優(yōu)先采用命令行指定的值。這就意味著如果你在command里硬性指定了-u root那么清單里的ansible_user就會(huì)失效。理解這個(gè)優(yōu)先級(jí)有助于你快速定位“為什么我改清單里的用戶沒(méi)起作用”這類困惑。4.3 常見(jiàn)誤操作和防呆建議我見(jiàn)過(guò)最意外的事故是把生產(chǎn)環(huán)境的清單文件內(nèi)容改動(dòng)后沒(méi)有驗(yàn)證就直接跑批量部署。結(jié)果新加的某臺(tái)機(jī)器由于沒(méi)有初始化直接被劇本執(zhí)行了一堆依賴安裝最后系統(tǒng)狀態(tài)變得不可預(yù)期。這類問(wèn)題不是-i本身造成的而是沒(méi)有在清單變更后做嚴(yán)格驗(yàn)證。所以我的防呆流程是這樣修改清單后總是先執(zhí)行ansible-inventory -i inventory/prod/ --list確認(rèn)數(shù)據(jù)結(jié)構(gòu)完整。再用ansible -i inventory/prod/ all --list-hosts確認(rèn)目標(biāo)范圍。最后才跑真正的playbook并且盡量加上--check參數(shù)做干跑演練。這套流程不用花很多時(shí)間但能幫你躲掉大多數(shù)因?yàn)榍鍐巫兏鼘?dǎo)致的批量事故。自動(dòng)化運(yùn)維半年來(lái)我最大的感悟就是慢就是快越小心的前置檢查才越能避免大故障的后置恢復(fù)。5. 實(shí)測(cè)驗(yàn)證從命令行到Playbook的組合玩法5.1 用-i組合模塊命令做快速巡檢單純列參數(shù)講概念遠(yuǎn)不如實(shí)際演示來(lái)得直觀。下面我給出幾個(gè)我在日常運(yùn)維里真實(shí)會(huì)用到的命令每個(gè)都緊密結(jié)合-i。第一批量檢查多臺(tái)機(jī)器的時(shí)間同步ansible -i inventory/prod/ prod -m shell -a date timedatectl這個(gè)命令會(huì)遍歷prod組內(nèi)所有主機(jī)先輸出日期時(shí)間再查看時(shí)區(qū)和NTP同步狀態(tài)。加上-i顯式指定環(huán)境之后我可以放心地把同一腳本丟給測(cè)試環(huán)境只需要換路徑ansible -i inventory/test/ test -m shell -a date timedatectl第二批量收集機(jī)器信息并生成報(bào)告ansible -i inventory/prod/ all -m setup 2/dev/null | grep ansible_hostnamesetup模塊會(huì)自動(dòng)收集主機(jī)的facts包括主機(jī)名、IP、系統(tǒng)版本、內(nèi)存、磁盤等大量信息。用-i指定環(huán)境后你得到的就是該環(huán)境下的機(jī)器“體檢報(bào)告”。我在做資產(chǎn)盤點(diǎn)時(shí)就用這個(gè)方法比登到每臺(tái)機(jī)器上去看快好幾倍。第三批量分發(fā)配置文件ansible -i inventory/prod/ web -m copy -a src/opt/config/nginx.conf dest/etc/nginx/nginx.conf backupyes這條命令配合-i的目錄化清單直接在指定環(huán)境的web組里分發(fā)配置。backupyes參數(shù)還會(huì)自動(dòng)備份遠(yuǎn)端舊配置降低出錯(cuò)的回滾成本。5.2 Playbook中使用-i的正確姿勢(shì)當(dāng)從單條ansible命令切換到ansible-playbook時(shí)-i的使用邏輯不變但要注意變量引用的差異。Playbook內(nèi)部通過(guò)hosts:關(guān)鍵字選擇執(zhí)行組這個(gè)組名必須能由-i指定的清單解析出來(lái)。一個(gè)典型的用法ansible-playbook -i inventory/prod/ deploy.yml --tags deploy --limit web這條命令的含義是從生產(chǎn)環(huán)境清單中僅選擇web組下的主機(jī)且只執(zhí)行playbook中帶有deploy標(biāo)簽的任務(wù)。因?yàn)?i指向生產(chǎn)才會(huì)有安全的環(huán)境隔離--limit再做一層細(xì)粒度控制兩條配合起來(lái)既靈活又安全。我還在playbook里大量使用group_vars按環(huán)境區(qū)分配置。目錄化的-i讓Ansible自動(dòng)加載對(duì)應(yīng)環(huán)境的group_vars這樣同一份playbook在不同環(huán)境跑拿到的配置參數(shù)天然不同。比如生產(chǎn)環(huán)境連的是生產(chǎn)數(shù)據(jù)庫(kù)地址測(cè)試環(huán)境連的是測(cè)試數(shù)據(jù)庫(kù)地址這些差異全部由inventory目錄結(jié)構(gòu)消化掉playbook本身保持純凈。5.3 動(dòng)態(tài)Inventory與Playbook的深度整合動(dòng)態(tài)inventory和playbook配合的坑在于動(dòng)態(tài)腳本輸出的組名必須和playbook里的hosts:字段完全一致否則就會(huì)報(bào)“no hosts matched”。所以我建議先把動(dòng)態(tài)腳本的輸出導(dǎo)出來(lái)看一眼python /opt/scripts/dynamic_inventory.py --list /tmp/inv.json jq .web /tmp/inv.json確認(rèn)web組存在后再執(zhí)行ansible-playbook -i /opt/scripts/dynamic_inventory.py deploy.yml另外一個(gè)細(xì)節(jié)是動(dòng)態(tài)inventory里經(jīng)常需要附帶額外變量比如ansible_user、ansible_ssh_private_key_file等。這些變量可以寫在腳本輸出JSON的_meta字段里Ansible會(huì)讀取_meta.hostvars來(lái)獲取主機(jī)級(jí)變量。很多初學(xué)動(dòng)態(tài)inventory的人只返回了主機(jī)列表忘了返回_meta導(dǎo)致playbook跑起來(lái)后用錯(cuò)誤的SSH用戶連接。所以設(shè)計(jì)動(dòng)態(tài)清單腳本時(shí)既要輸出組成員關(guān)系也要輸出連接參數(shù)這樣整個(gè)鏈路才是閉環(huán)的。6. 擴(kuò)展玩法自定義Inventory插件體系初探6.1 為什么需要自定義插件多數(shù)團(tuán)隊(duì)的清單需求無(wú)非是靜態(tài)文件和動(dòng)態(tài)腳本兩種。但當(dāng)你需要從內(nèi)部CMDB、工單系統(tǒng)、甚至一個(gè)Excel表格里讀取主機(jī)信息時(shí)動(dòng)態(tài)腳本仍然不夠優(yōu)雅。Ansible從2.4開(kāi)始豐富了inventory plugin機(jī)制允許通過(guò)配置ansible.cfg以插件方式加載自定義的清單來(lái)源。用插件最大的好處是可以和Ansible自己的配置體系無(wú)縫集成比如插件的配置可以直接寫在ansible.cfg里支持緩存、支持組合過(guò)濾條件。而動(dòng)態(tài)腳本則更加“黑盒”一切行為由腳本自己解釋閱讀和排錯(cuò)都更費(fèi)力。6.2 一個(gè)簡(jiǎn)單的自定義清單插件模板如果你的環(huán)境里已經(jīng)有了一個(gè)內(nèi)部API可以返回主機(jī)列表那么寫一個(gè)簡(jiǎn)單插件并不復(fù)雜。下面我提供一個(gè)最小可用的框架思路具體實(shí)現(xiàn)還是要結(jié)合你的內(nèi)部API格式來(lái)調(diào)整。插件主體通常是一個(gè)繼承BaseInventoryPlugin的Python類核心實(shí)現(xiàn)parse方法。parse方法里從配置讀取API地址請(qǐng)求數(shù)據(jù)解析JSON然后調(diào)用self.inventory.add_group、self.inventory.add_host、self.inventory.set_variable等API把主機(jī)信息填充進(jìn)Ansible的inventory對(duì)象。關(guān)鍵點(diǎn)有三個(gè)在ansible.cfg里配置enable_plugins確保自定義插件被加載。插件文件名要放在指定的inventory_plugins目錄里Ansible啟動(dòng)時(shí)會(huì)到這些目錄下尋找插件。-i后面的值要配置成插件對(duì)應(yīng)的清單源描述例如-i my_cmdb.yml其中my_cmdb.yml里寫API地址和認(rèn)證信息。寫自定義插件的門檻確實(shí)比動(dòng)態(tài)腳本高一點(diǎn)但它的回報(bào)也是很明顯的插件可以復(fù)用Ansible的緩存機(jī)制、變量合并機(jī)制調(diào)試起來(lái)也比腳本清晰得多。如果你所在團(tuán)隊(duì)的運(yùn)維規(guī)模常年幾百臺(tái)機(jī)器、且有成熟的資產(chǎn)系統(tǒng)這條路線值得投入時(shí)間去搭建。6.3 我的選型建議與判斷標(biāo)準(zhǔn)聊了這么多最后給一個(gè)務(wù)實(shí)的選型建議。如果機(jī)器數(shù)量在幾十臺(tái)級(jí)別且變化不頻繁直接用靜態(tài)文件加目錄化組織就夠了完全沒(méi)有必要引入動(dòng)態(tài)機(jī)制如果機(jī)器上了云、經(jīng)常擴(kuò)縮容那么動(dòng)態(tài)inventory腳本是性價(jià)比最高的選擇如果你已經(jīng)有了一套CMDB或者資產(chǎn)系統(tǒng)希望所有工具的清單來(lái)源統(tǒng)一再考慮自定義inventory插件。做任何方案之前先想清楚-i指向的清單到底承擔(dān)了什么角色它只是給Ansible提供連接信息還是同時(shí)承載了環(huán)境隔離、配置分層、權(quán)限邊界想得越清楚方案就越不容易被推翻。我本人目前的架構(gòu)是核心環(huán)境用靜態(tài)目錄化inventory臨時(shí)用逗號(hào)列表云節(jié)點(diǎn)通過(guò)動(dòng)態(tài)腳本接入內(nèi)部資產(chǎn)系統(tǒng)正逐步用自定義插件替換原先的腳本。這套組合并不復(fù)雜關(guān)鍵是每一步的選擇都有明確理由支撐。7. 踩坑經(jīng)驗(yàn)與工作習(xí)慣建議7.1 踩過(guò)的坑從環(huán)境混淆到變量泄漏先說(shuō)說(shuō)我真實(shí)經(jīng)歷過(guò)的幾次踩坑相信很多人也有共鳴。第一次是環(huán)境混淆事故。當(dāng)時(shí)項(xiàng)目剛開(kāi)始規(guī)模不大我把生產(chǎn)、測(cè)試的主機(jī)放在同一個(gè)inventory文件里靠著--limit來(lái)區(qū)分。結(jié)果某次升級(jí)時(shí)--limit寫錯(cuò)了一個(gè)前綴把一臺(tái)測(cè)試機(jī)器當(dāng)成生產(chǎn)機(jī)器執(zhí)行了數(shù)據(jù)遷移腳本。雖然沒(méi)有造成真正的數(shù)據(jù)丟失但那次教訓(xùn)讓我徹底放棄了“單文件加限制”的方案轉(zhuǎn)而用目錄強(qiáng)制隔離環(huán)境。第二次是變量泄漏。由于多個(gè)環(huán)境的group_vars放在同一個(gè)目錄層級(jí)下某個(gè)變量文件里不小心覆蓋了公共變量導(dǎo)致生產(chǎn)環(huán)境的數(shù)據(jù)庫(kù)連接地址被測(cè)試環(huán)境的值給串了。還好當(dāng)時(shí)有監(jiān)控及時(shí)發(fā)現(xiàn)沒(méi)有引發(fā)嚴(yán)重故障。從那以后每個(gè)環(huán)境的group_vars一律放在獨(dú)立的目錄里絕不允許相互引用。第三次是關(guān)于動(dòng)態(tài)inventory的-i權(quán)限問(wèn)題。腳本一開(kāi)始放在某個(gè)普通用戶目錄下Ansible執(zhí)行時(shí)報(bào)“Permission denied”。排查了很久才發(fā)現(xiàn)是腳本沒(méi)有加執(zhí)行權(quán)限-i解析時(shí)把它當(dāng)作不可執(zhí)行文件處理了。自那以后我每次新建動(dòng)態(tài)清單腳本都會(huì)順便執(zhí)行chmod x并寫進(jìn)團(tuán)隊(duì)規(guī)約里。7.2 推薦的工作習(xí)慣讓-i成為命令的標(biāo)準(zhǔn)配置從這些坑里我總結(jié)了幾條必須堅(jiān)持的工作習(xí)慣。第一所有ansible命令都顯式帶-i。不管這條命令是針對(duì)開(kāi)發(fā)環(huán)境還是生產(chǎn)環(huán)境把-i寫出來(lái)相當(dāng)于給執(zhí)行目標(biāo)上了保險(xiǎn)。你可以把它理解成寫SQL時(shí)必須帶上WHERE條件如果忘了寫可能就會(huì)更新到全表數(shù)據(jù)。第二環(huán)境目錄命名要統(tǒng)一且不可隨意改動(dòng)。inventory/dev/、inventory/test/、inventory/prod/一旦定了就不要為了節(jié)省幾個(gè)字符去改成d、t、p。全稱命名雖然長(zhǎng)但在腳本審查時(shí)一眼就能看出目標(biāo)環(huán)境這對(duì)多人協(xié)作極其重要。第三在playbook的首個(gè)任務(wù)里輸出當(dāng)前執(zhí)行環(huán)境。我經(jīng)常在playbook開(kāi)頭加一個(gè)debug任務(wù)- name: Print current environment debug: msg: Executing on {{ inventory_dir }}這樣每次跑playbook終端都會(huì)先打印出inventory目錄路徑確認(rèn)當(dāng)前環(huán)境。如果這里顯示的路徑和自己想的不一致馬上停止執(zhí)行。第四利用ansible-inventory做清單可視化。Ansible提供了一個(gè)專門調(diào)試inventory的子命令ansible-inventory用法是ansible-inventory -i inventory/prod/ --list ansible-inventory -i inventory/prod/ --graph--graph會(huì)以樹(shù)狀結(jié)構(gòu)顯示組與主機(jī)的關(guān)系非常直觀。我在寫復(fù)雜清單時(shí)都會(huì)先跑一下--graph檢查有沒(méi)有組嵌套錯(cuò)誤。7.3 腳本化-i參數(shù)管理的進(jìn)階建議如果團(tuán)隊(duì)規(guī)模稍大我建議把-i相關(guān)的路徑定義抽成公共變量或者腳本參數(shù)避免每個(gè)腳本里硬編碼一長(zhǎng)串路徑。比如可以寫一個(gè)運(yùn)維入口腳本#!/bin/bash ENV$1 shift ansible-playbook -i inventory/${ENV}/ $這樣在執(zhí)行時(shí)./ops.sh prod deploy.yml --tags restart好處是環(huán)境名稱被約束在dev/test/prod等枚舉值里降低了誤寫路徑的概率也讓新同事更容易上手。雖然這只是一個(gè)小包裝但在團(tuán)隊(duì)協(xié)作時(shí)的收益相當(dāng)明顯。如果你使用GitLab CI或Jenkins還可以把-i環(huán)境參數(shù)定義成CI變量讓不同流水線天然帶上環(huán)境標(biāo)簽從流程上杜絕“手動(dòng)指定錯(cuò)誤環(huán)境”的可能。這也是我把-i從“命令行參數(shù)”提升到“自動(dòng)化體系基礎(chǔ)設(shè)計(jì)”的原因它不是一個(gè)小細(xì)節(jié)而是環(huán)境邊界的錨點(diǎn)。所有上游工單系統(tǒng)、CMDB、發(fā)布平臺(tái)的對(duì)接最終都會(huì)落到“當(dāng)前這個(gè)任務(wù)應(yīng)該用哪份inventory”這個(gè)問(wèn)題上。我現(xiàn)在每寫一個(gè)自動(dòng)化腳本第一件事就是確定它的inventory來(lái)源每設(shè)計(jì)一個(gè)發(fā)布流程第一件事也是確定它作用于哪個(gè)清單環(huán)境。-i不只是參數(shù)而是一種把自動(dòng)化行為固定在可控邊界內(nèi)的紀(jì)律。有了這個(gè)紀(jì)律Ansible的強(qiáng)大能力才能真正安全地發(fā)揮出來(lái)。