Ansible 基礎知識,沒有它你的劇本就會變成一塊黏糊糊的義大利麵

我對其他人的 Ansible 程式碼做了很多審查,我自己也寫了很多。 在分析錯誤(包括其他人的和我自己的)以及多次採訪的過程中,我意識到 Ansible 用戶所犯的主要錯誤 - 他們在沒有掌握基本知識的情況下就陷入了複雜的事物。

為了糾正這種普遍的不公正,我決定為那些已經了解 Ansible 的人寫一篇介紹。 我警告你,這不是男人的重述,這是一本長書,有很多字母,沒有圖片。

讀者的預期水平是已經寫了幾千行 yamla,有些東西已經在生產中,但「不知怎的,一切都是歪的」。

標題

Ansible 使用者犯的主要錯誤是不知道某些東西叫什麼。 如果您不知道名稱,就無法理解文件的內容。 一個活生生的例子:在一次訪談中,一個似乎說自己用 Ansible 寫了很多東西的人無法回答「劇本由哪些元素組成?」的問題。 當我提出「答案是預期的,劇本由遊戲組成」時,隨之而來的是「我們不使用它」的嚴厲評論。 人們寫 Ansible 是為了錢,而不是為了玩。 他們實際上使用它,但不知道它是什麼。

讓我們從簡單的事情開始:它叫什麼。 也許你知道這一點,也可能你不知道,因為你閱讀文件時沒有註意。

ansible-playbook 執行劇本。 playbook 是一個副檔名為 yml/yaml 的文件,其中包含以下內容:

---
- hosts: group1
  roles:
    - role1

- hosts: group2,group3
  tasks:
    - debug:

我們已經意識到整個文件是一個劇本。 我們可以顯示角色在哪裡以及任務在哪裡。 但玩在哪裡呢? 戲劇和角色或劇本之間有什麼區別?

這一切都在文件中。 他們很懷念它。 初學者-因為內容太多,你不可能一下子記住所有內容。 經驗豐富——因為“瑣事”。 如果您有經驗,請至少每六個月重新閱讀一次這些頁面,您的程式碼將成為一流的。

所以,請記住:Playbook 是一個包含 play 和 import_playbook.
這是一場戲:

- hosts: group1
  roles:
    - role1

這也是另一齣戲:

- hosts: group2,group3
  tasks:
    - debug:

什麼是玩? 為什麼是她?

遊戲是劇本的關鍵元素,因為遊戲且僅遊戲將角色和/或任務清單與必須在其上執行的主機清單相關聯。 在文件的深處,您可以找到提到的 delegate_to、本地查找插件、特定於網路 CLI 的設定、跳轉主機等。 它們允許您稍微更改執行任務的位置。 但是,忘記它吧。 這些聰明的選擇中的每一個都有非常具體的用途,而且它們絕對不是通用的。 我們正在談論每個人都應該了解和使用的基本知識。

如果你想在“某處”表演“某事”,你就寫劇本。 不是一個角色。 不是有模組和委託的角色。 你拿去寫劇本。 其中,在主機欄位中列出要執行的位置,在角色/任務中列出要執行的內容。

很簡單,對吧? 不然怎麼可能?

當人們不透過遊戲而渴望做到這一點時,典型的時刻之一就是「設定一切的角色」。 我想要一個既配置第一類型伺服器又配置第二類型伺服器的角色。

一個典型的例子是監控。 我想要一個可以設定監控的監控角色。 監控角色指派給監控主機(根據角色)。 但事實證明,為了進行監控,我們需要將包包傳送到我們正在監控的主機上。 為什麼不使用委託? 您還需要設定 iptables。 代表? 您還需要編寫/修正 DBMS 的配置以啟用監控。 代表! 如果缺乏創造力,那麼你可以派一個代表團 include_role 在嵌套循環中,在群組列表上使用棘手的過濾器,並在內部 include_role 你可以做更多 delegate_to 再次。 然後我們就走了...

一個美好的願望 - 擁有一個「包辦一切」的單一監控角色 - 會導致我們陷入徹底的地獄,而大多數情況下只有一種出路:從頭開始重寫一切。

這裡哪裡發生了錯誤? 當您發現要在主機 X 上執行任務“x”時,您必須轉到主機 Y 並在那裡執行“y”,您必須做一個簡單的練習:去編寫 play,在主機 Y 上執行 y。 不要在“x”上添加任何東西,而是從頭開始編寫。 即使使用硬編碼變數。

看起來上面段落中的所有內容都說得正確。 但這不是你的情況! 因為您想要編寫 DRY 且類似函式庫的可重複使用程式碼,並且您需要尋找一種方法來實作它。

這是另一個嚴重錯誤潛伏的地方。 這個錯誤使許多項目從寫得還算可以的(可能會更好,但一切正常並且很容易完成)變成了連作者都無法理解的完全恐怖。 它有效,但上帝禁止你改變任何事。

錯誤是:角色是函式庫函數。 這個類比毀掉瞭如此多的良好開端,讓人看著很傷心。 該角色不是函式庫函數。 她無法進行計算,也無法做出遊戲層面的決定。 提醒我遊戲會做什麼決定?

謝謝,你是對的。 Play 做出關於在哪些主機上執行哪些任務和角色的決定(更準確地說,它包含資訊)。

如果您將這一決定委託給某個角色,即使進行了計算,您也注定會陷入悲慘的境地(以及試圖解析您的程式碼的人)。 該角色並不決定其在何處執行。 這個決定是透過遊戲做出的。 這個角色照著它所告訴的去做,在它所告訴的地方做。

為什麼在 Ansible 中編程很危險以及為什麼 COBOL 比 Ansible 更好,我們將在有關變數和 jinja 的章節中討論。 現在,讓我們說一件事——你的每一次計算都會留下全局變數變化的不可磨滅的痕跡,而你對此無能為力。 當兩條「痕跡」相交時,一切都消失了。

謹記:該角色一定會影響控制流。 吃 delegate_to 並且它有合理的用途。 吃 meta: end host/play。 但! 還記得我們教的基礎知識嗎? 忘記了 delegate_to。 我們正在談論最簡單、最美麗的 Ansible 程式碼。 易於閱讀、易於編寫、易於調試、易於測試且易於完成。 所以,再一次:

play 和 only play 決定在哪些主機上執行什麼。

在本節中,我們討論了遊戲和角色之間的對立。 現在我們來談談任務與角色的關係。

任務和角色

考慮玩:

- hosts: somegroup
  pre_tasks:
    - some_tasks1:
  roles:
     - role1
     - role2
  post_tasks:
     - some_task2:
     - some_task3:

假設您需要執行 foo. 它看起來像 foo: name=foobar state=present。 我應該在哪裡寫這篇? 在預? 郵政? 創建角色?

……任務去哪裡了?

我們再次從基礎開始——播放設備。 如果你在這個問題上猶豫不決,你就不能把遊戲當作其他一切的基礎,你的結果就會「搖搖欲墜」。

播放裝置:hosts 指令、播放本身和 pre_tasks、任務、角色、post_tasks 部分的設定。 剩下的遊戲參數現在對我們來說並不重要。

其任務和角色部分的順序: pre_tasks, roles, tasks, post_tasks。 由於從語義上講,執行順序介於 tasks и roles 不清楚,那麼最佳實踐說我們正在添加一個部分 tasks,只有當不是 roles... 如果有 roles,然後所有附加任務都放置在部分中 pre_tasks/post_tasks.

剩下的就是一切在語意上都是清楚的:首先 pre_tasks, 然後 roles, 然後 post_tasks.

但我們仍然沒有回答這個問題:模組呼叫在哪裡? foo 寫? 我們需要為每個模組編寫一個完整的角色嗎? 還是所有事情都扮演厚重的角色會更好? 如果不是角色,那我該在哪裡寫──前置還是後置?

如果這些問題沒有合理的答案,那麼這就是缺乏直覺的表現,也就是說,同樣是「基礎不穩固」。 讓我們弄清楚一下。 首先,一個安全問題:如果遊戲有 pre_tasks и post_tasks (並且沒有任務或角色),那麼如果我執行第一個任務,可能會出現問題 post_tasks 我會把它移到最後 pre_tasks?

當然,問題的措辭暗示它會崩潰。 但到底是什麼?

... 處理程序。 閱讀基礎知識揭示了一個重要的事實:所有處理程序都會在每個部分之後自動刷新。 那些。 所有任務來自 pre_tasks,然後是所有收到通知的處理程序。 然後執行所有角色以及角色中通知的所有處理程序。 後 post_tasks 和他們的處理者。

因此,如果您將任務從 post_tasks в pre_tasks,那麼您可能會在執行處理程序之前執行它。 例如,如果在 pre_tasks Web 伺服器已安裝並配置,且 post_tasks 有東西發送給它,然後將此任務轉移到該部分 pre_tasks 將導致這樣一個事實:在「發送」時,伺服器尚未運行,一切都會中斷。

現在我們再想一想,為什麼我們需要 pre_tasks и post_tasks? 例如,為了在履行角色之前完成所需的一切(包括處理程序)。 A post_tasks 將允許我們處理執行角色(包括處理程序)的結果。

精明的 Ansible 專家會告訴我們它是什麼。 meta: flush_handlers,但是如果我們可以依賴播放中各部分的執行順序,為什麼我們需要flush_handlers呢? 此外,使用 meta:flush_handlers 可能會給我們帶來意想不到的結果,即重複的處理程序,在使用時給我們帶來奇怪的警告 when у block ETC。 您對 ansible 的了解越多,您可以為「棘手」解決方案命名的細微差別就越多。 一個簡單的解決方案 - 使用前/角色/後之間的自然劃分 - 不會造成細微差別。

然後,回到我們的「foo」。 我應該把它放在哪裡? 在前、後或角色中? 顯然,這取決於我們是否需要 foo 處理程序的結果。 如果它們不存在,則 foo 不需要放置在 pre 或 post 中 - 這些部分具有特殊含義 - 在程式碼主體之前和之後執行任務。

現在,「角色或任務」問題的答案歸結為已經在起作用的內容 - 如果那裡有任務,那麼您需要將它們添加到任務中。 如果有角色,您需要建立一個角色(即使是從一項任務)。 讓我提醒您,任務和角色不能同時使用。

了解 Ansible 的基礎知識可以為看似品味的問題提供合理的答案。

任務和角色(第二部分)

現在讓我們來討論一下剛開始寫劇本時的情況。 您需要製作 foo、bar 和 baz。 這三個任務、一個角色還是三個角色? 總結一下這個問題:你該從什麼時候開始寫角色? 當你可以寫任務時,寫角色還有什麼意義?...什麼是角色?

最大的錯誤之一(我已經討論過這一點)是認為角色就像程式庫中的函數。 通用函數描述是什麼樣的? 它接受輸入參數,與副作用交互,產生副作用,並傳回一個值。

現在,注意。 從這個角色可以做什麼? 隨時歡迎您呼叫副作用,這就是整個 Ansible 的本質——創建副作用。 有副作用嗎? 初級。 但是“傳遞一個值並返回它”——這就是它不起作用的地方。 首先,您不能將值傳遞給角色。 您可以在角色的 vars 部分中設定一個具有生命週期大小的全域變數。 您可以在角色內部設定一個具有生命週期的全域變數。 或甚至是劇本的生命週期(set_fact/register)。 但你不能有「局部變數」。 你不能“獲取一個值”並“返回它”。

主要的事情是這樣的:你不能在 Ansible 中寫一些東西而不引起副作用。 更改全域變數始終是函數的副作用。 例如,在 Rust 中,更改全域變數是 unsafe。 在 Ansible 中,它是影響角色值的唯一方法。 注意使用的字詞:不是“將值傳遞給角色”,而是“更改角色使用的值”。 角色之間不存在隔離。 任務和角色之間不存在隔離。

合計: 角色不是函數.

這個角色有什麼好處呢? 首先,角色有預設值(/default/main.yaml),其次,角色有額外的目錄用於儲存檔案。

預設值有什麼好處? 因為在馬斯洛金字塔中,Ansible 相當扭曲的變數優先權表,角色預設值是最低優先權的(減去 Ansible 命令列參數)。 這意味著,如果您需要提供預設值並且不擔心它們會覆蓋庫存或群組變數中的值,那麼角色預設值是唯一適合您的地方。 (我撒了一點謊——還有更多 |d(your_default_here),但如果我們談論固定位置,則只有角色預設值)。

角色還有哪些精彩之處? 因為他們有自己的目錄。 這些是變數的目錄,包括常數(即為角色計算)和動態(有模式或反模式 - include_vars 與...一起 {{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml.)。 這些是目錄 files/, templates/。 此外,它還允許您擁有自己的模組和插件(library/)。 但是,與劇本中的任務(也可以擁有所有這些)相比,這裡唯一的好處是文件不會轉儲到一堆,而是轉儲到幾個單獨的堆中。

還有一個細節:您可以嘗試創建可重複使用的角色(透過 Galaxy)。 隨著集合的出現,角色分配幾乎被遺忘了。

因此,角色有兩個重要的功能:它們有預設值(一個獨特的功能),並且允許您建立程式碼。

回到最初的問題:什麼時候做任務,什麼時候要做角色? 劇本中的任務最常用作角色之前/之後的“粘合劑”,或者作為獨立的構建元素(那麼代碼中不應該有角色)。 一堆正常的任務與角色混合在一起是毫不含糊的馬虎行為。 您應該遵循特定的風格 - 任務或角色。 角色提供了實體和預設值的分離,任務可讓您更快地閱讀程式碼。 通常,更多「固定」(重要且複雜)的程式碼會放入角色中,輔助腳本以任務風格編寫。

可以將 import_role 作為一項任務來執行,但是如果您編寫此內容,請準備好向您自己的美感解釋為什麼要執行此操作。

精明的讀者可能會說,角色可以導入角色,角色可以透過 Galaxy.yml 擁有依賴關係,還有一個可怕可怕的 include_role — 我提醒您,我們正在提升基本 Ansible 的技能,而不是花樣體操的技能。

處理程序和任務

讓我們討論另一個明顯的事情:處理程序。 知道如何正確使用它們幾乎是一門藝術。 處理程序和拖曳之間有什麼區別?

由於我們記住了基礎知識,因此這裡有一個範例:

- hosts: group1
  tasks:
    - foo:
      notify: handler1
  handlers:
     - name: handler1
       bar:

角色的處理程序位於 rolename/handlers/main.yaml 中。 處理程序在所有遊戲參與者之間進行翻查:pre/post_tasks 可以拉取角色處理程序,並且角色可以從遊戲中拉取處理程序。 然而,對處理程序的「跨角色」呼叫比重複一個簡單的處理程序更麻煩。 (最佳實務的另一個要素是盡量不要重複處理程序名稱)。

主要區別在於任務始終執行(冪等)(加/減標籤和 when),以及處理程序 - 按狀態變更(僅當已變更時才觸發通知)。 這是什麼意思? 例如,當您重新啟動時,如果沒有更改,則不會有處理程序。 為什麼在生成任務沒有變化的情況下我們需要執行handler? 例如,因為某些內容發生了損壞並發生了變化,但執行並未到達處理程序。 例如,因為網路暫時關閉。 配置已更改,服務尚未重新啟動。 下次啟動時,配置不再更改,服務仍保留舊版本的配置。

配置的情況無法解決(更準確地說,您可以使用文件標誌等為自己發明一個特殊的重啟協議,但這不再是任何形式的“基本ansible”)。 但還有另一個常見的故事:我們安裝了該應用程序,並記錄了它 .service-file,現在我們想要它 daemon_reload и state=started。 而處理這個問題的自然地點似乎是處理程序。 但是,如果您將其設定為任務清單或角色末尾的任務而不是處理程序,則每次都會以冪等方式執行。 即使劇本中途破裂了。 這根本不能解決重啟問題(你不能用restarted屬性做任務,因為冪等性丟失了),但是state=started絕對值得做,playbooks的整體穩定性增加了,因為連線數和動態減少。

處理程序的另一個積極特性是它不會阻塞輸出。 沒有任何變化 - 輸出中沒有額外的跳過或確定 - 更易於閱讀。 這也是一個負面屬性 - 如果您在第一次運行時在線性執行任務中發現拼寫錯誤,那麼處理程序將僅在更改時執行,即在某些情況下 - 很少。 例如,五年後,這是我人生中的第一次。 當然,如果名稱出現拼字錯誤,一切都會崩潰。 如果您不第二次運行它們,則不會發生任何變化。

另外,我們需要談談變數的可用性。 例如,如果您用循環通知任務,變數中會包含什麼? 您可以透過分析進行猜測,但這並不總是微不足道的,特別是當變數來自不同的地方時。

……因此,處理程序的用處遠不如看上去那麼有用,而且問題也比看起來多得多。 如果你可以在沒有處理程序的情況下寫出漂亮的東西(沒有多餘的裝飾),那麼最好不要使用它們。 如果效果不理想,最好還是和他們在一起。

腐蝕性的讀者正確地指出我們還沒有討論過 listen一個處理程序可以呼叫另一個處理程序的通知,一個處理程序可以包含import_tasks(可以使用with_items 執行include_role),Ansible 中的處理程序系統是圖靈完備的,來自include_role 的處理程序以一種奇怪的方式與遊戲中的處理程序相交,等等.d. - 這一切顯然不是「基礎」)。

儘管有一個特定的 WTF 實際上是您需要牢記的功能。 如果你的任務是用 delegate_to 並且有notify,則執行對應的handler,無需 delegate_to, IE。 在分配播放的主機上。 (當然,儘管處理程序可能有 delegate_to 相同的)。

另外,我想談談可重用角色。 在集合出現之前,有一個想法是可以製作通用角色 ansible-galaxy install 然後就走了。 適用於所有情況下所有變體的所有作業系統。 所以,我的意見是:這不起作用。 任何具有品質的角色 include_vars支持 100500 個案例,注定會出現極端案例錯誤的深淵。 它們可以透過大量測試來覆蓋,但與任何測試一樣,要么你有輸入值和總函數的笛卡爾積,要么你有「覆蓋的個別場景」。 我的觀點是,如果角色是線性的(圈複雜度為 1),那就更好了。

更少的 if(顯式或聲明性 - 形式為 when 或形式 include_vars 透過一組變數),作用就越好。 有時你必須建立分支,但是,我再說一遍,分支越少越好。 所以這對銀河系來說似乎是一個很好的角色(它有效!) when 可能不如五項任務中「自己」的角色更可取。 當你開始寫一些東西的時候,銀河系的角色就變得更好了。 當事情變得更糟的時候,你會懷疑這是因為「與星系的作用」。 打開它,裡面有五個內含物、八個任務表和一疊 when'ov...我們需要解決這個問題。 不是 5 個任務,而是一個沒有什麼可打破的線性清單。

在以下部分

  • 關於清單、群組變數、host_group_vars 插件、hostvars 的一些資訊。 如何與義大利麵快刀斬亂麻。 範圍和優先權變量,Ansible 記憶體模型。 “那麼我們在哪裡存儲資料庫的用戶名呢?”
  • jinja: {{ jinja }} — nosql notype nosense 軟橡皮泥。 它無所不在,甚至在你意想不到的地方。 一點關於 !!unsafe 和美味的山藥。

來源: www.habr.com

添加評論