基於 Bitbucket Pipeline + Amazon S3 的自動化運維體系

1 前言介紹

隨着自動化運維水平的提高,一個基礎的運維人員維護成百上千臺節點已經不是太難的事情,當然,這需要依靠於穩定、高效的自動化運維體系。本篇文章即是闡述如何利用 bitbucket pipeline 結合 Amazon S3 存儲實現項目的自動構建、自動發佈以及異常報警等完整的自動化流程處理。不同於常見的自動化項目一般服務於局域網體系,有很大的併發限制與網絡帶寬限制,本篇介紹並實現的運維體系不存在網絡瓶頸,可以支撐廣域網運維自動化,並且支持高併發、升級速度快、架構穩定可擴展性強等優點。

2.1 應用概述

本架構中涉及到的主要應用有:
Bitbucket:一種基於 Git 做版本控制的代碼託管平臺,其他比較流行的平臺有 Github、GitLab、Coding。Bitbucket 支持 pipeline 功能,這也是我們自動化體系CI(持續集成)/CD(持續交付)的基礎。
Amzaon S3:AWS 官網介紹是:“提供了一個簡單 Web 服務接口,可用於隨時在 Web 上的任何位置存儲和檢索任何數量的數據”。簡而言之,就是 Amazon S3 提供一個網絡存儲平臺,我們可以利用其提供的 Web API 進行內容的存儲與訪問。
Docker Hub:目前 Docker 官方維護的一個公共 Docker 鏡像倉庫,而在我們的自動化體系中,也是基於基礎的 Docker 鏡像所做的二次開發。
Ansible:一個基於 SSH 安全認證的批量操作工具,相比較於 saltstack 其操作、部署以及維護比較簡單,但是連接速度較慢。
Slack:優秀的企業協作通訊平臺,我們可以利用其提供的接口進行自動化執行結果的消息通知與展示。

2.2倉庫結構與倉庫權限概述

我們將一個產品項目(ProjectX)根據邏輯功能不同拆分拆分成若干個獨立的模塊,每個模塊存儲在一個獨立的倉庫中。另外,我們根據倉庫的維護者不同將倉庫分爲源碼庫(source repository)、發佈庫(release repository)、整合庫(distribution repository)。比如我們的項目 ProjectX 對應的整合倉庫爲 ProjectX.dist ,該項目中有個功能模塊爲 ModuleX ,針對這個模塊需要有源碼倉庫(ModuleX.src)以及發佈倉庫(ModuleX.rel)。 其中源碼庫(後簡稱 SRC 倉庫)由研發人員維護,即提供某個功能模塊的源代碼、模塊功能介紹與使用說明等信息。開發人員對源碼庫可讀寫,運維人員可讀;發佈庫(後簡稱 REL 倉庫)由系統運維人員維護,提供源碼(假設源碼爲編譯型語言)編譯後的二進制文件安裝程序。運維人員對發佈庫可讀寫,開發人員無權限;整合庫(後簡稱 DIST 倉庫)則是將不同的倉庫模塊按照一定的邏輯順序組合起來,形成一個完整的產品項目。運維人員對整合庫可讀寫,開發人員無權限。產品項目的 Git 倉庫邏輯分層結構示意圖如下:

圖1 Git 倉庫邏輯分層結構示意圖
 

一個項目由一個 DIST 倉庫、N 個 SRC 倉庫、N+M 個 REL 倉庫組成。只有一個整合倉庫,這個很好理解,但是 SRC 與 REL 倉庫的關係如何理解呢?一般來說一個 SRC 倉庫會對應一個 REL 倉庫,即研發人員只負責提供源代碼,至於如何安裝到系統、安裝到系統的哪個目錄、以及如何制定安全備份策略、日誌回滾策略等問題則不需要關注,這些由系統運維人員將該程序的執行策略以程序或者腳本的形式存在對應的 REL 發佈庫中,因此一個 SRC 倉庫會有一個相應的 REL 倉庫與之對應。但是一個 REL 倉庫有可能是不需要 SRC 倉庫的,比如我們在系統中會引入某個開源程序(如 Nginx),爲了項目的穩定性,我們會把 Nginx 指定版本的 RPM 安裝包作爲一個文件存儲至 REL 倉庫中,這樣這個倉庫其實就不需要源碼庫即 SRC 倉庫,但是如果我們要對 Nginx 做二次研發,然後再編譯成二進制文件,則需要額外創建 SRC 倉庫用於存儲源代碼。

2.3 架構執行流程概述

當開發者將代碼推送至 Bitbucket 上的源碼倉庫之後,會觸發 SRC 倉庫的 pipeline ,調用指定的 Docker Image (一般需要我們做二次修改、發佈才能使用)完成源碼自動編譯、打包,我們定義打包文件名同二進制文件名加上指定的壓縮後綴(比如 ModuleX.src 中提供了一個二進制程序爲 modulex-client ,那麼該模塊打包名稱即爲 modulex-client.tar.gz),並且存儲至 Amazon S3 存儲桶中;然後運維人員需要根據程序的配置文檔(由研發人員提供相應的 README 文件,指明程序如何安裝、如何指定配置文件等)開發相應的安裝程序,然後將安裝程序推送至 REL 倉庫中。這同樣會觸發 REL 倉庫的 pipeline ,通過調用相應的 Docker Image 將安裝腳本打包存儲至 Amazon S3 中,同樣,我們也需要定義發佈庫庫的打包文件名同發佈倉庫名稱(去掉 .rel 後綴)加速指定的壓縮後綴(比如 ModuleX.rel 打包之後的名稱爲 ModuleX.tar.gz)。這樣在 S3 中就有了編譯後的二進制程序,以及該程序的安裝腳本,我們在 DIST 倉庫的整合程序中只需要根據兩個包的 S3 存儲位置將其下載下來,執行安裝腳本即完成該模塊的安裝,對於其他模塊也是如此。DIST 倉庫只需要一個整合程序將所有的子模塊(倉庫)按照一定的邏輯順序組合起來,就可以完成了一個項目的發佈。至此,我們已經能夠完成一個“半自動”的項目安裝,運維人員只需要將 DIST 倉庫中的整合程序下載到節點並執行,整合程序會去相應的 Amazon S3 存儲上下載各個倉庫的安裝程序與二進制文件(如果有的話),然後執行各個模塊的安裝程序,這樣當所有的模塊安裝配置完成後,一個完整的項目就已經正常運行起來了。

接下來,我們可以藉助自動化執行工具(如 Ansible、Puppet、Saltstack 等)與 pipeline 完成節點的自動化批量部署。即系統運維人員將整合腳本提交至 DIST 倉庫後,會觸發倉庫的 pipeline ,將整合腳本與其他相關文件(如項目版本信息、子模塊引用信息,以及相應的存儲信息等)推送至安裝了自動化執行工具的中控節點,中控節點將安裝或者更新任務分發到子節點上,繼而完成全球節點持續集成與持續發佈。

當完成節點的發佈任務之後,我們需要對發佈結果(特別是失敗結果)做有效監控與通知反饋。從中控平臺發佈任務到節點執行,至少包含兩個關鍵節點:一是中控平臺能否成功將任務下發到被控節點,二是被控節點能否成功執行下發任務。前一個關鍵節點可以通過中控平臺的分發任務執行結果(或者日誌)得知,後一個關鍵節點可以通過被控節點的任務執行狀態(或者日誌)得知。架構的執行流程圖如下:

圖2 架構執行流程圖

3 架構實現

3.1 Amazon S3 存儲規則

我們需要制定 S3 Bucket 存儲桶結構規則,以便於後期程序可以按照這樣的規則完成自動存儲與獲取。首先,一個項目對應一個存儲桶,即 Amazon S3 Bucket;其次,每個子倉庫(模塊)是該存儲桶下的一個對象(可以簡單的理解爲一個目錄),而上文提到的源碼倉庫編譯後打包的二進制文件,以及發佈倉庫打包的安裝配置等文件皆存儲在該目錄中。如果我們繼續以上文提到的 ProjectX 項目爲例,那麼當前存儲桶的結構如下:

ProjectX:  
        - ModuleX  
            - modulex-client.tar.gz  
            - ModuleX.tar.gz  

這樣已經可以滿足單個模塊功能的信息存儲,但是隻能存儲最近一次打包結果。如果我想存儲之前發佈的某個版本,則不能實現,設置不能夠進行該版本的任何測試就會被推上生產系統,這顯然不合理。爲了解決這個問題,我們給存儲桶又加了一層“分支”與“版本控制”的結構概念。對於分支,一般我們在生產開發中至少都會有兩個 git 分支,即 master 與 developer 分支,其中 developer 分支代碼用於功能測試,當測試通過之後提交到 master 分支,用於正式版本發佈。因此我們在 S3 存儲桶中模塊對象下,又新增了一層目錄結構,developer 目錄與 master 目錄,針對測試代碼,我們無需存儲之前的版本,只保留當前最新代碼即可,因此直接將打包後的文件存儲至 developer 目錄下即可替換之前的測試程序。但是對於發佈版,則需要保留歷史版本,以便快速回滾,或者由於某些其他模塊引用了當前模塊的某個指定版本,而導致該版本必須保留。因此 master 分支與 developer 不同,不能通過簡單的創建一個 master 目錄來解決問題,基於此,我們對 mater 分支制定了一個規則,即一旦合併了 developer 分支,必須打上相應的 tag 信息(否則無法觸發 PIPELINE 完成自動構建),存儲桶功能模塊下不再以 master 作爲主分支打包程序的放置目錄,而是以 tag 名稱作爲放置目錄。這樣的話,我們就可以通過 tag 信息,獲知某個目錄下存儲的是哪個 tag 的發佈代碼,這樣當前的存儲桶目錄結構如下:

•       ProjectX:  
•        - ModuleX  
•               - developer  
•                - modulex-client.tar.gz  
•                - ModuleX.tar.gz  
•            - 1.0.0  
•                - modulex-client.tar.gz  
•                   - ModuleX.tar.gz  
•            - 2.0.0  
•                   - modulex-client.tar.gz  
•                   - ModuleX.tar.gz  
•        - ModuleY  
•            - developer  
•                - moduley-client.tar.gz  
•                - ModuleY.tar.gz  
•            - 3.2.0  
•                - moduley-client.tar.gz  
•                - ModuleY.tar.gz  

3.2 倉庫目錄結構與功能說明

源碼倉庫(source repository):倉庫名稱以 ‘.src’ 作爲後綴,包含源代碼、PIPELINE 文件(注意:bitbucket 的 PIPELINE 文件名稱必須是 bitbucket-pipelines.yml)、程序配置文件模板(如果需要的話)、CHANGELOG 與 README 等說明文件,以便運維人員開發相應的安裝配置程序。
 
發佈倉庫(release repository):倉庫名稱以 ‘.rel’ 作爲後綴,包含源碼編譯後的二進制程序安裝與配置程序(腳本)、升級程序、配置文件、源碼庫版本信息(安裝程序就是根據這個版本(或者說是 tag)信息去 S3 上獲取相應 tag 名稱目錄下的打包文件)。
  
整合倉庫(distribution repository):倉庫名稱以 ‘.dist’ 作爲後綴,包含整合程序(安裝與升級)、維護一個項目(project)所需的模塊列表以及各模塊的版本信息,整合程序根據各個模塊列表的版本信息從 Amazon S3 的存儲上下載相應模塊的指定版本,通過執行各模塊的安裝腳本,完成各個模塊的安裝。
 
模塊版本信息文件(repo-info):該文件存在於 REL 倉庫與 DIST 倉庫中,滿足 yaml 文件格式。該文件在 rel 倉庫中主要用於記錄該模塊的源碼 tag 信息(後面如無特殊數碼,同“發佈版本信息”同義),發佈庫分支或版本信息,以及依賴模塊的發佈版本信息,文件格式如下:

•    # Repo-Info 文件配置模板,其中 repo_name 可以理解爲單個模塊功能在 git 中的倉庫名稱  
•    rel_branch: [developer|rel_tag_info] # 生產環境只能 developer OR rel_tag_info 二選一  
•    src_branch: [developer|src_tag_info] # 生產環境只能 developer OR src_tag_info 二選一  
•    depend_list: [none] # 生產環境只能是 none 或者 下面的 KEY-VALUES 二選一  
•      repo_name1: 1.2.3  
•      repo_name2: 1.1.1  
•      repo_name3: 1.1.2  
•      repo_name4: 2.1.3  
•      repo_name5: 3.1.3  

通過上面的格式定義, rel 倉庫中的安裝腳本就可以去 S3 存儲上獲取源碼安裝包,以及某個版本的依賴模塊安裝包。簡單說明下配置文件各個參數的含義: rel_branch: 用於指定 REL 倉庫的打包文件存放位置,如果是 developer 則表示存儲在 S3 上模塊名稱目錄下,developer 目錄中,用於生產測試;而如果是 rel_tag_info 則表示存儲在 S3 上模塊名稱目錄下,對應 tag 名稱目錄中,用於生成發佈。因爲只有在測試通過之後,纔會合併到 master 分支,而 master 分支也只有在打上 tag 之後,纔會觸發 pipeline 完成自動構建,將對應的打包文件存儲在以當前 tag 爲名稱的目錄下。所以,S3 存儲中 tag 目錄下的打包文件,均是生產發佈版本。 src_branch: 用於指定 SRC 倉庫的打包文件存放位置,與 rel_branch 設計理念相同,測試分支會存儲在 developer 目錄下,正式發佈版本會存儲在以 tag 信息作爲名稱的目錄下。 depend_list: 用於指定該模塊的依賴模塊(或者說是庫),如果值爲 ‘none’ ,表示該模塊沒有依賴模塊,可以直接安裝。如果不是 ‘none’ ,則需要指定依賴模塊的名稱,以及相應依賴模塊的版本信息。需要注意的是,這裏的模塊版本信息我們規定只能使用 tag 信息,也就是隻能指定某個模塊的發佈版,而不能使用 developer 下的打包文件,即該模塊的測試版,因爲依賴模塊作爲一個底層模塊,必須保證穩定的前提下才能被其他上層模塊所引用,也才能保證上層模塊的穩定性。 而在 DIST 倉庫中的 repo-info 文件,文件格式同樣滿足 yaml 語法,文件的內容參數略有調整,以下爲模板文件:

 rel_branch: [developer|rel_tag_info] # 生產環境只能 developer OR rel_tag_info 二選一    
 sub_repo:  [none]       ##如果是 none ,則不會有下面需要安裝的倉庫列表信息 
   repo_name1: 1.2.3  
   repo_name2: 1.1.1  
   repo_name3: 1.1.2  
   repo_name4: 2.1.3    
   repo_name5: 3.1.3  
 upgrade_list: [none] # 如果是 none ,則不會有下面需要升級的倉庫列表信息    
   repo_name3: 1.1.3  
   repo_name4: 2.1.4  
   repo_name5: 4.0.0  

其中 rel_branch 與在 REL 倉庫中的含義一樣,用於表示 DIST 倉庫存儲在 Amazon S3 上的位置,但是需要注意的是 DIST 倉庫是沒有 src_branch 的,因爲這個倉庫就是專門用於各個子模塊的整合,不存任何代碼,只保留各模塊版本(也即是存儲)信息。 sub_repo: 表示這個項目(Project)由哪些模塊組成,各模塊的版本是什麼,整合腳本就是根據 sub_repo 的信息進行對應的功能模塊獲取與安裝的,這裏需要注意的是,模塊安裝會存在順序關係,這裏需要區別對待依賴關係。依賴關係是缺少某個模塊會導致當前模塊無法運行,或者部分功能不能使用;而順序關係,則是邏輯關係,缺少某個或者順序不對不會導致另一個無法安裝,只是整個業務邏輯上會存在一定的問題。舉個例子,我們編譯 nginx 需要 gcc 庫,必須先安裝 gcc 庫才能編譯安裝 nginx,這就是依賴關係。而 nginx 在接收客戶端請求後,可能會將請求轉給 php 處理,php 可能會去操作數據庫,我們一般安裝的時候,就會先安裝數據庫,但是這兩者之間的安裝就不存在依賴關係,而是一種順序關係。如果先安裝 nginx 然後在安裝數據庫,可能只會導致業務不能處理動態請求而已,而不會影響兩個應用的正常安裝。 upgrade_list: 用於指定項目升級(也即 DIST 倉庫升級)涉及到哪些模塊的更新,‘none’ 表示無升級需求,如果爲空,則需要根據下面的倉庫名稱,下載相應的子模塊,執行裏面的升級腳本(upgrade.sh)。

3.3安裝與升級

在我們定義完 Amazon S3 存儲規則,並且在發佈庫與整合庫中提供了模塊信息,各個模塊的安裝或升級程序就可以利用這些信息到 S3 上獲取相應的打包文件,這個文件會包含源碼編譯後的二進制文件(如果有的話)和針對該二進制文件的安裝配置腳本,這樣我們只需到讓整合倉庫根據 repo-info 文件中中的 sub_repo 模塊信息到 S3 上下載各個模塊的打包文件,執行各自的安裝與升級腳本即可。至於某個模塊依賴哪些模塊,則不用在 DIST 庫中處理,只需要在執行單個模塊的安部署裝(deploy.sh)或升級腳本(upgrade.sh)即可,因爲這個模塊是最清楚自己依賴哪些模塊的,在這裏處理也是最清晰、簡單的。下圖爲項目的安裝流程,升級流程與安裝流程類似,只不過安裝流程是下載各個模塊,執行各模塊的安裝腳本,而升級是執行各模塊的升級腳本。

圖3 項目安裝與升級流程圖

3.4 PIPELINE 規則制定

Bitbucket 支持 pipeline 功能,在滿足條件時觸發某種操作,這個操作我們可以簡單的理解爲調用 docker 鏡像完成某種行爲,比如編譯、打包、上傳至 S3 存儲等。這裏我們所需要闡述的是,如何制定 pipeline 的觸發規則?要回答這個問題,我們首先要弄明白利用 pipeline 的目的,即我們希望當開發人員 push 代碼時,能夠完成代碼的自動編譯、打包、發佈、測試、以及上線等行爲。其中代碼編譯、打包、發佈等行爲是統一的,而測試與上線是最終不同的兩個目的,這樣我們就像需要設定不同的規則來觸發測試與上線的不同行爲。根據 bitbucket 的官方文檔 中關於 pipeline 的觸發條件,可以分爲三種(實際上是四種,但是 bookmarks 是針對 Mercurial ,我們暫不討論),下面簡單說下這三種:

  • default :即一旦用戶 push 代碼即會觸發當前pipeline 。
  • tags: 即一旦用戶爲代碼打上 tag 標籤時就會觸。
  • branches: 即一旦用戶往指定分支上提交代碼時就會觸發。

結合我們設定的自動化執行邏輯,需要使用 tags 與 branch 作爲觸發條件,branch 設置爲 developer 分支,這樣當開發人員將代碼提交到 developer 分支時會立刻觸發 pipeline ,執行相關的編譯、打包、發佈、測試工作。tags 則用於觸發測試之後的發佈,即一旦我們在分支上打上 tag 標籤,就會自動觸發 pipeline 完成編譯、打包、發佈、上線工作。這裏需要注意的是,bitbucket pipeline 無法區分 tag 是來自於 master 分支或者 developer 等任意分支,只要檢測到有 tag 產生,即會觸發 pipeline ,所以打 tag 時一定要慎重,我們規定:必須在 developer 分支完成充分測試之後,才能 pull request 到 master 分支,而所有的 tag 也必須是打在 master 分支上的。當然,意外不可避免,我們做了相關的考慮,如果因爲誤操作或者測試不充分導致當前版本部署到實際生產節點不能正常使用,只需要 rerun 上個 tag 的 pipeline 即會重新打包發佈老版本。

3.5一源多存問題

在講述該問題前,我們先來聊一個業務場景,公司研發了一個底層的基礎功能模塊,如何被兩個甚至多個項目所引用,最簡單的做法是將這個模塊作爲代碼的一部分直接提供給各個項目使用,這就容易造成生產中經常遇到的多源問題,後期隨着由於各項目的發展,可能會對引入的基礎模塊功能做調整,那麼調整後的基礎模塊就不再能夠保證被其他項目所通用,那麼修改後的代碼提交到哪呢?只能建立新倉庫提交了,這樣我們從維護一個基礎庫就變成了維護多個基礎庫,開發基礎模塊的目的本來就是抽象功能、代碼複用、提高開發效率,這顯示事與願違。

當然針對這種多源問題,比較常見的解決辦法是使用 subtree 或者 submodule 的方式將倉庫引入到項目中,subtree 會直接將代碼引入,這樣會導致我很難快速的指定當前項目引入的是哪個版本的基礎模塊。submodule 可以理解爲引入的是一個指針,指向某個版本的基礎庫,這在一定程度上能夠達到我們的目的,但是一旦被多個項目或者多級(特別是多級引用)引用,如果更新基礎模塊,則引用該模塊的所有上級模塊需要一級一級重新引入,簡直就是噩夢。而我們實現的通過倉庫中指定引用基礎模塊版本的方式就非常簡單了,當基礎模塊更新時,引用模塊如果不需要更新,則不用修改自身的依賴倉庫版本信息,如果需要使用新版本基礎庫,只需要將依賴庫的版本號修改爲想要引用的版本號即可, S3 存儲桶中存放了所有的穩定版本,操作非常之簡單。這樣我們只需要從一個源提供代碼,就可以被多個項目引用,而且可以引用不同版本,互相不影響。

我們知道,一個完整的項目需要多個不同的模塊,我們是使用存儲桶的方式將各個模塊打包存放,那麼也就是說每個存儲桶中都包含了所有的模塊打包文件,以及不同版本的打包文件,那麼當一個基礎模塊在觸發了 pipeline 完成自動編譯打包之後,如果分發到不同的存儲桶中呢?這就是我們接下來要討論的“一源多存問題”。 這裏我們需要引入 pipeline 中變量的概念,我們從 bitbucket 關於變量的 官方文檔 中可以得知,pipeline 可以使用 bitbucket 內置的變量,當然我們也可以自己定義變量,如果變量名稱相同,會以自定義變量爲準(準確的說,會以最後聲明的變量爲準,但是內置變量聲明是在自定義變量聲明之前就完成的)。這樣我們就可以利用變量的方式,給不同的存儲桶設定不同的變量名稱,那麼打包存儲之後再根據不同的變量名稱將文件存儲至 S3 上即可。這樣我們就需要規範某些自定義變量,這些變量專門用於 pipeline ,而不能被其他腳本定義,這些變量有:

表1 PIPELINE 自動構建變量定義
 

上述表格,左邊第一列表示需要在 bitbucket 的每個模塊倉庫中定義爲 pipeline 使用的變量,第二列表示每個模塊的安裝與升級程序中定義的變量,第三列表示該變量的含義。下面我們從上到下解釋這幾個變量的含義以及用法:

S3 存儲桶名稱:用於告訴 pipeline 在完成打包之後發往哪個存儲桶。對於一源多存的問題,也是在此處解決的。首先由於我們是從一個源倉庫打包引入到不同項目中,那麼這份代碼在源倉庫中就是唯一且通用的,因此我們定義了變量“BUCKET_NAME” 作爲存儲桶名稱,有需要獲取 S3 存儲上的文件時,用變量代替;另外,我們通過在 bitbucket 的不同項目倉庫中設置不同的存儲桶名稱變量,在 pipeline 中將 ${BUCKET_NAME} 替換成 ${PROJECTA_BUCKET_NAME}   或者 ${PROJECTB_BUCKET_NAME},來將源文件中的存儲桶名稱修改爲當前項目的存儲桶名稱,並且發送至對應的存儲桶下,示例代碼如下:

	# 基礎模塊 REL 倉庫中的代碼(deploy.sh 與 upgrade.sh 均包含如下代碼)截取  
	BUCKET_NAME=''  
	  
	# 獲取源碼倉庫打包後的文件  
aws s3 cp s3://${BUCKET_NAME}/${REPO_NAME}/${BITBUCKET_TAG}/${BIN_NAME}.tar.gz ./  

從上面的代碼中,我們可以看到,獲取 S3 上存儲的文件,需要四個變量,即:${BUCKET_NAME}、${REPO_NAME}、${BITBUCKET_TAG}、${BIN_NAME},其中除了 ${BITBUCKET_TAG} 外,其餘的三個都是我們在上面自己定義的。稍後會對這幾個參數的使用做說明,我們繼續討論 ${BUCKET_NAME} 的使用。這裏我們看下 pipeline 中的部分代碼,就會明白是如何藉助與 bitbucket 的變量定義,完成不同源的多存儲問題了。

	# bitbucket-pipelines.yml 中部分代碼  
	script:  
	    # 發送存儲桶 BUCKET_A 中的打包文件  
	    - export AWS_ACCESS_KEY_ID=${BUCKETA_S3KEY} AWS_SECRET_ACCESS_KEY=${BUCKETA_S3SECRET}  
	    - sed -i 's#\${REPO_NAME}#'${REPO_NAME}'#g ; s#\${BUCKET_NAME}#'${PROJECTA_BUCKET_NAME}'#g' deploy.sh upgrade.sh  
	    - tar czf ${REPO_NAME}.tar.gz etc repo-info deploy.sh upgrade.sh  
	    - aws s3 cp ${REPO_NAME}.tar.gz s3://${PROJECTA_BUCKET_NAME}/${REPO_NAME}/${BITBUCKET_TAG}/  
	  
	    # 發送存儲桶 BUCKET_B 中的打包文件  
	    - export AWS_ACCESS_KEY_ID=${BUCKETB_S3KEY} AWS_SECRET_ACCESS_KEY=${BUCKETB_S3SECRET}  
	    - sed -i 's#\${REPO_NAME}#'${REPO_NAME}'#g ; s#\${BUCKET_NAME}#'${PROJECTB_BUCKET_NAME}'#g' deploy.sh upgrade.sh  
	    - tar czf ${REPO_NAME}.tar.gz etc repo-info deploy.sh upgrade.sh  
    - aws s3 cp ${REPO_NAME}

首先需要說明的一點是,上面的 pipeline 使用的 docker 鏡像是 atlassian/pipelines-awscli ,由 aws 官方提供,其中變量 AWS_ACCESS_KEY_ID 與 AWS_SECRET_ACCESS_KEY 分別表示訪問存儲通的 key 和 secret ,每個存儲桶的 key 和 secret 均不一樣,具體請參考 AWS IAM ,此處不再贅述。

我們通過在 bitbucket 上設置變量 ${PROJECTA_BUCKET_NAME} 和 ${PROJECTB_BUCKET_NAME} 來替換腳本中 ${BUCKET_NAME} ,以將存儲在某個桶下的項目腳本可以正確的適配該項目。

接下來我們說下其餘的幾個變量:

倉庫名稱:即 ${REPO_NAME},需要在 bitbucket 的每個倉庫中設定,主要有兩個目的,一是確定 S3 上的存儲位置;二是確定 REL 倉庫打包後的名稱。

二進制文件名稱:即 ${BIN_NAME},同樣需要在 bitbucket 的每個倉庫中設定,目的與 ${REPO_NAME} 一樣,用於確認 SRC 文件打包後的名稱,已經在 S3 上的文件存儲位置。

而變量 ${BITBUCKET_TAG} 則是 bitbucket 內置的變量,是用於獲取最近一次倉庫 tag 名稱的,這個不需要我們手動設定,只需要直接在 pipeline 中引用即可。

綜上所述,我們通過規定變量名稱,以及利用 bitbucket pipeline 內置變量、自定義變量,解決了同一份數據源適配不同項目的問題。這樣不同的項目整合程序,在各自的項目中引入的模塊就是以及適配好的程序,可以直接用於項目的安裝與升級。那麼接下來,我們就開始討論項目的自動化部署。

3.6 自動化部署

在我們完成 Amazon S3 存儲規則、倉庫結構、安裝與升級邏輯流程、PIPELINE 等規則內容設定以及一源多存問題解決之後,我們已經可以快速便捷的完成單個模塊、部分模塊以及整個項目的安裝與更新操作,但是這還需要人工參與,接下來我們就討論如何利用批量化部署工具,完成全球節點的自動化部署。 首先我們對批量化部署工具進行選擇,當前比較熱門的有 saltstack 、puppet、ansible 等,爲了降低維護成本以及快速部署,我們結合當前生產環境現狀以及生產節點數量,選擇了輕便快捷的 ansible 作爲批量控制平臺,它是基於 ssh 協議完成任務分發,所以安全性還是比較高的,另外不需要客戶端,操作簡單、學習成本低等特點,都是我們現下比較需要的。當然,後期隨着自動化平臺功能的不斷完善,以及運維節點數量的增加、對運維時效性的要求提高等因素,可能需要更換中控平臺或者自研該平臺,這雖是後話,但是我在設計當前的自動化運維體系時已經將該問題考慮進去,將整個體系進行模塊化分割,與中控平臺之間的耦合性降到最低,這樣以後無論使用何種中控平臺,都不會對現有體系造成太大波動,以達到降低影響、快速迭代上線的目的。

接下來我們就討論如何利用 Ansible 完成全球節點的自動構建與更新,核心邏輯處理流程如下: 爲了不產生歧義,我們以節點自動構建作爲說明對象,自動更新與之類似。當我們決定採用 ansible 作爲批量部署工具後,就需要根據業務邏輯設計相應的 ansible 執行腳本即 playbook ,腳本需要具備判斷某個節點是否滿足安裝或者更新條件的能力。爲了達到這個目的,我們通過在節點上創建 images.yaml 文件,模板如下:

 # 定義 image 版本信息  
 image:   
     version: 2.1.0   
 status: building  

我們對鏡像文件引入“節點狀態”與“節點版本”的概念,並且將節點狀態分爲:節點狀態文件不存在、building(節點構建中)、maintain(節點維護中)、failure(失敗)、active(可用)。對於這五個狀態,其意義如下:

節點狀態文件 images.yaml 不存在:即我們任務該節點爲新節點,會對該節點執行安裝操作(這個狀態文件作爲自動化執行的重要邏輯判斷依據,是絕對不允許被人爲干預的,即使是誤操作,程序也會自動重建該節點,以保證節點後期的可維護性)。

building(節點構建中):這是當對一個新節點進行項目安裝時,會新增 images.yaml 文件,並將狀態值修改爲 building,其實我們在 DIST 倉庫中存儲的 imags.yaml 文件初始狀態就是 building ,這也是符合我們的執行邏輯,安裝時直接將該文件放置到指定位置即可。

maintain(節點維護中):這種狀態是由節點的 active 轉變,即節點符合更新維護條件,則會將 active 修改爲 maintain 。

failure(失敗):表示節點安裝或者更新失敗,需要進行人工干預。

active(可用):解釋節點安裝或者更新成功,可以正常提供服務。

那麼 ansible 是如何根據上面的五種狀態,來決定該節點是否能夠進行安裝或者更新?如何避免對正在維護的節點再次觸發維護執行維護程序?Ansible 的判斷依據是,節點狀態文件不存在,直接執行安裝操作,安裝操作由 DIST 的安裝程序執行,該程序執行完會判斷本次執行是否成功,成功則將節點狀態由 building 修改爲 active ,不成功則修改爲 failure 。這樣,我們就能得出 failure 狀態是由於整合倉庫的安裝(或者更新)腳本導致,而跟 ansible 無關,也就不應該再讓 ansible 繼續對 failure 狀態的節點下發安裝或者更新任務。因此,我們得出結論,ansible 只會對新節點執行安裝操作,只應該對 active 狀態的節點執行升級操作(當然這是必要條件,而不是充分條件,否則節點會一直處在 maintain 跟 active 間切換)。因此我們通過引入“節點版本”的概念,來作爲另外一個判斷是否可以進行升級操作的必要條件。我在開發 ansible playbook 腳本時,引入了“可更新節點版本”的變量(該變量通過 playbook 的變量文件存儲,因此每次升級項目修改整合倉庫時,也需要修改該變量文件中關於“可更新節點”變量的值),即只有當這個變量與鏡像文件中的節點版本相匹配時,纔會滿足升級條件。即,我們判斷節點是否應該升級會對節點狀態與節點版本判斷,只有節點狀態爲 active 且節點版本與 ansible 中定義的可升級版本所匹配,纔會執行升級操作。

接下來,我們繼續討論新版本發佈問題。當我們確定要發佈新版本時,會根據本次版本中各個模塊的調整修改 DIST 倉庫的 repo-info 信息、ansible playbook 執行文件、CHANGELOG 、 README 等信息,當然,可能還需要修改 DIST 倉庫中的安裝腳本。爲什麼說可能呢?根據前面的設計邏輯, DIST 倉庫中的安裝腳本只負責根據 repo-info 文件從 S3 上獲取各個模塊的安裝包,然後執行各安裝包中的部署腳本即可。這樣的話,其實 DIST 倉庫的安裝腳本只需要執行一個循環,依次讀取 repo-info 中模塊信息,然後下載文件、安裝文件即可。無論是新增、刪除、修改功能模塊,只需要對 repo-info 中該模塊的內容進行相應的新增、刪除、修改即可,而不必去修改整合腳本的內部邏輯,這樣極大的降低了維護 DIST 倉庫的人員水平。也就是說,絕大多數情況下都不需要修改 DIST 倉庫的安裝腳本,但是有些情況下可能需要單獨處理某個模塊,例如這個模塊的處理邏輯與其他模塊不同,那麼就需要調整整合腳本,以兼容該模塊。舉個最簡單的例子,假設模塊C 在執行部署程序時,需要給該程序傳遞指定參數,那麼這就與其他模塊的處理邏輯不同,必須單獨設定。

總而言之,我們需要根據將要發佈的版本做整合倉庫的適配,然後將代碼提交至 DIST 倉庫 developer 分支,觸發 pipeline ,將倉庫中指定文件(包括 playbook 以及其變量配置文件、images.yaml、安裝與升級腳本、倉庫 repo-info 文件等)推送至 ansible 控制節點的某個目錄下,ansible 會定期執行該目錄下的 playbook 文件,進行全球節點任務分發,對滿足安裝或者升級的節點進行自動化安裝與更新操作。綜上所述,ansible 執行自動化安裝與升級流程圖如下:

圖4 Ansible 執行自動化安裝與升級流程圖

整個流程可以概括爲:
(1). 適配 DIST 倉庫內容,觸發 pipeline 推送至 ansible 控制節點。
(2). Ansible 執行 DIST 倉庫中 playbook 文件,全球節點下發任務。
(3). 利用 playbook 邏輯對各節點進行狀態判斷,決定執行安裝、更新程序或者終止任務。
(4). 對於執行安裝或者更新程序的節點,會根據程序的執行結果修改節點狀態爲 active 或 failure 。

3.7 監控與報警

當我們利用 bitbucket pipeline + ansible 完成全球節點的持續集成與持續發佈之後,接下來需要解決的問題就是監控。需要注意的是,這裏我們提到的監控是指 CI/CD 結果的監控,而不是常規的節點流量、內存、CPU 等性能監控。通過上面的分析我們知道從 ansible 獲取最新的 playbook 到將任務推送至全球節點執行,可以分爲兩個環節,第一是 ansible 將任務推送至全球各節點上,進行安裝或更新邏輯判斷;第二是 ansible 執行完判斷邏輯後,符合安裝或更新的節點會執行相應的腳本進行安裝或者升級,然後腳本根據自身的執行結果對節點狀態進行修改。這樣的話,我們的監控也應該觸及到這兩個環節,第一、Ansible 是否成功將任務推送至全球各節點;第二、節點在接收到 ansible 推送的任務後,是否成功執行了安裝或升級腳本。對於第一個點,我們可以通過分析 ansible 的執行日誌獲取;對於第二點則可以從各節點狀態中獲取。這樣就形成了監控報警的框架,即我們首先對 ansible playbook 日誌中 recap 結果進行分析,對其中產生失敗(比如 failed、unreachable)的主機節點進行報警;然後通過 ssh 的方式連接遠程節點,對狀態值爲 failure 也進行報警,報警平臺我們使用 slack ,其提供了高效的報警接口(我們對該接口又做了二次開發,使其功能更加豐富),調用起來非常簡單,如下是發送 slack 的報警信息如下:

  時間: 2018-10-25 08:17:46,282    
 主機: 192.168.10.129    
 Ansible 執行狀態: success  
 節點狀態: failure

解釋一下報警信息各條目的含義:

時間:表示 ansible 推送任務到該節點的時間,以 ansible 服務器時間爲準。

主機:表示 ansible 正在將任務推送至哪臺節點。

Ansible 執行狀態:表示從 ansible 對該主機下發任務的狀態,狀態值有unreachable、 failed、success。 其中 unreachable 表示 ansible 與這臺節點連接失敗; failed 表示雖然 ansible 可以成功連接到該節點,並且執行 playbook 中的任務,但是因爲某些原因導致任務執行失敗;success 表示 ansible 成功連接到該節點,並且將 playbook 中的任務依次成功執行完。而根據我們之前的設計,節點的升級或者安裝,是由 ansible 調用升級或安裝腳本去執行的,因此 ansible 執行狀態成功,並不能表示節點被成功安裝或者升級了,我們需要繼續對節點狀態進行判斷。

節點狀態: 表示各個節點上 images.yaml 文件中記錄的節點狀態信息,有 active、failure、building、maintain 四個,另外加上節點狀態文件不存在與連接遠程節點失敗,一共有六個狀態值,但是 active、building、maintain 爲正常狀態,因此會產生報警行爲的節點狀態有 failure、節點狀態文件不存在以及連接遠程節點失敗三種。

綜上所述,下面的情況行爲會產生報警行爲:

(1). Ansible 連接遠程節點失敗
(2). 節點上執行 playbook 失敗
(3).節點上執行安裝或者升級腳本失敗
 
雖然我們設計的報警邏輯基本上涵蓋了大多數可能出現的錯誤,但並不是很嚴謹,我們也會繼續努力去制定更多維度的監控,以提高監控的有效性。

4 當前架構的優缺點分析

4.1 架構優點分析:

•    權限分配清晰

我們通過對 Git 倉庫實行三層邏輯分層,將開發人員與運維人員權限做了嚴格的區分,使其可以很好的協作而不會互相干擾。

•    自動化程度較高,且運維成本低

通過利用 Bitbucket pipeline + Amazon S3 + Ansible 實現了自動化持續集成與持續部署,且因爲各個功能模塊打包存放在 S3 中,中控節點僅推送 DIST 倉庫中的幾個文本文件到各節點上,然後在每個節點上執行安裝或升級腳本即可,各個節點會從 S3 上下載相應的模塊文件進行安裝或升級,這樣對中控節點的物理硬件性能要求非常低。縱觀這個運維項目,從 bitbucket 代碼倉庫,到 Amazon S3 存儲,再到 ansible 中控節點服務器,整個運維成本大概在 20 - 30 $/Month 。另外,由於自動化流程經過高度抽象與模塊劃分,對操作人員的技能要求也大大降低,這也減輕了運維成本。

•    支持廣域網自動化運維,不存在網絡瓶頸,併發能力非常強

傳統的自動化運維平臺一般是面向局域網的,一般的執行流程是把代碼下載至與同一局域網內的中控節點,然後由中控節點將其推送局域網內的所有節點,這就對中控節點的網絡帶寬帶來了極大的考驗,因爲一個項目上線,所有功能模塊的安裝包少則幾十兆,多則百兆甚至到 GB 級別,這是根本沒法實現廣域網自動化部署的,因此傳統自動化運維平臺一般都會有比較嚴重的網絡瓶頸,任務併發處理能力較低。而我們通過架構優化,讓中控平臺只推送數量很小的文本文件,可以輕鬆達到幾十甚至上百個任務併發,依然不會對中控節點造成太大的壓力。各個節點在執行安裝或者升級腳本時,會從 Amazon S3 上下載文件,而 S3 所支持的下行帶寬是不存在任何下載上的瓶頸的。

•    模塊引入非常靈活簡單

基礎模塊的引入一般是比較頭疼的問題,我們通過藉助於物理存儲的形式,以及存儲規則設定,可以輕鬆實現多個項目對某個模塊不同版本的引用,且數據源僅一個,不會造成後期的多源問題。

•    版本升級與回滾非常便捷

我們對整個運維流程進行高度抽象化、模塊化,項目或者單個模塊的安裝與升級全部依靠於各自的倉庫信息文件,即 repo-info 文件,升級或者回滾操作,僅需要修改配置文件中 S3 存儲的指向即可,秒級完成版本升級與回滾。

•    監控報警形成自動化流程閉環,安全性較高

監控是對整個運維框架的邏輯補充,可以幫助我們簡單且高效找到問題點,提升整個自動化流程的安全可靠性。

4.2架構缺點分析:

•    基於 ansible 處理,連接速度較慢,節點數在千臺以內

由於中控平臺採用 ansible 作爲批量化操作工具,所以也會受到其性能的影響。Ansible 是基於 ssh 連接對各節點進行操作,這就導致了連接速度比較慢,且維護量級一般在千臺以內。

•    監控與分析平臺不夠強大

目前監控平臺是基於兩個核心維度進行的,雖然可以發現絕大多數常見的自動化部署產生的問題,但是維度太小,遇到比較複雜的情況無法實現有效監控,而我們也正努力從其他維度去攻克這一監控難題。

5 下個版本需要做什麼

當前的自動化運維體系支持多種雲平臺、支持廣域網,能夠滿足中小企業(批量操作節點在 1000 臺以內)的日常運維工作,但是在該體系的構建與應用中,發現了一些問題,另外還有一些新的想法希望能夠補充進來,我們將這些放在下個版本中實,這些問題或想法包括但不限於以下這些:

•    提高監控展示能力:

監控維度不夠豐富全面,容易造成某些問題的遺漏。下個版本我們會從目標結果的方向出發(比如當前版本號、當前用戶連接數、當前流量等),做正向匹配監控,擬引入 elasticsearch 平臺,對這些目標數據進行採集,同時也將對安裝與升級日誌進行收集、以用於分析與結果展示,最終達到深度定位潛在的問題的目的。

•    提高中控節點連接速度

節點連接速度比較慢,ansible playbook 邏輯判斷太簡單,不太適合複雜模型處理。下個版本應該會調整批量化部署工具,以提高邏輯判斷能力與節點部署速度。

•    引入 CMDB 系統,實現節點分組

當前節點尚未分組,人工不干預的情況下,無法做灰度上線。這對生產發佈始終是一個隱患,下個版本應該會設計並引入合適的資產管理系統,擬通過引入 tag 的方式解決這一問題。

•    引入堡壘機與審計系統

這一功能雖然與當前版本不存在直接關係,但是確能夠解決運維中經常出現的節點權限分配混亂、事故責任糾纏不清、以及人員離職全球節點修改密碼等瑣碎問題。通過堡壘機機制,可以統一登錄入口,增加接入的安全性;另外可以在堡壘機中對人員權限進行嚴格分組、記錄用戶操作行爲等,這都是標準運維流程需要解決的事情。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章