Chienomi

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)
  headers.each do |k,v|
    $stdout.printf("%s: %s\n", k, v)
  end
  $stdout.puts
  $stdout.puts body
end

begin
  data = JSON.load($stdin.read)
  cgi = CGI.new

  res(200, {"Content-Type" => "text/plain"}, "Hello, #{data["name"]}")
rescue
  res
end

Rack CGI

生Rackを扱うことはなかなかないが、CGIの場合ルーティングを書く必要はないため、Rackで書くのはかなり有力。 ちなみに、Rackupに付属するハンドラは現在はCGIとWebrickの2種類しかない。

入力パラメータを処理する機能はRackにはないため、env["rack.input"]で入力を扱う。 envHashオブジェクトである。

require 'rubygems'
require 'bundler/setup'
require 'rack'
require 'rackup'
require 'rackup/handler/cgi'
require 'json'

class Hello
  def call(env)
    data = JSON.load(env["rack.input"])
    [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では動作しない。

body.rewind if body.respond_to?() # somebody might try to read this stream

と一見ケアしているように見えるのだが、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'

post '' do
  data = JSON.parse request.body.read
  "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
    out = fcgi.out

    data = JSON.load(fcgi.in)
    res = ["Status: 200", "Content-Type: text/plain", "", "Hello, #{data["name"]}"]
    out.puts res
  ensure
    fcgi.finish
  end
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
  set , "puma"
  set , "localhost"
  set , 8888

  use Rack::JSONBodyParser

  post "/index" do
    [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#bodyWEBrick::HTTPRequest#queryを使う。 パスはWEBrick::HTTPRequest#pathで読める。 ボディパラメータをHashにする機能や、パスパラメータをHashにする機能はない。

require 'webrick'
require 'json'

srv = WEBrick::HTTPServer.new({
  BindAddress: '127.0.0.1',
  Port: 8888})

srv.mount_proc("/foo") do |req, res|
  begin
    params = JSON.load(req.body)

    res.content_type = "text/plain"
    res.body = "Hello, #{params["name"]}"
  rescue
    res.status = 500
  end
end

trap("INT"){ srv.shutdown }
srv.start

環境の構成

bundlerを使う場合は

bundle init
bundle config set path vendor/bundle
bundle add webrick

使わない場合は

gem install webrick

WEBrickを使うスクリプトはそれ自体がアプリケーションサーバーとなる。

Sinatraを使う場合同様、リバースプロキシの設定と自動起動の設定を行うだけである。