SHOYAN BLOG

I am a pragmatic programmer.

RACK_ENVとUnicorn、SinatraでのRACK_ENVの扱いと注意点

RACK_ENVについて調べた内容とUnicorn、SinatraでのRACK_ENVの扱い方や注意点をまとめました。
RACK_ENVとはRACKの環境変数です。
何を設定するかによって使用するmiddlewareが変わります。
RACK_ENVには、developmentdeploymentnoneの3種類があります。

以下がそれぞれのRACK_ENVで使われるmiddlewareです。

1
2
3
#       - development: CommonLogger, ShowExceptions, and Lint
#       - deployment: CommonLogger
#       - none: なし

https://github.com/rack/rack/blob/028438ffffd95ce1f6197d38c04fa5ea6a034a85/lib/rack/server.rb#L157

developmentdeploymentに該当しない場合はmiddlewareは何も使われないようです。

https://github.com/rack/rack/blob/028438ffffd95ce1f6197d38c04fa5ea6a034a85/lib/rack/server.rb#L228

ここを間違えてproductionと設定する例を時々見るのですが、productionは存在しないので、noneと同じ挙動となります。
capistrano3-unicornのソースコードを見てみると、productionを指定した場合deploymentを設定するようになっていました。

1
set :unicorn_rack_env, -> { fetch(:rails_env) == "development" ? "development" : "deployment" }

https://github.com/tablexi/capistrano3-unicorn/blob/master/lib/capistrano3/tasks/unicorn.rake#L7

unicornはちょっと挙動を変えていて、developmentdeploymentのときは ShowExceptionsとLintは読み込まないようです。

1
2
3
4
5
6
7
8
  case ENV["RACK_ENV"]
  when "development"
  when "deployment"
    middleware.delete(:ShowExceptions)
    middleware.delete(:Lint)
  else
    return inner_app
  end

https://github.com/defunkt/unicorn/blob/master/lib/unicorn.rb#L79

Sinatraの環境変数

SinatraはRACK_ENVとenvironmentという環境変数が2つありややこしいです。
基本的には、environmentを定義して使います。
異なる環境で実行したい場合、RACK_ENVを指定することができます(これが混乱のもと)。

1
2
異なる環境を走らせるには、RACK_ENV環境変数を設定します。
RACK_ENV=production ruby my_app.rb

https://github.com/sinatra/sinatra/blob/4c7d38eb1b2cc02ce51029f28e0c3c34ca12ccfd/README.ja.md#%E7%92%B0%E5%A2%83%E8%A8%AD%E5%AE%9Aenvironments

set :environment, :production と定義して、RACK_ENV=development ruby my_app.rb を実行してみました。
このときの settings.production?の戻り値は何になるでしょうか。
trueとなり、productionと判定されます。
set :environmentで指定されたもので判定されるようです。

まとめると以下のような挙動になります。

  • set :environment が指定されているときは指定された値が使われる
  • set :environment が指定されていないときは RACK_ENVが使われる
  • set :environment も RACK_ENVも指定されていないときは developmentになる

ここで1つ気になることがでてきました。
capistrano3-unicornを使った場合は、RACK_ENVはdeploymentが指定されます。
Sinatraで set :environment を設定しなかった場合はRACK_ENVの値が優先されます。
ということは、本番環境でもsettings.production?がfalseとなってしまいます。
これは意図しない挙動ですね。
また、テンプレートのキャッシュについても効かなくなってしまいます。

まとめ

Sinatraではset :environment を明示的に指定しましょう。

1
set :environment, ENV["RACK_ENV"] == "deployment"? :production : ENV["RACK_ENV"].to_sym

参考文献

LaravelをMacにインストールして起動する手順

PHPで人気のフレームワーク、Laravelを手元のPCにインストールして起動するチュートリアルです。
冗長な説明を除きLaravelを動かすために重要な部分のみ解説することでスピーディにLaravelを動作できるようにしています。

Laravelのインストール

まずはcomposerでlaravelコマンドをインストールします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
  composer global require "laravel/installer"
Changed current directory to /Users/PMAC025S/.composer

Using version ^1.3 for laravel/installer
./composer.json has been created
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing symfony/process (v3.0.4)
    Downloading: 100%

  - Installing symfony/polyfill-mbstring (v1.1.1)
    Loading from cache

  - Installing symfony/console (v3.0.4)
    Downloading: 100%

  - Installing guzzlehttp/promises (1.1.0)
    Downloading: 100%

  - Installing psr/http-message (1.0)
    Loading from cache

  - Installing guzzlehttp/psr7 (1.3.0)
    Downloading: 100%

  - Installing guzzlehttp/guzzle (6.2.0)
    Downloading: 100%

  - Installing laravel/installer (v1.3.3)
    Downloading: 100%

symfony/console suggests installing symfony/event-dispatcher ()
symfony/console suggests installing psr/log (For using the console logger)
Writing lock file
Generating autoload files

.bashrc.zshrc にパスを追加します。

1
export PATH="$PATH:/Users/PMAC025S/.composer/vendor/bin"

これでlaravelコマンドが実行できるようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  laravel
Laravel Installer version 1.3.3

Usage:
  command [options] [arguments]

Options:
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Available commands:
  help  Displays help for a command
  list  Lists commands
  new   Create a new Laravel application.

laravel new application_name でインストールします。

1
2
  laravel new blog
Crafting application...

以下の構成で作成されています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  tree -L 1 blog
blog
├── app
├── artisan
├── bootstrap
├── composer.json
├── composer.lock
├── config
├── database
├── gulpfile.js
├── package.json
├── phpunit.xml
├── public
├── readme.md
├── resources
├── server.php
├── storage
├── tests
└── vendor

設定

設定情報はconfigディレクトリに保存します。
今回は設定を変更しません。

データベースを準備する

### データベースのマイグレーション

Laravelに同梱されているartisanというツールを使って作成します。

1
2
  php artisan make:migration create_tasks_table --create=tasks
Created Migration: 2016_04_28_011840_create_tasks_table

database/migrationsディレクトリに作成されます。
2016_04_28_011840_の部分はartisanが作成するので、makeするタイミングによって変わります。

2016_04_28_011840_create_tasks_table.phpを編集して、nameカラムを追加します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTasksTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('tasks');
    }
}

.envファイルのデータベースの設定を自分のローカルのPCのmysqlに変更します。
デフォルトでは、HomesteadというLaravel開発用の仮想環境の設定です(今回は使いません)。

1
2
3
4
5
6
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=root
DB_PASSWORD=

マイグレーションを実行します。

1
2
3
4
5
  php artisan migrate
Migration table created successfully.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table
Migrated: 2016_04_28_011840_create_tasks_table

モデルを作成する

LaravelはEloquentというORMがデフォルトで使われるようになっています。

artisanでモデルを作成します。

1
2
  php artisan make:model Task
Model created successfully.

モデルは app ディレクトリ配下に作成されます。

ルーティング

ルーティングの設定は、app/Http/routes.phpに定義します。
(ルーティングの設定はsinatraっぽい)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

use App\Task;
use Illuminate\Http\Request;

/**
 * Show Task Dashboard
 */
Route::get('/', function () {
    return view('tasks');
});

/**
 * Add New Task
 */
Route::post('/task', function (Request $request) {
    //
});

/**
 * Delete Task
 */
Route::delete('/task/{task}', function (Task $task) {
    //
});

Viewの設定

LarvelはデフォルトでBladeというテンプレートエンジンを使います。
viewのディレクトリはresources/viewsです。

1
$ mkdir resources/views/layouts

全体のレイアウトのテンプレートとして使うapp.blade.phpを作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 <!-- resources/views/layouts/app.blade.php -->

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Laravel Quickstart - Basic</title>

        <!-- CSS And JavaScript -->
    </head>

    <body>
        <div class="container">
            <nav class="navbar navbar-default">
                <!-- Navbar Contents -->
            </nav>
        </div>

        @yield('content')
    </body>
</html>

tasks.blade.phpを作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 <!-- resources/views/tasks.blade.php -->

@extends('layouts.app')

@section('content')

    <!-- Bootstrap Boilerplate... -->

    <div class="panel-body">
        <!-- Display Validation Errors -->

        <!-- New Task Form -->
        <form action="" method="POST" class="form-horizontal">
            {!! csrf_field() !!}

            <!-- Task Name -->
            <div class="form-group">
                <label for="task" class="col-sm-3 control-label">Task</label>

                <div class="col-sm-6">
                    <input type="text" name="name" id="task-name" class="form-control">
                </div>
            </div>

            <!-- Add Task Button -->
            <div class="form-group">
                <div class="col-sm-offset-3 col-sm-6">
                    <button type="submit" class="btn btn-default">
                        <i class="fa fa-plus"></i> Add Task
                    </button>
                </div>
            </div>
        </form>
    </div>

    <!-- TODO: Current Tasks -->
@endsection

サーバーを起動する

さて、一通りの設定が整いました。
サーバーを起動してブラウザで確認してみましょう。
以下のコマンドでサーバーを起動します。

1
2
  php artisan serve
Laravel development server started on http://localhost:8000/

http://localhost:8000/ にブラウザでアクセスします。

Taskという文字とフォームが表示されれば成功です!

参考文献

Webサイトのメタタグを取得するsite-parserをつくった

Webサイトのメタデータを抽出するツールを作成しました。
https://github.com/shoyan/site-parser

使い方

  1. git clone git@github.com:shoyan/site-parser.git
  2. cd site-parser
  3. bundle install
  4. site.csv をテキストエディタで編集
  5. ruby site-parser.rb

site.csv にパースしたいURLを記入して ruby site-parse.rb とすればメタデータが表示されます。

1
2
3
4
5
6
7
8
9
10
11
$ ruby site-parser.rb
"http://www.yahoo.co.jp/"
[
    [0] {
                             "url" => "http://www.yahoo.co.jp/",
                           "title" => "Yahoo! JAPAN",
                     "description" => "日本最大級のポータルサイト。検索、オークション、ニュース、メール、コミュニティ、ショッピング、など80以上のサービスを展開。あなたの生活をより豊かにする「ライフ・エンジ ン」を目指していきます。",
                          "robots" => "noodp",
        "google-site-verification" => "fsLMOiigp5fIpCDMEVodQnQC7jIY1K3UXW5QkQcBmVs"
    }
]

また、サーバーを起動してAPIとして利用することもできます。

サーバーを起動

1
2
3
4
5
$ ruby server.rb
== Sinatra (v1.4.7) has taken the stage on 4567 for development with backup from Thin
Thin web server (v1.6.4 codename Gob Bluth)
Maximum connections set to 1024
Listening on 0.0.0.0:4567, CTRL+C to stop

APIを実行

1
2
$ curl http://localhost:4567 -X POST -d "url=http://www.yahoo.co.jp/"
=> {"url":"http://www.yahoo.co.jp/","title":"Yahoo! JAPAN","description":"日本最大級のポータルサイト。検索、オークション、ニュース、メール、コミュニティ、ショッピング、など80以上のサービスを展開。あなたの生活をより豊かにする「ライフ・エンジン」を目指していきます。","robots":"noodp","google-site-verification":"fsLMOiigp5fIpCDMEVodQnQC7jIY1K3UXW5QkQcBmVs"}

その他

内部的にはNokogiriを使ってhtmlをパースしています。

文字化けして内容がみれない場合があったので、以下のワークアラウンドをいれました。

1
2
html = URI.parse(url).read
node = Nokogiri::HTML(html.toutf8, nil, 'utf-8')

参考資料

以下が参考になりました。ありがとうございます。

Sinatraのレスポンスの設定とストリーミングヘルパー

通常はルーティングブロックの戻り値にセットした文字列がbodyにセットされます。
任意の評価フローでbodyをセットしたい場合はどうすればいいでしょうか。
Sinatraには任意の評価フローでbodyをセットできる、bodyヘルパーが用意されています。

1
2
3
4
5
6
7
get '/foo' do
  body "bar"
end

after do
  puts body
end

Status Codeを設定するstatusヘルパー、headerを設定するheadersヘルパーも用意されています。

1
2
3
4
5
6
7
get '/foo' do
  status 418
  headers \
    "Allow"   => "BREW, POST, GET, PROPFIND, WHEN",
    "Refresh" => "Refresh: 20; http://www.ietf.org/rfc/rfc2324.txt"
  body "I'm a tea pot!"
end

引数の伴わないbody, status, headersは現在の値を確認するために使えます。

Streamingヘルパー

streamヘルパーを使うとおもしろいことができます。
レスポンスボディの部分を生成している段階でデータを送信することができます。
この仕組みを使ってストリーミングAPIを実装することもできます。

1
2
3
4
5
6
7
8
9
get '/' do
  stream do |out|
    out << "It's gonna be legen -\n"
    sleep 0.5
    out << " (wait for it) \n"
    sleep 1
    out << "- dary!\n"
  end
end

以下は、ストリーミングAPIのサンプルです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# app.rb
# long polling

set :server, :thin
connections = []

get '/subscribe' do
  # register a client's interest in server events
  stream(:keep_open) do |out|
    connections << out
    # purge dead connections
    connections.reject!(&:closed?)
  end
end

post '/message' do
  connections.each do |out|
    # notify client that a new message has arrived
    out << params['message'] << "\n"

    # indicate client to connect again
    out.close
  end

  # acknowledge
  "message received"
end

ストリーミングはThinやRainbowsなどのイベント型サーバーでないと動かないので注意が必要です。

サーバーを起動させてみます。

1
2
$ gem insatall thin
$ ruby app.rb

別のターミナルを開いて、以下を実行します。

1
$ curl http://localhost:4567/subscribe

何もレスポンスがありませんが、接続が維持された状態となります。

別のタブを開いて以下の実行してみましょう。

1
$ curl http://localhost:4567/message -X POST -d "message=hello"

すると、subscribeのほうにhelloと表示されて接続が切れます。
これは、/messageのout.closeを実行しているからです。
試しに、out.closeをコメントアウトしてサーバーを起動すると接続維持されたままになります。

Rubyでhttp通信をするhttp Gem

rubyでhttp通信をする際に便利なhttp gemというものがあるので紹介します。
https://github.com/httprb/http

Install

1
$ gem install http

getしてみる

1
2
3
4
5
6
7
$ pry
$ require http'
$ response = HTTP.get("https://github.com")
$ response.status
=> 200
$ response.body.to_s
=> "<html><head><meta http-equiv=\"content-type\" content=..."

http メソッドを指定する

以下のように指定します。

1
2
3
4
5
>> response = HTTP.post('https://restapi.com/objects')
>> response = HTTP.put('https://restapi.com/put')
>> response = HTTP.patch('https://restapi.com/put')
>> response = HTTP.delete('https://restapi.com/put')
>> response = HTTP.head('https://restapi.com/put')

クエリストリングを指定する

以下のように paramsオプションで指定します。

1
2
3
4
5
6
$ get = HTTP.get("http://example.com/resource", :params => {:foo => "bar"})
=> "#<HTTP::Response/1.1 404 Not …"

# 以下のように生成されたuriを確認できます
$ get.uri
=> #<HTTP::URI:0x007fce62a33aa8 URI:http://example.com/resource?foo=bar>

bodyを指定する

formオプションに設定します。
URLエンコードされたパラメーターが設定されます。

1
HTTP.post("http://example.com/resource", :form => {:foo => "42"})

生データを渡したいときは、bodyオプションを使います。

1
HTTP.post("http://example.com/resource", :body => "foo=42&bar=baz")

ネストしたbodyの渡し方

ネストした配列を渡したい場合はjsonを使って以下のように指定できます。

1
HTTP.put("http://example.com/resource", :params => {foo: "A"}, json: { bar: { hoge: "ok!"} })

Basic認証に対応する

以下のように設定ができます。

1
http = HTTP.basic_auth(user: 'user_name', pass: 'password')

SSL証明書の検証を無効にする

開発環境の場合などでSSL証明書の検証を無視したい場合は以下のように設定できます。

1
2
3
ctx = OpenSSL::SSL::SSLContext.new
ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
HTTP.get("https://example.com", :ssl_context => ctx)