半熟前端

軟體工程師 / 台灣人 / 在前端的路上一邊探索其他領域的可能性

rails

Rails app 自動化部屬 - hubot 與 heaven

前言

目前所在的公司裡頭是直接在本地端的 terminal 跑 cap staging deploy 指令。 capistrano 作為自動部署化的工具非常方便,但難免會遇到幾個問題:

  • 不是團隊中的每個人都有相同的環境
  • 大家都在部署,結果 staging 上現在到底是哪個 branch,完全一頭霧水。
  • deploy 這件事情卡在本地端。

對一家新創來說,越穩定的開發效率和流程,就越能夠專注在產品當中。所以我們希望做到幾件事:

  • 開發團隊都可以輕鬆的部署
  • 不用在本地端下指令部署,還要多設定 ssh。
  • 就算沒有開著電腦,也可以輕鬆地部署
  • 能夠記錄部署的狀況
  • 如果出問題了,可以快速 rollback 回上一個版本

逐漸厭倦了在 terminal 打指令,ssh key 手動加的日子。於是打算自己研究有沒有更流暢的部署流程。

之前在 Sudo 裡頭,幸好有 @ocowchun@henry 兩位懶工,devops 做得非常完整,才能夠專注在開發功能,而不是一堆繁複的設定當中。(雖然才剛開發完就關閉服務了...)

目前覺得最合適的解決方案是搭配 hubot-deploy 以及 heaven 來幫助部署。

但 heaven 的文件實在寫的有夠爛。

看了老半天,甚至看了一下 source code 才知道到底該怎麼設定。於是決定將整個設定流程分享給大家,希望能夠減少其他 devops 們走歪路的時間。

主要流程

{% asset_img "process.png" "Github deployment process" %}

hubot 接收到部署指令後,會發送 github deployment,同時會觸發 deployment 這個事件,這時 github 就會發送 POST 給在 webhook 設置的 url(這邊接收者為 heaven),heaven 接收到請求之後,就會開始部署,再一一回傳我們想要知道的部數狀況。

hubot-deploy

hubot-deploy 能夠用 slack 對 slack-bot 下指令的方式建立 github 的 deployment event。

heaven

是一個 Rails 的 application。主要有一個 /events 負責接收從 github deployment 傳來的 deployment 與 payload。

設定步驟

heaven 的文件寫得不明所以hubot-deploy 也是草草帶過。幾乎只能靠著他們提供的流程圖,不斷的試錯與通靈。

設定 hubot-deploy

  • getting started with hubot

  • 利用 yeoman 產生 hubot,並且選擇 adapterslack

  • package.json 中加入 hubot-deploy,或者 run npm install hubot-deploy --save-dev

  • external-scripts.json 裡頭加入 hubot-deploy

  • apps.json 中設定想要部署的 repos 有哪些:

    {
     "repo_name": {
       "provider": "capistrano",
       "auto_merge": false,
       "repository": "kjj6198/deploy101",
       "environments": ["production", "staging"]
     }
    }

    這些資料在 hubot 送出 deployment 時會一併塞入 payload 當中。像是這樣:

       payload: {
         "name": "repo_name",
         "robotName": "yourrobot",
         "hosts": "",
         "notify": {
           "adapter": "slack",
           "room": "123456789",
           "user": "123456789",
           "user_name": "kjj6198"
         },
         "config": {
           "provider": "capistrano",
           "auto_merge": false,
           "repository": "kjj6198/deploy101",
           "environments": [
             "production",
             "staging"
           ]
         }
       }

特別要注意的是,provider 的欄位之後會送給 heaven,所以 provider 的值必須是 heaven 有的(之後會提到),或是自己實作 Provider。

這樣子我們的 hubot 就算設定完成了。先部署到 heroku 上測試看看,部署到 heroku 很簡單:

heroku login
git init
git add .
git commit "init"
heroku create
git push heroku master

部署成功後,比較重要的變數有幾個:

變數名稱 用途
HUBOTGITHUBTOKEN GITHUB_TOKEN,到個人帳號 > settings > personal access tokens 設定。設定好權限,因為 hubot 只是用來建立 repo 的 deployment,勾選 repo 即可。
HUBOTSLACKTOKEN 你的 slack-bot token。可以到這裡設定

全域變數可以到 heroku 的 dashboard 或是直接用 command line 設定:

heroku config:set HUBOT_GITHUB_TOKEN=abcccc
heroku config:set HUBOT_SLACK_TOKEN=abcccc

測試一下是否成功。在你設定的頻道中輸入 hubot deploy:version

{% asset_img "success.png" "success" %}

其中的 hubot 要跟你的機器人名稱相同,例如機器人的名稱為 tripmomo,那麼我就要輸入 tripmomo deploy:version

成功的話 hubot 會回應你目前的版本訊息。

  1. 確認 hubot 有送出 deployment 事件。輸入 hubot deploy app to statging

  2. 輸入 curl -H "Authorization: token YOUR_GITHUB_TOKEN" https://api.github.com/repos/my-github/my-repo/deployments 看看 deployment 是否建立成功。如果成功會回傳:

    {
       "url": "https://api.github.com/repos/my-github/my-repo/deployments/28301325",
       "id": 123456,
       "sha": "2e3xxxxxxxaaaaaaabbbbbbb",
       "ref": "develop",
       "task": "deploy",
       "payload": { // from apps.json
         "name": "my-app",
         "robotName": "tripmomo",
         "hosts": "",
         "notify": {
           "adapter": "slack",
           "room": "aabbccdd",
           "user": "aabbccdd",
           "user_name": "kalan.chen"
         },
         "config": {
           "provider": "capistrano",
           "auto_merge": false,
           "repository": "my-github/my-repo",
           "environments": [
             "production",
             "staging"
           ]
         }
       },
       "environment": "staging",
       "description": "deploy on staging from hubot-deploy-v0.13.27",
       "creator": {
         "login": "kjj6198",
         "id": 123456,
         "avatar_url": "https://avatars2.githubusercontent.com/u/123456?v=3",
         "gravatar_id": "",
         "url": "https://api.github.com/users/kjj6198",
         "html_url": "https://github.com/kjj6198",
         "followers_url": "https://api.github.com/users/kjj6198/followers",
         "following_url": "https://api.github.com/users/kjj6198/following{/other_user}",
         "gists_url": "https://api.github.com/users/kjj6198/gists{/gist_id}",
         "starred_url": "https://api.github.com/users/kjj6198/starred{/owner}{/repo}",
         "subscriptions_url": "https://api.github.com/users/kjj6198/subscriptions",
         "organizations_url": "https://api.github.com/users/kjj6198/orgs",
         "repos_url": "https://api.github.com/users/kalanchen/repos",
         "events_url": "https://api.github.com/users/kjj6198/events{/privacy}",
         "received_events_url":"https://api.github.com/users/kjj6198/received_events",
         "type": "User",
         "site_admin": false
       },
       "created_at": "2017-03-01T12:24:20Z",
       "updated_at": "2017-03-01T12:24:20Z",
       "statuses_url": "https://api.github.com/repos/my-github/my-repo/deployments/12345667/statuses",
       "repository_url": "https://api.github.com/repos/my-github/my-repo"
     }

    更多 deployment API 可以到 github deployment API 看看。

設定 heaven

  • heaven 將 repo clone 下來。
  • 設定全域變數
變數名稱 用途
DEPLOYMENTPRIVATEKEY 因為 heaven 是用 ssh 登入,需要 private key。如果 server 在 ec2 上,也可以用 pem 的方式來設定。
GITHUBCLIENTID 到個人設定頁面 > OAuth application 產生
GITHUBCLIENTSECRET 到個人設定頁面 > OAuth application 產生
DATABASE_URL heaven 會建立資料庫紀錄 deployment
GITHUB_TOKEN heaven 會使用 gist 來當作 stdout stderr。所以在設定 token 時記得把 gist 打勾勾。

其他的變數可以到 這裡 查看。

補充說明 DEPLOYMENT_PRIVATE_KEY:原始檔案長這樣

-----BEGIN RSA PRIVATE KEY-----
MJVGa/WNT9aFs63ykxLCdGzav8CfQ5vKXrLrllHXUYFaB2yaN72L+fSsXAy9zMs2
vy6wV2fB6j3YrVNCnBwUUNGTX9Ka6eeK98dCvHVyyE9Iz3CJAWZxaI03Px/xX9ps
M4kDWe7IA6+mnuCVSzwQVWMdOoAXbQbhGdfeixbqljNhJrKW/jA9w4BNarwGYv4E
0MwdU9x7zpk826ytza87yXHSdNuTKcsGQk4XHMYxJECj4EM8vTlVlEyEXZtCeh2z
P4bjYkTcBom4nC/q7Ea7Pmy1iDJqs0qc1L/xtNMypMhx4iIaeDVawkvBaL6t9IPT
KVuC9Y1uw5nJP1gwxXa5qoazhcikzqRYmaeWIzsZrcVShZBrJO9/a/APxXY7qJpJ
0r1YYTykw7THYj2QYiv8cfF64/vh9cB0NELEp5hIuS82Mf6CjqRR7QYR+By3uIdD
hQ77NMpQlmIC+TCJsLoADqwmEEZCiQSejtkXXtN/mNl581jP8+ViNkWZfPYWe7g6
yUeXVN1cBPo6AIu+lStE+SlR8lbu7sdpn6lid1pJf50zeythabze81y/nrAdx+Jn
scACBJBrERkhm2wdULkqwMV2g0U53YpYVAs2fFU1hGzRcE5zF1sdy9RLLX45Mzrm
lRErTbSUcnoQJhhCso5uNY6MMnr/rQF920KA0Ufr40IBcQ8bOSX7lJucST5bZLDg
H7g16rimHgK4I9rrvKy4plvbolfpuKGMYJDS3Q7IW5cL5lWLU3HaVSn+VyZe8p3A
prVx0XmSCwpmUzbDI6FoqniVPVdgis2tV1uKdnJPVn0DoK0ersosGXmALytbYLeE
arH/cIlGGCoGbIX+Iv3u8aICBEG2eR8eXmQSlGI5rp9hGK/JrlkL3PywVmPw4Efi
atiS6Y12Tuu8bdpPxBTzXK3PoZ23Pc+1l7NXXIzBeGnj56bALOIbAY5kg+lIRdtP
NSTAW8IVgFJUl4uzy/NXn/ewiE093ZVs59I2x4OoS14S20mkM/ldWbvlVm4Z3JxC
xIWsIV8aLznttic5MJUGjGoqH1Brg0o1HyWdkoEcC1N0G57oO4pN4UTD5co5xY9j
Ai2NIcFCYzqrdTfSlPWJBZLhjZ5hOXIwuTeJfRxDAVphaUqfpXb3o3URGRWiGENA
kIYKiq4XeNguwrFBzg5CB7NEKvjbjJ31GI26yAPa7yrKpuNFAjPpO6JKdL8slvx8
GXCOSbhGPFxzmtYzEeMxmnHqOa0Z953XeheKfJoipqRAyENxPBvclDonqVfxuTvw
cZzqFD+XjDJCJ5INwuwk2WupVzQjzV6TagcIX63Kq1Z9HSoFIBiCrdLzTMDG4Ro3
2wpN1tFQFz6alvwKtifGwhvG3qqmsfcQqw56gGY0DWIqG5x/thdG7UzZT7iMVDJV
LAO5wNnBK6L+feov9LqP7ONAonBVawmTv0ArjVhhkYZEi6d+ymvPpL1ORFAymLne
dpk4VmmmQvkUu0KudRqulavTIrnXFkuv2va+5X9mHGoNNMo1TXk2XX1eM4Rc7nAY
6IwPyAuFEtT5ocWBklB/qUZtdu4fG876o0X87GklR9ZfPG+tWpH2F+1j1mMHKuiP
-----END RSA PRIVATE KEY-----

要修改成:

-----BEGIN RSA PRIVATE KEY-----\nMJVGa/WNT9aFs63ykxLCdGzav8CfQ5vKXrLrllHXUYFaB2yaN72L+fSsXAy9zMs2\nvy6wV2fB6j3YrVNCnBwUUNGTX9Ka6eeK98dCvHVyyE9Iz3CJAWZxaI03Px/xX9ps\nM4kDWe7IA6+mnuCVSzwQVWMdOoAXbQbhGdfeixbqljNhJrKW/jA9w4BNarwGYv4E\n0MwdU9x7zpk826ytza87yXHSdNuTKcsGQk4XHMYxJECj4EM8vTlVlEyEXZtCeh2z\nP4bjYkTcBom4nC/q7Ea7Pmy1iDJqs0qc1L/xtNMypMhx4iIaeDVawkvBaL6t9IPT\nKVuC9Y1uw5nJP1gwxXa5qoazhcikzqRYmaeWIzsZrcVShZBrJO9/a/APxXY7qJpJ\n0r1YYTykw7THYj2QYiv8cfF64/vh9cB0NELEp5hIuS82Mf6CjqRR7QYR+By3uIdD\nhQ77NMpQlmIC+TCJsLohtJEmEEZCiQSejtkXXtN/mNl581jP8+ViNkWZfPYWe7g6\nyUeXVN1cBPo6AIu+lStE+SlR8lbu7sdpn6lid1pJf50zeythabze81y/nrAdx+Jn\nscACBJBrERkhm2wdULkqwMV2g0U53YpYVAs2fFU1hGzRcE5zF1sdy9RLLX45Mzrm\nlRErTbSUcnoQJhhCso5uNY6MMnr/rQF920KA0Ufr40IBcQ8bOSX7lJucST5bZLDg\nH7g16rimHgK4I9rrvKy4plvbolfpuKGMYJDS3Q7IW5cL5lWLU3HaVSn+VyZe8p3A\nprVx0XmSCwpmUzbDI6FoqniVPVdgis2tV1uKdnJPVn0DoK0ersosGXmALytbYLeE\narH/cIlGGCoGbIX+Iv3u8aICBEG2eR8eXmQSlGI5rp9hGK/JrlkL3PywVmPw4Efi\natiS6Y12Tuu8bdpPxBTzXK3PoZ23Pc+1l7NXXIzBeGnj56bALOIbAY5kg+lIRdtP\nNSTAW8IVgFJUl4uzy/NXn/ewiE093ZVs59I2x4OoS14S20mkM/ldWbvlVm4Z3JxC\nxIWsIV8aLznttic5MJUGjGoqH1Brg0o1HyWdkoEcC1N0G57oO4pN4UTD5co5xY9j\nAi2NIcFCYzqrdTfSlPWJBZLhjZ5hOXIwuTeJfRxDAVphaUqfpXb3o3URGRWiGENA\nkIYKiq4XeNguwrFBzg5CB7NEKvjbjJ31GI26yAPa7yrKpuNFAjPpO6JKdL8slvx8\nGXCOSbhGPFxzmtYzEeMxmnHqOa0Z953XeheKfJoipqRAyENxPBvclDonqVfxuTvw\ncZzqFD+XjDJCJ5INwuwk2WupVzQjzV6TagcIX63Kq1Z9HSoFIBiCrdLzTMDG4Ro3\n2wpN1tFQFz6alvwKtifGwhvG3qqmsfcQqw56gGY0DWIqG5x/thdG7UzZT7iMVDJV\nLAO5wNnBK6L+feov9LqP7ONAonBVawmTv0ArjVhhkYZEi6d+ymvPpL1ORFAymLne\ndpk4VmmmQvkUu0KudRqulavTIrnXFkuv2va+5X9mHGoNNMo1TXk2XX1eM4Rc7nAY\n6IwPyAuFEtT5ocWBklB/qUZtdu4fG876o0X87GklR9ZfPG+tWpH2F+1j1mMHKuiP\n-----END RSA PRIVATE KEY-----

既然公開,這組 private key 當然報廢了

設定 Gemfile

因為 heaven 的動作會是拉下最新的 repo 後,執行 cap ... deploy 的指令,所以 capistrano 的版本必須跟要部署的那個版本相同。同時,也要注意任何 asset 相關的 gem 也要一併放入 heaven。舉例來說,如果我的 Capfile 有用到

gem 'capistrano', '3.4.0'
gem 'capistrano3-unicorn'
gem 'capistrano-rails'
gem 'sitemap_generator'
gem 'capistrano-rvm'

那麼就要將這些 gem 加入 heaven 的 Gemfile 當中。因為 heaven 會將要部署的 repo 抓下來之後,進去資料夾輸入 cap staging ... deploy 的指令,所以如果沒有安裝相對應的 gem,heaven 就沒辦法部署了。

串接 github deployment

  • 先到 repo 的 settings > deploy key 加入 ssh-key。
  • 到 repo 的 settings > webhooks > add webhook
  • Payload URL 填入你的 heaven 部署 host 的網址,例如:https://yourapp.com.tw/events。如果想要修改,可以到 heaven repo 的 routes.rb 中修改
  • Content Type 選擇 application/json
  • Secret 依需求選填
  • 下面問你這個 webhook 要監聽哪些事件,我們是用 deployment 來做部署的,所以選擇 deployment 以及 deployment status。

部署

如果是部署到 heroku 的話,因為 heaven 要開 redis 跟 resque。記得加入相對應的 add-on 以及 REDIS_URL

同時別忘記了要建立資料庫 heroku run rake db:migrate

hubot-deploy 常用指令

  • hubot deploy:version 目前版本
  • hubot deploy repo: 根據 apps.json deploy 指定的 repo name。
  • hubot deploy repo/branch:將指定 repo 的某一個 branch 部署到預設的 environment 中。可設定 HUBOT_DEPLOY_DEFAULT_ENVIRONMENT 來決定
  • hubot deploy repo/branch to staging:將指定 repo 中的 branch 部署到 staging

筆記

  • heaven 的文件雖然不明所以,但是程式碼跟測試寫得蠻完整的,熟悉 ruby 的開發者甚至可以將整個 heaven 架設好,修改一下程式碼,加上 routes,直接建立 UI 一鍵部署。

  • OptionParser::AmbiguousOption: ambiguous option: -s:不確定是不是 Capistrano 更新之後指令有變動。解決方法是到 lib/heaven/provider/capistrano.rb 修改 deploy_command

    module Heaven
    # Top-level module for providers.
    module Provider
      # The capistrano provider.
      class Capistrano < DefaultProvider
    	.....
        def execute
          return execute_and_log(["/usr/bin/true"]) if Rails.env.test?
    
          unless File.exist?(checkout_directory)
            log "Cloning #{repository_url} into #{checkout_directory}"
            execute_and_log(["git", "clone", clone_url, checkout_directory])
          end
    
          Dir.chdir(checkout_directory) do
            log "Fetching the latest code"
            execute_and_log(%w{git fetch})
            execute_and_log(["git", "reset", "--hard", sha])
            deploy_command = [cap_path, environment, "部署的 cap 指令"]
            log "Executing capistrano: #{deploy_command.join(" ")}"
            execute_and_log(deploy_command)
          end
        end
      end
    end
    end
  • 因為 heaven 在部署時會使用 gist 當作 stdout 跟 stderr,在設定 GITHUB_TOKEN 的時候一定要記得把 gist 的 scope 打勾

  • Net::SSH::AuthenticationFailed: Authentication failed for user apps@staging.tripmoment.com :SSH private_key 設定有誤。先確定這組 ssh key 是否已經加入 github,再來確定將 passphrase 拿掉,並且將 ssh private key 變成一行加上 \n。

  • ArgumentError: Could not parse PKey: no start line 沒有將 SSH private key 的 passphrase 移除

後記

通常在公司裡頭,開發團隊人數不多的話,devops 都是由後端兼任的,前端比較少接觸。不過用「我是前端,我不需要管 devops」這種藉口搪塞自己不去學習好像也說不太過去,畢竟開發一個健全的系統絕對不可能只有前端而已。

這篇文章試著將文件中沒有提到或是省略的步驟整合起來,heaven 跟 hubot-deploy 的文件中有太多沒有提到的細節,導致整合起來時需要花不少時間試錯。希望能夠節省大家踩雷跟翻原始碼的時間。

這篇文章還有許多 devops 的細節沒有詳述,畢竟建立一套完整的 devops pipeline 需要時間,自己對於 CI/CD 的設定也還不夠熟悉。

參考資源: