Rubyでささっとウェブアプリを展開する方法
プログラミング::web
序
Rubyは手早くプログラムを書くことができる言語である。 そして、近年はwebのエンドポイントを求められることが非常に多い。
Rubyは別にweb向きの言語ではない。 webアプリケーションを書くことが前提であるのであれば、Node.jsを使ってJavaScriptで書くほうがだいぶ安い。
だが、あなたがRubyistであるならば、きっとRubyを使って秒で書きたいと考えるところだろう。 ここでは別にweb向きではないRubyを用いて、素早く、簡単にwebエンドポイントを提供することを目指す話をする。
なお、前提として読者はRubyが書けるし読めるものであるとする。
アプローチの種類
CGI
CGIは古くからあるウェブアプリケーションの仕様である。 Unixの基本的な機能を活用しており、シンプルなものである。
Nginxをウェブサーバーとして既に動作させている状態の場合、Nginxが直接にCGIをハンドルできないためあまり楽にならない。
また、リクエストにEOFをつけないHiawathaを使う場合は、rack/handler/cgi
は使えない。
cgiライブラリを使う場合は、レンタルサーバーなど自身が管理しないサーバー上でも動作させることが可能である。
また、複数のアプリケーションを動作させるにあたり、1度のセットアップで独立したコードにより動作させることができるのも大きなメリット。
Rubyでウェブアプリケーションを実装する上では悪くない選択である。
FastCGI
FastCGIはCGIに代わるウェブアプリケーションインターフェイスである。
CGIという名前はついているが、プログラムをデーモンプロセスとして動作させる、専用のバイナリプロトコルであるなど、CGIと特徴を共有しない。 基本的に時代遅れなものではあるが、PHPでは現在でも主流である。
Rack 3.0よりFastCGIのサポートが削除されたため、fcgiライブラリを使うのが基本的な選択肢。
この場合はspawn-fcgi
を組み合わせるのが一般的か。
この選択肢は、非常にプリミティブで非力なライブラリを使うことになるのが難点。
また、重要なポイントとして、現代における一般的なアプリケーションサーバーがHTTPを話すものであるのに対し、FastCGIは専用のプロトコルのサーバーであるというのもある。 つまり、FastCGIを成立させるためには
- FastCGIスクリプト
- FastCGIデーモナイザー
- FastCGIサポート
- ウェブサーバー
の4レイヤーが必要になるのである。 FastCGIをネイティブサポートするサーバーを使用している場合は必要なコンポーネントが少ないというメリットはあるが、開発/テストの意味でもほとんどの場合はアプリケーションサーバーを用意したほうが良いだろう。
個人的にはあまりおすすめしない。
Sinatra
Rackを使うwebフレームワークであるSinatraを利用する。
Rackの依存構成にSinatraが追加される形になるが、より短くコードを書けるのがメリット。
WEBrick
WEBrickはかつてはRubyにバンドルされた標準ライブラリであった。
現在は削除され、defaults gemsでもbundled gemsでもないという状態になってしまい、他のサーバーを使う場合と比べて環境構築上のアドバンテージはない。
ただし、WEBrickはそれ自体でサーブレットを構成できるフレームワークでもあるため、全体コンポーネントが少なくて済むというメリットがある。 また、WEBrickの機能は比較的豊富なため、静的ファイルのサーブなどと組み合わせたい場合も重宝する。
サーバーレス
AWS LambdaやGCP Cloud Runのようなサーバーレスソリューションにより動作させる方法。
サーバー環境を用意する必要がなく、既存の環境がない場合に手っ取り早い方法ではある。 ただし、LambdaもCloud Runも動作させるための手順が多く、あんまり手早く手軽にという感じはしない。 また、コスト予測が難しいのも難点。
プラットフォームの操作手順説明になってしまうため、本記事では解説しない。
実装
CGI
cgiライブラリ
古典的な形式であれば比較的扱いやすい。 レスポンスを処理するための機能がないため、手動でケアしなければならない。
レスポンスのための便利機能はいくつかあるが、使い勝手を考えると自作したほうがいい。
また、リクエストボディにアクセスするメソッドがないため、原始的にreadすることになる。
リクエストはurl form encodedにしておくほうが簡単。
大きな欠点として、GET
, HEAD
, POST
メソッドにしか対応していない。
require 'cgi'
require 'json'
def res(code=500, headers={}, body)
$stdout.printf("Status: %d\n", code)
.each do |k,v|
headers$stdout.printf("%s: %s\n", k, v)
end
$stdout.puts
$stdout.puts body
end
begin
= JSON.load($stdin.read)
data = CGI.new
cgi
200, {"Content-Type" => "text/plain"}, "Hello, #{data["name"]}")
res(rescue
resend
Rack CGI
生Rackを扱うことはなかなかないが、CGIの場合ルーティングを書く必要はないため、Rackで書くのはかなり有力。 ちなみに、Rackupに付属するハンドラは現在はCGIとWebrickの2種類しかない。
入力パラメータを処理する機能はRackにはないため、env["rack.input"]
で入力を扱う。
env
はHash
オブジェクトである。
require 'rubygems'
require 'bundler/setup'
require 'rack'
require 'rackup'
require 'rackup/handler/cgi'
require 'json'
class Hello
def call(env)
= JSON.load(env["rack.input"])
data [200, {"Content-Type" => "text/plain"}, ["Hello, #{data["name"]}"]]
end
end
Rackup::Handler::CGI.run Hello.new
rack
, rackup
, rackup/handler/cgi
の順にロードする必要があり、bundlerまで使うとrequire
が長い。
ちなみに、rack-contribにはRack::JSONBodyParser
というのがあったりするが、これはbody.rewind
する関係上、CGIでは動作しない。
.rewind if body.respond_to?(:rewind) # somebody might try to read this stream body
と一見ケアしているように見えるのだが、STDIN.respond_to?(:rewind)
は普通に真なので、ケアできていない。
Sinatra CGI
実はSinatraをCGIで使うことも可能。
ただ、いくら小さいとはいえ、CGIで使うことを想定しないSinatraを毎回起動することはパフォーマンスに影響はちょっとある。 とはいえ、本記事では基本的にそうした点は考慮しなくて良い想定をしている。
Sinatraを使うことでより入出力の扱いが楽になるというメリットがある。 とはいえ、Sinatraの最も重要なメリットであるルーティング機能は必要ない上に、SinatraはJSONボディの解釈はしないので、そのメリットは非常に薄い。
require 'rubygems'
require 'bundler/setup'
require 'rack'
require 'rackup'
require 'rackup/handler/cgi'
require 'sinatra'
require 'json'
'' do
post = JSON.parse request.body.read
data "Hello, #{data["name"]}"
end
Rackup::Handler::CGI.run Sinatra::Application
環境の構成
CGIを利用可能なウェブサーバー(例えばApache)を使っているのなら、CGIスクリプトを配置するだけである。 レンタルサーバーを使っている場合も同様。
私はLighttpdを使用している。 Lighttpdは少し設定が必要で
server.modules += ( "mod_cgi" )
cgi.assign = (
".cgi" => "/usr/bin/ruby",
".rb" => "/usr/bin/ruby"
)
といった設定を行う。
Apacheはプログラムを直接起動するためスクリプトには実行権限が必要だが、Lighttpdは拡張子に応じてLighttpdが処理系を呼び出す仕組みなので、実行権限がいらない。
一方で、Cなどで書いた場合はenv(1)
に渡すなど工夫が必要になる。
FastCGI
fcgiライブラリ
fcgiライブラリはcgiライブラリ互換のインターフェイスと固有のインターフェイスの2種類がある。 今どきcgiライブラリを使っているソフトウェアをfcgiライブラリへ移植することはほとんどないと思うが、「思いのほかアクセス数が多かった」というようなケースでパフォーマンスのために変更することは全く考えられないわけではない。
#each_cgi
を使う場合は、ブロックの中身はcgiと同じになるが、STDIN
/STDOUT
を直接使う方法が効かなくなる。
cgiライブラリはプライベートメソッドであるCGI#stdinput
, CGI#stdoutput
を使っている。
出力に関してはCGI#out
でアクセスする方法を採用できるが、リクエストボディを直接読みたい場合は$stdin
あるいはSTDIN
にアクセスするしかない。そして、それはJSONでのリクエストに対応する唯一の方法である。
これをFCGI#in
に変更するようなことも必要になってくる。
そのほか、CGIの場合は独立したプロセスとして起動されることを前提にすることで他の方法よりかなり楽になるという面もあるが、それを使えないため、cgiライブラリとの互換インターフェイスを使う必然性はまるでない。
#!/usr/bin/ruby
require 'rubygems'
require 'bundler/setup'
require 'fcgi'
require 'json'
FCGI.each do |fcgi|
begin
= fcgi.out
out
= JSON.load(fcgi.in)
data = ["Status: 200", "Content-Type: text/plain", "", "Hello, #{data["name"]}"]
res .puts res
outensure
.finish
fcgiend
end
FCGI#finish
を呼ばないと応答を完了してくれないので要注意。
環境の構成
Lighttpd
扱いやすいFastCGIモジュールを持っているため、Lighttpdはかなり適性が高い。 CGIを動かすためのコンテナとしても使えるため、FastCGIで動かす必要がある環境の場合はかなり有力な選択肢になる。
設定の必要箇所を抜き出すと次の通り
server.modules += ( "mod_fastcgi" )
fastcgi.server = (
"/fcgi1/" => ((
"socket" => "/path/to/fcgi.sock",
"bin-path" => "/path/to/fcgi.rb"
))
)
これは補足説明をしたほうがいいだろう。
まず/fcgi1/
の部分は、prefixがこれに一致する場合に当該FastCGIにルーティングされる。
これは、FastCGIスクリプトの所在には関係ない。
通常は拡張子を指定するが、/
から始めることでprefixパスを指定できる。
prefix指定にすることで、現実的に複数アプリを動かすことができる。
これだと/fcgi1/
以下はなんでもこのFastCGIにルーティングされそうだが、存在しないドキュメントを要求すると404
になってしまう。
このため、実際に当該パスにファイルが存在するようにしなければならない。これは、空のファイルで良い。
もっと良い方法があるが、それで動くので私は良しとしている。
bin-path
はFastCGIスクリプトのパスだが、そのファイルは実行可能である必要がある。
この値には引数を含めることができるため、/usr/bin/ruby
などに渡すことも可能である。
Nginx + spawn-fcgi
Nginxをリバースプロキシにしている場合、Nginx自体はFastCGIを起動する機能はなく、FastCGIデーモンに対してFastCGIプロトコルで接続しにいくモジュールがある。
このため、
spawn-fcgi -s <path/to/socket> <APP>
のようにしてデーモナイズし、
location /fcgi {
fastcgi_pass unix:/path/to/socket.sock;
fastcgi_index index.rb;
include fastcgi_params;
}
という感じでルーティングする。
そしてspawn-fcgi
をいい感じに実行するSystemdユニットを作るなどする。
Sinatra
Sinatraライブラリ
個人的に一番よく使う方法である。 私は以前はSinatraの推奨に従ってthinをサーバーとして使っていたが、Rackからthinのサポートが外れたため、Pumaを使うようになった。
#!/usr/bin/ruby
require 'rubygems'
require 'bundler/setup'
require 'sinatra/base'
require 'json'
require 'rack/contrib/json_body_parser'
class App < Sinatra::Base
:server, "puma"
set :bind, "localhost"
set :port, 8888
set
Rack::JSONBodyParser
use
"/index" do
post [200, {"Content-Type" => "text/plain"}, "Hello, #{params["name"]}"]
end
end
App.run!
CGIの場合と違い、Rack::JSONBodyParser
を使っている。
ただ、依存関係をひとつ増やしてまでやるかは微妙。
環境の構成
bundlerを使う場合はこんな感じ。
bundle init
bundle config set path vendor/bundle
bundle add sinatra puma rack rack-contrib
Sinatraを使うRubyスクリプトそのものがアプリケーションサーバープロセスとして起動される。 このため、ここからすべきことは
- Nginxなどフロントサーバーから転送を設定する
- SystemdなどでRubyスクリプトが自動起動/再起動管理されるようにする
WEBrick
webrickライブラリ
Webrickの機能は多く、使いこなそうとすると固有の知識が結構必要になる。
ただし、Rack applicationのような感覚で使いたい場合は、基本的にWEBrick::HTTPServer#mount_proc
を使うだけで良い。
ブロックを渡すこともできるし、第2引数としてProc
オブジェクトを渡すこともできる。
引数はreq, res
である。
パラメータ読み込みはWEBrick::HTTPRequest#body
とWEBrick::HTTPRequest#query
を使う。
パスはWEBrick::HTTPRequest#path
で読める。
ボディパラメータをHash
にする機能や、パスパラメータをHash
にする機能はない。
require 'webrick'
require 'json'
= WEBrick::HTTPServer.new({
srv BindAddress: '127.0.0.1',
Port: 8888})
.mount_proc("/foo") do |req, res|
srvbegin
= JSON.load(req.body)
params
.content_type = "text/plain"
res.body = "Hello, #{params["name"]}"
resrescue
.status = 500
resend
end
trap("INT"){ srv.shutdown }
.start srv
環境の構成
bundlerを使う場合は
bundle init
bundle config set path vendor/bundle
bundle add webrick
使わない場合は
gem install webrick
WEBrickを使うスクリプトはそれ自体がアプリケーションサーバーとなる。
Sinatraを使う場合同様、リバースプロキシの設定と自動起動の設定を行うだけである。