Elixir/Phoenixで作ったアプリの簡単なデプロイ方法 by ansible
こんにちは! Ruby/Railsプログラマーの方がElixir/Phoenixにチャレンジしていると、「Capistranoみたいに簡単にデプロイする方法ないの?」と思うかもしれません。
私が調べたところ、完璧にCapistranoの代替になるようなライブラリはなさそうでした。そこで下記の2つの方法を試してみようと思ったのですが、
- Deployment via docker
- Deployment via Ansible
docker管理は慣れているとそれほどですが、プライベートでやるにはtoo much workかなと思い今回は避けました。なので、今回ご紹介する方法はansibleによるデプロイ方法です。これはとても簡単なのでぜひ試してみてください。
なぜ簡単なのか。それはAnsistranoというライブラリがほとんどCapistranoのような振る舞いをしてくれるからです。
Ansible role to deploy scripting applications like PHP, Python, Ruby, etc. in a capistrano style
そう、ここではElixir/Phoenixをあげましたが、それらでなくても問題なくデプロイできます。 今回はPhoenixのデプロイプロセスをこのライブラリに当てはめていきます。
参考までに私が作ったレポジトリになります。
使い方
1. 環境設定
書き換える必要がある箇所を"TODO"としていますので、grepしてみてください。
2. サーバを用意
$ ansible-playbook -i production centos.yml -u root
3. Deploy app
$ ansible-playbook -i production deploy.yml -u deploy
基本的にはこれだけになります!細かいところは先程あげたレポジトリに譲りますが、それほど詰まることなくデプロイまで行けると思います。
中規模Railsアプリのアーキテクチャ設計
Railsでアプリを作っていますか?設計に問題を抱えている、あるいは悩んでいませんか?もしそうであればこの記事が役に立つかもしれません。
アプリケーションが大きくなっていくに連れて、コードがカオスになってきたり、どこで何が起こっているのか追いにくくなってくるものです。いわゆる「ファットコントローラ」「ファットモデル」と呼ばれる問題ですね。ところで、どうしてこの問題が起こるのでしょうか。それはMVCアーキテクチャが常に完璧ではないからです。Railsはアプリケーションの立ち上げに関しては本当に簡単な方法を提供してくれています。素晴らしいです。単にMVCと呼ばれる構造に従ってコードを書いていけばアプリケーションを書けるようになるのです。ただ、前述のように、どこかのタイミングで設計の変更を余儀なくされていきます。もうあなたはそのフェーズにいるかも知れません。どうやって何を変更したら良いのでしょう。
ここでは、ひとつの解決方法として、中規模サイズのRailsアプリケーションにおけるストラテジーを紹介します。ここでの方法はDDD(Domein Driven Design)と呼ばれる手法をベースにしています。記事の最後に本を紹介しているので興味があればぜひ手にとってみてください。
Environment
コントローラ層
コントローラ層はすべてのリクエストを受け付けます。基本的に、この層のみがドメイン層を呼べます。この層は可能な限りシンプルに保たれるべきです。
def save @request.assign_attributes(assignable_request_params) if @request.save RequestFlow::ReceiveFromAPI.execute(@request, @request_token) render json: { result: 'success' } else render json: { result: 'failed' } end end
ここでの RequestFlow::ReceiveFromApi
はドメインです。
ドメイン層
これは最も重要な層のひとつです。ドメインは技術者のみでなく、ビジネスやカスタマーサポートの人にも理解可能な共通言語で名前付けしてください。このタイミングでみんなで話し合うのもいいですね。例えば私は SelectXXX
, SendXXX
, CreateXXX
のようにつけることが多いです。ここでEmailなどの通知のようなフックイベントを定義します。理想的にはドメインはプロセス(手順)であったほうがよいです。
module RequestFlow module Update module_function def execute(request, options={}) GeocodeJob.perform_later(request) unless request.geocoded? Pipedrive::Updator.update_parameters( PipedriveStage.hash[request.status], request.pipedrive_deal, request ) request.update_status end unless options[:skip_notification] == true # This is a Service Layer Notifier::Request.complete_updating(request_offer) end end end end
ドメインはモデルとサービスを呼ぶことができます。しかし彼らから"呼ばれる"のは禁止です。 ドメインは中で何が起こっているのか理解しやすいようにしましょう。そうすると、このような会話が生まれることが想定されます。
CS: ユーザーがリクエストを更新したとき、何がおきるんだっけ? IT: ドメインをチェックしてみるね。そのリクエストは位置情報を付与されて、pipedriveに情報を投げているね。その後アップデート完了という流れだね。
いい感じですね。
サービス層
ここはステートレスな層です。サービスはコマンド(命令)のようなものだと思ってください。例えば、geocode_setter
, locale_finder
, distance_calculator
などです。私の経験では Notifier
(通知者)をここに置くのは良いと思います。
class Notifier::Request def initialize(request, timing = :wait, skip_validation = false) end ... def complete_request klass = new(request) klass.notify_internaly klass.via_sms klass.via_email end def via_email # call ActionMailer end end
モデル層
サービス層とドメイン層のおかげで、モデル層は薄くできるようになったはずです。モデルは自身が何を表しているかだけに関心を持ちます。純粋なRubyクラスもどんどん作りましょう。ActiveRecord::Base
を継承してることは必須ではないですからね。pureなRubyクラスは綺麗なデザイン、理解しやすい設計に大いに役立つはずです。
バリデーション層
この層もモデル層を薄く保つのに役立ちます。もしRailsにおいてバリデーション層をどう実装していいか知らなければ検索してみてください。実装方法は簡単で、ActiveModel::Validator
を継承するだけです。 それから validates_with
などを実装しましょう。当然ですが、ここではバリデーションのみに関心を持ちましょう。
これが基本的な設計の考え方にになります。もちろん、実際のプロダクトではもっと複雑になるでしょうけど、いままで私が関わったプロダクトではこのやり方はスケーラビリティにおいてもうまくいきました。この設計で重要なのは「誰が誰を呼べるか」の「方向(矢印)」です。これが正しく守られていないと、またカオスに逆戻りです。
もしもっとこの設計の理解を深めたければ下記の本がおすすめです。
それでは、Enjoy coding!
redux-formでwizard実装
最近onBoardingページにReact.jsをredux, redux-formと使い始めました。 今回はその際に私が躓いた3つのポイントを紹介します。
- デフォルト値の設定方法
- ラジオボタンを使ったラベル選択方法
- ユーザーが次のフォームに移った時にどうやって自動的に値を設定するか
ちなみに開発環境は下記になります
- "react": "15.4.2"
- "redux": "3.6.0"
- "redux-form": "6.4.3"
デフォルト値の設定方法
もしデフォルトのフォームを使っているのであれば、単純にinitialValueを使って実現できます。
const FirstForm = (props) -> ( <div> ... </div> ) export default reduxForm({ form: "wizard", initialValue: { name: "My Name" } })
しかし、redux-formのwizardを利用している場合、少し厄介なことになります。wizard formでは、複数個のformが出現しますが、値をイニシャライズするタイミングが最初の一度しかないからです。各フォームが出現するたびに何度もイニシャライズを走らせ直して値を再設定する方法もありますが、少し気持ち悪いですよね。結果として、単純ですが、各Componentの初期化タイミングで設定するとうまく行きます。具体的には componentWillMount()
で設定してます。
実装はこんな感じです。
class FirstForm extends React.Component { componentWillMount() { this.props.change("form-name", "value"); } } export default reduxForm({ form: 'wizard', // <------ same form name destroyOnUnmount: false, // <------ preserve form data forceUnregisterOnUnmount: true, // <------ unregister fields on unmount validate })(FirstForm);
ラジオボタンを使ったラベル選択方法
ラジオボタンをカスタマイズしたい時によくぶつかる問題です。これは単純に下記のように実装できます。
const setSelected = element => { document.getElementById(element.currentTarget.htmlFor).checked = true element.currentTarget.className = "selected" return false } export default setSelected;
そして、onClickメソッドとしてこのメソッドを呼び出します。
<label htmlFor="form-name" onClick={setSelected}> ... </label> <Field name="form-name" className="hidden">
ユーザーが次のフォームに移った時にどうやって自動的に値を設定するか
私は電話番号の入力に react-phone-input というライブラリを使用しました。この時、2つのフォームを用意しました。1つ目が、このライブラリが返してくるユーザーインプット。もうひとつが、redux-formが提供するhidden formです。 ユーザーが何かを入力すると、そのhidden-formを更新することになります。
setPhoneNumber = () => { var phoneNumber = document.getElementsByClassName("react-tel-input")[0].children[0].value this.props.change("phone_number", phoneNumber) } ... <ReactPhoneInput defaultCountry="de" onChange={() => this.setPhoneNumber()} /> <Field name="phone_number" type="text" component={renderPhoneNumberField} className="hidden" />
こんな感じになります。redux-formは便利ですが癖があるのでよくdocumentを読むことをおすすめします。
【メール送信エラー】Net::SMTPAuthenticationError
メール送信周りでちょっとはまりかけたのでメモ。
ponyというgemを使ってSMTPでメール送信をしようとしてました。
サイトからユーザーが申し込みしてきたら、申込完了メールをユーザーに送信するイメージです。
★環境
・Ruby
・pony on sinatra
・Gmailで送信(from @gmail.com)
たぶんRails(ActionMailer)でもあまり変わらないと思います。
普通にドキュメントに書かれてる通り実装すると、
Net::SMTPAuthenticationError - 534-5.7.14 <https://accounts.google.com/ContinueSignIn ...
というエラーが出てしまいました。
http://www.google.com/accounts/DisplayUnlockCaptcha
上記にアクセスして許可すればいいとう記事もありましたが、うまくいかず。
結論をいうと、送信したいGmailアカウントの設定から2段階認証を有効にして、
アプリ固有のパスワードを作り、実装にそのパスワードを組み込むのが手っ取り早いと思います。
↑ 左上の「2段階認証プロセス」の設定を有効にすると「アプリパスワード」という項目が現れるので設定してください。
以上でうまくいくと思います。
※参考までにponyを使った実装を載せておきます。
def send_complete_mail # charsetはデフォルトでutf-8なのですが、指定してないと怒られた気がします。 Pony.mail( to: '送信先メールアドレス', subject: '件名', body: send_mail_body, charset: 'utf-8', via: :smtp, via_options: { address: 'smtp.gmail.com', user_name: 'アカウント名@gmail.com', password: 'アプリ固有のパスワード', authentication: :plain, domain: 'サイトのドメイン名' } ) end def send_mail_body # send_mail.erbというファイルを用意しておけばよいです。 # localsのハッシュでview内の変数に値を渡せます。 erb :send_mail, locals: { hoge: session[:hoge], fuga: session[:fuga] } end
Mechanizeでページ遷移しながらスクレイピング
ちょっとダルいポイントが有ったのでメモ程度に。
スクレイピング対象サイトとスクレイピングの流れは
- ページャで何ページか一覧ページがある
- 一覧ページのタイトルをクリックすると詳細ページが見れる
- 詳細ページの一部を使用
- また他のタイトルをクリックしていく
- CSVで出力(別にいらないけどメモ代わりに。。)
みたいな感じです。mechanizeだけでやります。
require 'mechanize' require 'csv' class ScrapingPages def initialize @agent = Mechanize.new @data = [] end def retrieve # 1ページ目から10ページ目までスクレイピングする (1..10).each do |i| page = @agent.get(url(i) each_section(page) do |section| title = section.css('h2.title > a').first.text detail = @page.links_with(text: title).first.click @data << { title: title, detail: detail.links.first.text } end end end def each_section(page) page.search('.articleBox').each do |section| yield section end end def url(current_page) "https://hogehoge.com/#{current_page}" end def make_file_as_csv CSV.open("./csv/scraping-#{Time.now.to_i}.csv", "wb", encoding: 'Shift_JIS') do |csv| csv << %w(title detail) @data.each do |record| csv << [record[:title], record[:detail]] end end end end scraping_pages = ScrapingPages.new scraping_pages.retrieve scraping_pages.make_file_as_csv
コードは実際のものとちょこちょこ変更箇所あるので流しでいいのですが、ポイントはclickのところ。
mechanizeは中でnokogiriを使っているようで、
上記の@agent, pageとかはmechanizeクラスが 親のオブジェクトなのですが、
.cssとか使うと返ってくるオブジェクトがnokogiriクラスのインスタンスオブジェクトが返ってきます。
で、clickメソッドはmechanizeクラスに対してしか使えないので、微妙に工夫が必要。
title = section.css('h2.title > a').first.text detail = @page.links_with(text: title).first.click # .css('h2.title > a').first.clickとかすると、 # nokogiriがclickメソッド持ってないのでエラーになる。
ここです。title変数で一旦クリック個所のテキストを格納しておいて、
mechanizeクラス継承の@pageに対してリンクを辿って
テキストを指定してクリックする、と。
csvはよく忘れるのでメモ程度に載せただけです。
もっと良いやり方あるかもだけど。
Supervisor経由でunicornを立ち上げている環境にCapistrano3で自動デプロイ
前回の続きです。前回はCapistrano3の導入について書きました。
【入門】Capistrano3で自動デプロイ
★★★
私の環境ではsupervisor経由でunicornを監視しているのですが、supervisorをリスタートしてしまうとhot deploy出来ない問題がありました。
そちらに関しての解決策としては下記を参考にしてください。
supervisord + unicornでhot restart (deploy) する
さて、今回は上記を踏まえて
- capistrano3
- unicorn
- supervisor
- Rails4
- Ruby2.1.1(ここのバージョンはあまり関係ない)
という環境で自動デプロイしたいと思います。
# config/deploy.rb # config valid only for Capistrano 3.1 lock '3.2.0' set :application, 'アプリ名' set :repo_url, 'git@hogehoge:hogehoge/application.git' set :branch, 'master' set :scm, :git set :format, :pretty set :log_level, :info # :info or :debug set :keep_releases, 3 set :rbenv_type, :user set :rbenv_path, '~/.rbenv' set :rbenv_ruby, '2.1.1' set :rbenv_prefix, "RBENV_ROOT=#{fetch(:rbenv_path)} RBENV_VERSION=#{fetch(:rbenv_ruby)} #{fetch(:rbenv_path)}/bin/rbenv exec" set :rbenv_map_bins, %w{rake gem bundle ruby rails} set :rbenv_roles, :all namespace :deploy do task :stop do on roles(:app) do execute 'kill -USR2 `cat tmp/pids/unicorn.pid`' end end task :graceful_stop do on roles(:app) do execute 'kill -USR2 `cat tmp/pids/unicorn.pid`' end end task :reload do on roles(:app) do execute 'kill -USR2 `cat tmp/pids/unicorn.pid`' end end task :restart do on roles(:app) do stop end end after :finishing, 'deploy:cleanup' end
capistranoが通常の方法でリスタートしにいくところを上書きしに行ってるような感じです。
あとは環境ごとの設定を好きなように。
# config/deploy/production.rb set :stage, :production set :rails_env, 'production' set :bundle_gemfile, -> { release_path.join('Gemfile') } set :bundle_dir, -> { shared_path.join('bundle') } set :bundle_flags, nil set :bundle_without, %w{development test}.join(' ') set :bundle_binstubs, nil set :bundle_roles, :all role :app, %w{ユーザー名@デプロイ先IP} role :web, %w{ユーザー名@デプロイ先IP} role :db, %w{ユーザー名@デプロイ先IP} set :deploy_to, '/home/ユーザー名/アプリ名' set :ssh_options, { port: ポート番号, forward_agent: true } namespace :db do task :db_create do on roles(:db) do |host| execute "mysql -uroot -e 'CREATE DATABASE IF NOT EXISTS production_db;'" end end end
ポイントが有るとすると、Rails4からbinstubsが原因でバグる可能性が出てくるので、
binstubsオプションにnilを渡してコマンドを作らないようにしている。
(Rails3まではrailsのサブコマンドがscriptディレクトリ以下だったが、Rails4からbin以下になったため)
⇛参考 railsのサブコマンドが使えなくなる問題の原因はbinstubs
あとはデプロイするのみです。通常通り行ってください。
$ bundle exec cap production deploy
ちなみに後ろに--traceオプションをつけると、より詳細のログが見れます。
【入門】Capistrano3で自動デプロイ
※この記事はcapistrano3についてです。capistrano2.x系には対応していません。
ちょこちょこ新規開発しているのですが、毎回リモートサーバーにsshで入って
pullして手順見ながらbundleなんちゃらして・・・。
みたいなのが非常にめんどくさいので、capistranoを使ってみました。
一度覚えてしまうと楽チンなので損はないと思います!そんなに難しくないです!
今回はインストールから実際のデプロイまで順を追って説明していきたいと思います。
★やろうとしていること
- Rails4のアプリをリモートサーバーにローカルからデプロイ
- web, db, appサーバーはとりあえず同じサーバーで
- git pullとかassets:precompileとかmigrationとか自動でやりたい
- テスト的にvagrantで作った仮想環境にデプロイするまでを説明します。
★できてないこと
- unicornのhot deploy
■環境
- Ruby2.1.1(あまり今回は関係ない)
- rbenvを使っている
- vagrant(CentOS6.4)
まずcapistranoの導入です。Gemfileに書くだけなので超簡単です。
# Gemfile group :development do gem :capistrano gem :capistrano-rails gem :capistrano-bundler gem :capistrano-rbenv end
そしてあとはいつもどおりインストール。
$ bundle install
さて、ここまでできたらcapistranoのデフォルトファイル群を用意します。
これもコマンドひとつでできます。
$ bundle exec cap install mkdir -p config/deploy create config/deploy.rb create config/deploy/staging.rb create config/deploy/production.rb mkdir -p lib/capistrano/tasks Capified
こんな感じでいくつかファイルが出来たと思います。
この時オプション指定でdevelopmentを作ったりもできます。
今回はstagingを使ってvagrantにテストデプロイしていきます。
Capfile
まずはここからいきましょう。とりあえずいろいろ書かれていますが全部消しちゃっていいです。
require 'capistrano/setup' require 'capistrano/deploy' require 'capistrano/rails' require 'capistrano/rails/assets' require 'capistrano/rails/migrations' require 'capistrano/rbenv' require 'capistrano/bundler' # Loads custom tasks from `lib/capistrano/tasks' if you have any defined. Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
使うものをrequireしているだけです。これでOK。
config/deploy/staging.rb
次にデプロイ先の環境を設定しましょう。今回はvagrantにデプロイします。
set :stage, :staging role :app, %{vagrant@デプロイ先IP} role :web, %{vagrant@デプロイ先IP} role :db, %{vagrant@デプロイ先IP}
config/deploy.rb
これがメインとなるファイルです。ここにデプロイのtaskを書いていきます。
と、その前にデプロイ後どういうディレクトリ構造になるのかというのを知っておいたほうが理解しやすいと思うので下に図示しておきます。
[vagrant@localhost ~]$ tree アプリ名/ -L 2 アプリ名/ ├── current -> /home/vagrant/アプリ名/releases/20140421075958 ├── releases │ ├── 20140421062631 │ ├── 20140421064312 │ ├── 20140421064620 │ ├── 20140421074708 │ └── 20140421075958 ├── repo │ ├── FETCH_HEAD │ ├── HEAD │ ├── branches │ ├── config │ ├── description │ ├── hooks │ ├── info │ ├── objects │ ├── packed-refs │ └── refs ├── revisions.log └── shared ├── bin ├── bundle └── public 17 directories, 6 files
上のように、capistranoは[current, releases, repo, shared]という4つのディレクトリを作ります。
releasesにリリースごとのバージョンが管理されていき、currentはreleasesの最新へのシンボリックリンクになっているという感じです。
その他のディレクトリについてはまた調べてみてください。
では、config/deploy.rbに戻りましてとりあえず必要最低限のところから設定していきましょう。
# config valid only for Capistrano 3.1 lock '3.1.0' set :application, 'アプリ名' set :repo_url, 'cloneしてくるレポジトリのURL.git' set :branch, 'master' # デフォルトがmasterなのでこの場合書かなくてもいいです。 set :deploy_to, "/home/vagrant/アプリルートディレクトリ" set :scm, :git # capistrano3からgitオンリーになった気がするのでいらないかも? set :format, :pretty set :log_level, :debug # :info or :debug set :keep_releases, 3 # 何世代前までリリースを残しておくか set :rbenv_type, :user set :rbenv_ruby, '2.1.1' set :rbenv_prefix, "RBENV_ROOT=#{fetch(:rbenv_path)} RBENV_VERSION=#{fetch(:rbenv_ruby)} #{fetch(:rbenv_path)}/bin/rbenv exec" set :rbenv_map_bins, %w{rake gem bundle ruby rails} set :rbenv_roles, :all # default value
こんな感じでしょうか。
set :key, :valuie
のような構造になっています。
さて、いよいよデプロイ時に走らすタスクを書いていくというフェーズなのですが、
実はcapistranoはもともとデフォルトで既にいくつかのタスクを用意しています。
$ bundle exec cap -T
とやるとタスクの一覧が見れるのでみてみましょう。
cap bundler:install # Install the current Bundler environment cap bundler:map_bins # Maps all binaries to use `bundle exec` by default cap deploy # Deploy a new release cap deploy:check # Check required files and directories exist cap deploy:check:directories # Check shared and release directories exist cap deploy:check:linked_dirs # Check directories to be linked exist in shared cap deploy:check:linked_files # Check files to be linked exist in shared cap deploy:check:make_linked_dirs # Check directories of files to be linked exist in shared cap deploy:cleanup # Clean up old releases cap deploy:cleanup_assets # Cleanup expired assets cap deploy:cleanup_rollback # Remove and archive rolled-back release cap deploy:compile_assets # Compile assets cap deploy:finished # Finished cap deploy:finishing # Finish the deployment, clean up server(s) cap deploy:finishing_rollback # Finish the rollback, clean up server(s) cap deploy:log_revision # Log details of the deploy cap deploy:migrate # Runs rake db:migrate if migrations are set cap deploy:normalize_assets # Normalize asset timestamps cap deploy:published # Published cap deploy:publishing # Publish the release cap deploy:restart # Restart application cap deploy:revert_release # Revert to previous release timestamp cap deploy:reverted # Reverted cap deploy:reverting # Revert server(s) to previous release cap deploy:rollback # Rollback to previous release cap deploy:rollback_assets # Rollback assets cap deploy:started # Started cap deploy:starting # Start a deployment, make sure server(s) ready cap deploy:symlink:linked_dirs # Symlink linked directories cap deploy:symlink:linked_files # Symlink linked files cap deploy:symlink:release # Symlink release to current cap deploy:symlink:shared # Symlink files and directories from shared to release cap deploy:updated # Updated cap deploy:updating # Update server(s) by setting up a new release cap install # Install Capistrano, cap install STAGES=staging,production
よく見てみると、すでにgit pullやrake db:migrateやassets:precompuleも用意されています。
なのでミニマムではタスクは何も書かなくてもよいでしょう。
でもせっかくなのでDBがデプロイ時になければ作成するというタスクを追加しておきましょう。
# デプロイ前に実行する必要がある。 desc 'execute before deploy' task :db_create do on roles(:db) do |host| execute "mysql -uroot -e 'CREATE DATABASE IF NOT EXISTS データベース名;'" end end
これはnamespace :deployの外に書いて個別に実行すると良いと思います。
いくつかタスクについて説明すると、まずnamespaceで名前空間を切ってそれぞれを管理できます。
あとはrolesというのがポロポロ出てきていると思うのですが、これは例えばroles(:db)としておくと、dbサーバーに対してのみ実行します。(dbサーバーはconfig/deploy/以下で指定したものですね。)
taskの前にdescを書くことが出来て、タスクの説明なんかも残しておけます。
デプロイ
さて、ここまでくれば後は実際にデプロイするのみです。 ローカルからdeployコマンドをうってみましょう。
$ bundle exec cap db_create $ bundle exec cap staging deploy
うまくできましたか?本番リリース時はstagingのところをproductionにしてデプロイして下さい。
余談
僕が導入しようと思った環境はsupervisor経由でunicornを動かしていて、リリースごとにsupervisorを再起動する必要があります。
なのでsupervisorの再起動中に503がでてしまって"ゔッ!"ってなるのでcapistrano経由でunicornのホットデプロイをできないかとも
考えていたのですが、そもそもsupervisorを使ったunicornのホットデプロイのベストプラクティスがわかっておらず、
(先輩が調べてくれたのですがなかなかやっかいそう…)
そこんとこまだ完全に自動化しきれていないなー、、というのが現状です。。
ともかくcapistrano自体の導入はそれほど敷居は高くないので導入してみてはいかがでしょうか!(๑╹ڡ╹๑)