Railsアプリの自動デプロイ - hubotとheaven
はじめに
現在所属している会社では、ローカルのターミナルで直接 cap staging deploy コマンドを実行している。capistrano は自動デプロイツールとして非常に便利だが、いくつかの問題に直面することが避けられない:
- チーム内の全員が同じ環境を持っているわけではない
- みんながデプロイするため、staging に今どのブランチがあるのか完全に分からなくなる
- デプロイという作業がローカル環境に縛られる
スタートアップにとって、開発効率とフローが安定すればするほど、プロダクトそのものに集中できるようになる。そこで僕たちは以下のことを実現したいと考えた:
- 開発チームの全員が手軽にデプロイできる
- ローカルからコマンドでデプロイする必要がなく、余計な SSH 設定も不要
- PC を開いていなくても簡単にデプロイできる
- デプロイの状況を記録できる
- 問題が発生した場合、素早く前バージョンにロールバックできる
ターミナルでコマンドを叩き、SSH キーを手動で追加する日々にだんだん嫌気が差してきた。そこで、もっとスムーズなデプロイフローがないか自分で調べてみることにした。
以前いた Sudo では、幸いなことに @ocowchun と @henry という2人の効率化マニアが DevOps を完璧に作り上げてくれていたおかげで、煩雑な設定に追われることなく機能開発に集中できていた。(開発が終わった直後にサービス終了してしまったけれど…)
現時点で最も適切だと感じたソリューションは、hubot-deploy と heaven を組み合わせてデプロイを行うことだ。
だが、heaven のドキュメントは本当にひどい出来だった。
散々読み込んで、挙句の果てにソースコードを読んでようやく設定方法が分かった。そこで、他の DevOps 担当者たちが無駄な回り道をしないで済むよう、設定プロセス全体を共有することにした。
全体の流れ
{% asset_img “process.png” “Github deployment process” %}
hubot はデプロイコマンドを受け取ると、github deployment を送信し、同時に deployment イベントを発火させる。この時、GitHub は Webhook に設定された URL(ここでは受信側である heaven)に POST リクエストを送信する。heaven はリクエストを受け取るとデプロイを開始し、僕たちが知りたいデプロイ状況を順次レスポンスとして返す。
hubot-deploy
hubot-deploy は、Slack で Slack ボットにコマンドを送信することで、GitHub の deployment イベントを作成できる。
heaven
Rails アプリケーションだ。主に /events というエンドポイントがあり、GitHub deployment から送られてくる deployment と payload の受信を担当する。
設定手順
heaven のドキュメントは要領を得ず、hubot-deploy もあっさり流す程度にしか書かれていない。提供されているフロー図を頼りに、ひたすら試行錯誤と直感で進めるしかなかった。
hubot-deploy の設定
-
Yeoman を使って Hubot を生成し、
adapterにはslackを選択する。 -
package.jsonにhubot-deployを追加するか、npm install hubot-deploy --save-devを実行する。 -
external-scripts.jsonにhubot-deployを追加する。 -
apps.jsonでデプロイ対象のリポジトリを設定する:{ "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
デプロイ成功後、重要になる環境変数がいくつかある:
| 変数名 | 用途 |
|---|---|
| HUBOT_GITHUB_TOKEN | GITHUB_TOKEN。個人アカウント > Settings > Personal access tokens で設定。hubot はリポジトリの deployment を作成するだけなので、repo にチェックを入れるだけでよい。 |
| HUBOT_SLACK_TOKEN | Slack ボットのトークン。ここで設定できる。 |
環境変数は Heroku のダッシュボードから、またはコマンドラインで直接設定できる:
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 は現在のバージョン情報を返してくれる。
-
hubot が deployment イベントを送信したか確認する。
hubot deploy app to statgingと入力する。 -
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 にアクセスし、リポジトリをクローンしてくる。
- 環境変数を設定する
| 変数名 | 用途 |
|---|---|
| DEPLOYMENT_PRIVATE_KEY | heaven は SSH ログインを使用するため、秘密鍵(private key)が必要になる。サーバーが EC2 上にある場合は、pem 形式で設定することも可能。 |
| GITHUB_CLIENT_ID | 個人設定ページ > OAuth application から生成する |
| GITHUB_CLIENT_SECRET | 個人設定ページ > OAuth application から生成する |
| DATABASE_URL | heaven が deployment を記録するためのデータベースを作成する |
| GITHUB_TOKEN | heaven は stdout と stderr の出力先として Gist を使用する。そのため、トークンを設定する際は gist にチェックを入れるのを忘れないようにする。 |
その他の変数については ここ を確認してほしい。
DEPLOYMENT_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
--
これを以下のように変更する:
--\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-----
公開してしまった以上、この秘密鍵はもちろん無効化済みだ
Gemfile の設定
heaven の動作は、最新のリポジトリを pull してきた後に cap ... deploy コマンドを実行するというものなので、Capistrano のバージョンはデプロイ対象のバージョンと一致している必要がある。同時に、アセットに関連する gem も heaven に含める必要があることに注意しよう。例えば、僕の Capfile で以下のような gem を使っているとする:
gem 'capistrano', '3.4.0'
gem 'capistrano3-unicorn'
gem 'capistrano-rails'
gem 'sitemap_generator'
gem 'capistrano-rvm'
その場合、これらの gem を heaven の Gemfile に追加しなければならない。heaven はデプロイ対象のリポジトリをローカルに取得した後、そのディレクトリに入って cap staging ... deploy コマンドを実行するため、対応する gem がインストールされていないとデプロイできないからだ。
GitHub deployment との連携
- まずリポジトリの Settings > Deploy keys に SSH キーを追加する。
- リポジトリの Settings > Webhooks > Add webhook に進む。
- Payload URL には heaven をデプロイしたホストの URL を入力する(例:https://yourapp.com.tw/events)。変更したい場合は heaven リポジトリの
routes.rbで変更可能だ。 - Content type は
application/jsonを選択する。 - Secret は必要に応じて設定する。
- 下部の「どのイベントを購読するか」という質問では、deployment を使ってデプロイを行うため、deployment と deployment status を選択する。
デプロイ
Heroku にデプロイする場合、heaven は Redis と Resque を動かす必要がある。対応するアドオンと REDIS_URL を忘れずに追加しよう。
同時にデータベースの作成・マイグレーション(heroku run rake db:migrate)も忘れずに行うこと。
hubot-deploy よく使うコマンド
hubot deploy:version:現在のバージョンhubot deploy repo:apps.jsonに基づき指定したリポジトリをデプロイする。hubot deploy repo/branch:指定したリポジトリの特定ブランチをデフォルトの環境にデプロイする。HUBOT_DEPLOY_DEFAULT_ENVIRONMENTでデフォルト環境を設定可能。hubot deploy repo/branch to staging:指定リポジトリのブランチをstagingにデプロイする。
メモ
-
heaven のドキュメントは意味不明だが、コードとテストはかなりしっかり書かれている。Ruby に精通している開発者なら、heaven 全体を構築した上でコードに手を加え、ルーティングを追加して、ワンクリックでデプロイできる 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 スコープにチェックを入れておくこと。
-
Net::SSH::AuthenticationFailed: Authentication failed for user apps@staging.tripmoment.com:SSH の秘密鍵(private_key)の設定に誤りがある。まずこの SSH キーが GitHub に登録されているか確認し、次にパスフレーズ(passphrase)が削除されているか、そして SSH 秘密鍵を1行にまとめて改行コード(\n)を入れているか確認しよう。 -
ArgumentError: Could not parse PKey: no start line:SSH 秘密鍵のパスフレーズが削除されていない。
おわりに
通常、会社の開発チームの人数が少ない場合、DevOps はバックエンドが兼任することが多く、フロントエンドが触れる機会は比較的少ない。しかし、「自分はフロントエンドだから DevOps なんて知らなくていい」という言い訳をして学ばないのも筋が通らない気がする。健全なシステムを開発するには、決してフロントエンドだけで完結することはないのだから。
この記事では、公式ドキュメントで触れられていなかったり省略されていたりする手順をまとめてみた。heaven と hubot-deploy のドキュメントには言及されていない詳細が多すぎて、連携させる際にかなりの試行錯誤が必要だった。この記事が皆さんの地雷を踏む時間やソースコードを読み漁る時間を減らす手助けになれば幸いだ。
この記事ではまだ詳しく触れられていない DevOps の細部もたくさんある。やはり完全な DevOps パイプラインを構築するには時間がかかるし、僕自身も CI/CD の設定についてはまだまだ勉強不足だ。
参考リソース:
関連記事
- 測定が目標になるとき:窓税からPull Request数まで かつて僕は小さなツールを自作し、四半期で自分がどれだけPRに貢献したか、レビューコメントをどれだけ残したか、チケットをどれだけ消化したかを集計して、上司にアウトプットを証明しようとしたことがある。上司は淡々と、評価はアウトプットだけで見るものではないと言った。数年後、僕はようやく理解した――測定が目標になるとき、それはもはや良い測定ではなくなるのだ。英国の窓税、ハノイのネズミ駆除の報奨金から、現代のPR数による開発者評価に至るまで、そのメカニズムはまったく同じだ。
- Cloudflare Images を画像ストレージ・変換ソリューションとして使う ウェブページに画像を1枚置くのはフロントエンドにとって最も簡単なことだが、リサイズや各種フォーマットの生成、さらにはトラフィックの負荷に耐えることまで完璧にやろうとすると、実際には一つの包括的なソリューションが必要になる。僕はその後、すべて Cloudflare Images に任せるようになり、オリジナル画像1枚だけを渡すようにしている。
- もう AWS Access Key を使うのはやめよう Access Key は AWS において見落とされがちなセキュリティリスクだ。OIDC と IAM Role を組み合わせることで、GitHub Actions にシークレットを一切保持させることなく、安全に AWS リソースを操作できるようにする。
- データベース主キー:AUTO_INCREMENT、UUID、そしてUUIDv7 バックエンド開発で度々直面する主キーの決定。auto incrementを使うべきか、それともUUIDか?衝突への懸念は?UUIDv7とcreated_at + インデックスの性能差はどれほどか?実際に2,000万件のデータで検証したベンチマークと設計上の意思決定を解説する。