重複利用的 Ansible Role 難題
大概一年前左右,我開始製作一個 Ansible 的 Playbook 來幫五倍紅寶石的客戶安裝環境。
不過當我們的客戶增加之後,其實開始有點變的很難透過 Fork 的機制來管理不同客戶的 Playbook。
這表示我必須先更新主要的 Playbook 然後再同步到每一個客戶的版本上,也因此我決定去把這些通用的部分拆成單獨的 Role 專案。
概觀
目前的 Playbook 大致上是這樣的:
├── [1.0K] README.md
├── [ 96] group_vars
│ └── [1.2K] all.yml
├── [ 96] inventories
│ └── [ 309] local
├── [ 480] roles
│ ├── [ 96] 5xruby_user
│ ├── [ 96] application
│ ├── [ 96] compile_env
│ ├── [ 96] deploy_user
│ ├── [ 96] init
│ ├── [ 128] logrotate
│ ├── [ 160] nginx_with_passenger
│ ├── [ 96] node
│ ├── [ 160] postgresql_server
│ ├── [ 96] ruby
│ ├── [ 96] ssh
│ ├── [ 96] sudo
│ └── [ 128] yum_install_commons
└── [ 467] setup.yml
當我們的客戶需要客製化他們的部署環境,我們會去 Fork 這份原始版本然後修改裡面的變數跟樣板。
不過在更新的時候就很容易遇到因為修改差異造成的衝突。
目標
在 Ansible Galaxy 裡面提供了相依管理的功能,這讓我們可以透過製作 roles/requirements.yml
來像像下面這樣管理:
1- src: https://github.com/5xruby/ansible-ruby
2 version: 0.1.0
3- src: https://github.com/5xruby/ansible-nginx
4 version: 0.1.0
在我們執行 Playbook 之前,我們可以利用 ansible-galaxy install -r roles/requirements.yml
來自動安裝對應的 Role,而且這能夠在 Ansible AWX(或者 Ansible Tower)上面正常運作。
看起來挺不錯的,不過實際上我遇到了一些問題。
Nginx 模組
以 Rails 專案來說,我們有很多種網頁伺服器的選擇。
如果我們選擇使用 Puma 的話,其實只需要將 Nginx 安裝並且設定為反向代理伺服器(Reverse Proxy)即可。
但是我們決定使用 Passenger 就必須將它編譯成一個 Nginx 模組。
這表示假設我們希望能夠同時支援 Puma 和 Passenger 的話,新製作的 Nginx Role 需要包含關於 Passenger 的任務。
我的第一個版本是利用 include_tasks
在 Passenger 被啟用的時候去增加額外的模組到 Nginx 上。
但是假設我們未來要增加更多的 Nginx 模組,我們的 Nginx Role 會越變越大最後就跟現在的 Playbook 狀況一樣。
手動管理相依
在經過幾次嘗試之後,我找到一個還可以接受的方法來處理這個問題。
- 產生一個內容為空陣列的 Fact 變數
nginx_module_options
- 遍歷
nginx_extar_modules
陣列然後import_role
去執行相關的 Role - 再額外模組的原始碼下載後,將額外的編譯參數插入到
nginx_module_options
這個 Fact 變數中
因此,在我們的 Playbook 裡面我們會像這樣設定相依:
1
2- src: https://github.com/5xruby/ansible-nginx
3 version: 0.1.0
4- src: https://github.com/5xruby/ansible-passenger
5 version: 0.1.0
並且覆蓋 Nginx 的變數,增加 Nginx 模組的設定作為預設值在 group_vars/all.yml
裡面套用到所有的 Web 節點上。
1nginx_extra_modules: ['passenger']
不過另外一個問題又緊接著解決了 Nginx 模組的問題出現。
Role 的相依性
當我準備好 Nginx, Ruby, Node.js 跟其他部署 Rails 必要的 Role 後,我開始設定 Rails Role 的相依設定。
1dependencies:
2 - src: https://github.com/5xruby/ansible-nginx
3 - src: https://github.com/5xruby/ansible-ruby
4 - src: https://github.com/5xruby/ansible-node
5 - src: https://github.com/5xruby/ansible-passenger
此時我執行我的 Playbook 去運行 Rails Role 的話,會從 Nginx Role 開始執行。
這看起來沒什麼問題,不過我們會需要設定 nginx.conf
並且將 root
設定到 Rails 專案的 public
目錄。
如果 Nginx Role 在 Rails Role 之前執行,我們就會碰到 Nginx 啟動失敗的錯誤。
我的第一個版本會透過 Nginx 來產生
root
的目錄,並且設定好所有者跟使用者群組,但是這是有問題的。如果deploy
這個使用者是透過 Rails Role 來產生的話,就會發生因為還沒有產生好使用者而無法設定目錄擁有者的情況。
不過在釐清問題之後,這算是一個人為的設計失誤。
「Nginx 真的是 Rails 的相依嗎?」
如果我們使用 Puma 的話,我們可以把 Nginx 替換成任何反向代理伺服器,實際上我們並不需要依賴於 Nginx。
最終成果
經過大概兩天的時間,最後終於完成了一個幾乎不太需要設定就能夠部署 Rails 伺服器的設計。
├── install.yml
├── group_vars
│ └── all.yml
├── inventory
├── playbooks
│ └── install-nginx.yml
│ └── install-postgres.yml
│ └── install-rails.yml
├── roles
│ └── requirements.yml
├── templates
└ ─── nginx.conf.j2
基本上是非常簡單的,大多只需要使用 import_role: nginx
這樣的語法去增加需要的 Role 即可。
如果我們需要更多的客製化,只需要覆蓋掉原本的變數(像是 nginx_config_template
)並且將自訂的樣板放到 templates/nginx.conf.j2
裡面。
在這邊我只放了預設的 Nginx 設定檔在 Nginx Role 裡面,如果要啟用 Passenger 的話需要自己放一個
nginx.conf
來設定。
結論
這算是一個很有趣的經驗來「解耦」一個部署腳本,作為一個工程師我們有很多規則可以去遵照來解耦程式碼。但是當你以一個維運的角度來看,要怎麼去製作一個可以重複利用跟管理的腳本呢?
不過這個還是一個起步,我現在正在思考假設未來要升級的話該怎麼做。
- 如何清理舊版本?
- 如果資料庫要升級,是否需要部署新的伺服器?
- 如果是用於製作 Cloud Image (像是 AMI)又該如何清理多餘的檔案?
DevOps 看起來似乎只要把工程師跟維運人員放在一起就可以做到,不過我認為要讓兩者能夠協作還是不容易的。