Heroku で運用するプレビュー環境に自動デプロイする試み(0)
中途半端だけど、せっかくやってみたので晒してみる。一番の収穫は、シェルスクリプトの trap を覚えた事だったりするのは内緒。
やっていること
- CI で CircleCI を使用している
- Git を使用している
- GitHub を使用している
- PR をレビューして master ブランチにマージする運用
- 開発時のプレビュー環境として Heroku を使用している
やりたいこと
- master ブランチの最新版を、労力少なくプレビュー環境に反映させたい
手作業
- 適宜 Heroku にデプロイする運用
- めんどくさい
Heroku の Automatic deploys
- Heroku の GitHub 連携機能のひとつ
- ブランチを指定しておくと、自動的にデプロイしてくれる機能
- Rails 固有の
rake db:migrate
みたいな後処理は実行させられない- ビルドパック作ればいいのかもしれないけど、試してない
- 残念
余談: Heroku の Pull request apps
- 存在する PR から Heroku アプリの複製を作れる機能
- PR が作られたら自動で複製を作ってくれる機能もある
- app.json を使う
- 環境変数の継承が使えて便利
- GitHub Integration: Pull Request Apps | Heroku Dev Center
- ちょっと前まで
inherit
キーを使うという話だった記憶があるけど、値を指定しなければ自動で継承される様になったっぽい?
- PostgreSQL 9.4 を使う場合、今は --version=9.4 を付ける必要があり、それを app.json の addons で記述する方法が分からない
- 環境変数の継承が使えて便利
- 便利 and 残念
CircleCI Heroku Deployment
- Continuous Deployment with Heroku - CircleCI
- CircleCI のアカウント設定で Heroku の API キーを登録する
- CircleCI のプロジェクト設定の Heroku Deployment で、使用する SSH キーのユーザをセットする
- ボタンを押すと Heroku と GitHub に公開鍵が登録される
- circle.yml の deployment でデプロイ設定書いたら ok
- 簡単 and 便利
雑感
- koshigoe/rails-example-circleci-heroku-deployment
- 既存環境の上書きは余計な心配を抱える事になって上手くないかもしれない
- Blue-Gree Deployment 的な考えで、毎回アプリを作成する方向で考えた方が健全かもしれない
- URL を変えずに運用する工夫の手間次第
- master ブランチの自動デプロイはやりすぎかもしれない
- 同一 Heroku アプリに対する並行デプロイが発生するはず
- 動作確認用の初期データを db:seed で冪等対策せず入れる事を考えているため、毎回 db:setup してしまっている
- プレビュー環境で確認中にデプロイされてデータが消えるとか、余計にストレスたるはず
- データベースのロールバックってどうするんだっけ
- 今回のリリースで変更したスキーマとデータを元に戻す、という作業になるんだっけ
- 今回は無理矢理 db:setup できるから気にしなくてもいいんだけど
- CircleCI の出力無しタイムアウトの上限はどれだけなのか
- コマンド全体のタイムアウトは別にあるのか、あるとして上限はどれだけなのか
- timeout: 3 して while で sleep 1 をすると延々続けられるみたいだけど…
machine: ruby: version: 2.2.1 dependencies: cache_directories: - "vendor/bundle" deployment: staging: branch: master commands: - ./deploy.sh: timeout: 600 # default
#!/bin/sh -ex # -e を使って途中でエラーが発生したら即終了へ # HEROKU_APP_NAME が未定義なら -u で拾って即終了 # HEROKU_APP_NAME が空なら if -z で拾って即終了 if [ -z "$HEROKU_APP_NAME" ] then echo "\$HEROKU_APP_NAME required." exit 1 fi # 1) まずはメンテナンスモードへ、失敗したら即終了 heroku maintenance:on -a $HEROKU_APP_NAME # よくあるシグナルを受け取ったら異常終了 trap "echo trap SIGHUP; exit 1" HUP trap "echo trap SIGINT; exit 1" INT trap "echo trap SIGQUIT; exit 1" QUIT trap "echo trap SIGTERM; exit 1" TERM # CircleCI の timeout は拾えない様なので諦める # ここ以降での終了を拾って後処理する # # 正常終了時) # 1) メンテナンスモード終了 # # 異常終了時) # 1) ロールバック # 2) DB セットアップ(リセット) # 3) メンテナンスモード終了 trap " case \$? in 0 ) echo Auto deploy successful. ;; * ) heroku rollback -a $HEROKU_APP_NAME heroku run rake db:setup -a $HEROKU_APP_NAME echo Auto deploy failed. ;; esac heroku maintenance:off -a \$HEROKU_APP_NAME " EXIT # 2) デプロイ git push -f git@heroku.com:$HEROKU_APP_NAME.git $CIRCLE_SHA1:refs/heads/master # 3) DB セットアップ(毎回リセット) heroku run rake db:setup -a $HEROKU_APP_NAME
シェルスクリプトの trap でシグナルを拾う場合の実行中コマンド
例えば、以下の様にして TERM を拾う様にしてみる。
#!/bin/bash -x trap "echo trap TERM; exit 1" TERM trap "echo EXIT" EXIT sleep 5 echo ok
以下は sleep 5
で待っている間に pkill -f trap-signal.sh
で TERM シグナルを送った時の結果。
$ ./trap-signal.sh + trap 'echo trap TERM; exit 1' TERM + trap 'echo EXIT' EXIT + sleep 5 ++ echo trap TERM trap TERM ++ exit 1 + echo EXIT EXIT
この時、 sleep 5
が完了するまでは待たされる。
シグナルを trap した時に、そのシェルスクリプト内で実行中のプロセスに対してシグナルを伝播させる簡単な方法は何だろう?
Heroku で Reverse Proxy
ろくに調べもせず、明後日の方向に進んでいる気がするけれど。
require 'rack/reverse_proxy' if ENV['BASIC_AUTH_USERNAME'] && ENV['BASIC_AUTH_PASSWORD'] && !ENV['BASIC_AUTH_USERNAME'].empty? && !ENV['BASIC_AUTH_PASSWORD'].empty? use Rack::Auth::Basic do |username, password| username == ENV['BASIC_AUTH_USERNAME'] && password == ENV['BASIC_AUTH_PASSWORD'] end end use Rack::ReverseProxy do reverse_proxy_options preserve_host: true, replace_response_host: true if ENV['BASIC_AUTH_USERNAME'] && ENV['BASIC_AUTH_PASSWORD'] && !ENV['BASIC_AUTH_USERNAME'].empty? && !ENV['BASIC_AUTH_PASSWORD'].empty? reverse_proxy '/', ENV['REVERSE_URL'], username: ENV['BASIC_AUTH_USERNAME'], password: ENV['BASIC_AUTH_PASSWORD'] else reverse_proxy '/', ENV['REVERSE_URL'] end end app = proc do |env| [ 200, { 'Content-Type' => 'text/plain' }, 'b' ] end run app
jaswope/rack-reverse-proxy を使って実現。gem だとリダイレクト対応がされていないので、GitHub の最新ソースを使用。
igrigorik/em-proxy も試してみたけれど、リダイレクト対応がよく分からず断念した。
最新 master ブランチからアプリを作って、デプロイが成功したら Reverse Proxy アプリの環境変数を更新(heroku config:set)してスイッチする、様な感じで使えれば良いな、と。
- Heroku の dyno 1 個で Reverse Proxy を動かすのはどうなんだろうか
- EventMachine 系であれば良いのかな
- Ruby 実装調べちゃったけど、node.js で Reverse Proxy したら良かったのかな
- リダイレクト対応が怪しい
- ログイン後にリダイレクトする様なアプリで試して上手くいったけど…
- google.com のリダイレクトはループした…
まあ、おもちゃ、かな。
FactoryGirl を使って冪等な Database seeding をやってみた
development, test 以外で FactoryGirl を使うのどうなんだ、という話は全力で脇に置く。こういう誤魔化し方もあるのかな、という試み。
んー、seeds.rb で find_or_create_by
を書かなくて良い、程度のおもちゃかな…。
構造
db/ 以下に色々置く感じの構造。
$ tree db/seeds db/seeds ├── development.rb ├── factories │ ├── cities.rb │ ├── countries.rb │ └── prefectures.rb ├── factories.rb ├── production.rb └── test.rb
rake db:seed
するとdb/seeds/#{env}.rb
が require されてコードが実行される- 共通処理書きたければ、各ファイルから共通ファイル require する、とか?
db/seeds/factories.rb
には共通処理を書いておくdb/seeds/factories/**/*.rb
にファクトリを置く- ファクトリは
initialize_with { find_or_initialize_by }
して冪等保証してるだけ
- ファクトリは
db/seeds.rb
これで spec/factories/ の方との干渉を防げるか自信なし。
表出力は本題とは関係なくて適当に。 Thread.current
も特に意味はなく適当に。
require 'factory_girl' include FactoryGirl::Syntax::Methods begin original_definition_file_path = FactoryGirl.definition_file_paths FactoryGirl.definition_file_paths = %W(db/seeds/factories) FactoryGirl.reload begin Thread.current[:seeding_results] = Hash.new {|h, k| h[k] = [] } ActiveRecord::Base.transaction do require_relative "seeds/#{Rails.env}" end Thread.current[:seeding_results].each do |klass, ids| puts Hirb::Helpers::AutoTable.render(klass.where(id: ids)) end end ensure FactoryGirl.definition_file_paths = original_definition_file_path end
使い方
ファクトリを書く
冪等保証のために initialize_with { find_or_initialize_by }
を使っているので、モデルごとにファクトリを作る必要がある。
ファクトリは db/seeds/factories/
以下に配置する想定。
FactoryGirl.define do factory :prefecture do initialize_with { Prefecture.find_or_initialize_by(country: country, name: name) } end end
seed を書く
実際の seed 処理は db/seeds/#{Rails.env}.rb
に書く想定なので、それぞれ書く必要がある。
create(:country, name: '日本') do |japan| create(:prefecture, country: japan, name: '東京都') do |tokyo| create(:city, prefecture: tokyo, name: '千代田区') create(:city, prefecture: tokyo, name: '港区') end create(:prefecture, country: japan, name: '神奈川県') do |kanagawa| create(:city, prefecture: kanagawa, name: '横浜市') create(:city, prefecture: kanagawa, name: '川崎市') end end