Rails 5 (非beta) + Heroku で作る、無料APIサーバ

この記事は、以下のブログ、プロジェクトのまとめです。

- [Rails 5 (非beta) でつくる、APIサーバ](http://blog.dogwood008.com/entry/rails5_api_server)
- [RailsプロジェクトをHerokuへデプロイし、無料ホスティングしてもらう](http://blog.dogwood008.com/entry/deploy_a_rails_project_to_heroku)
- [github: dogwood008/fetch_hatena_bookmark_and_post_to_hatena_blog](https://github.com/dogwood008/fetch_hatena_bookmark_and_post_to_hatena_blog)

# Rails 5でのプロジェクト作成

## Rails 5の使用

まずは準備。

```bash
$ mkdir fetch_rss_and_post_hateblog
$ cd fetch_rss_and_post_hateblog
$ echo "gem 'rails', '~>5.0'" > Gemfile
$ mkdir .bundle
$ echo '---                                                                                                                  
BUNDLE_PATH: vendor/bundle
BUNDLE_DISABLE_SHARED_GEMS: 1
BUNDLE_BIN: ./.bin' > .bundle/config
$ sudo yum install -y sqlite-devel # 必要に応じてapt-getなど
$ bundle install
```

プロジェクトをセットアップ。

```bash
$ rails new . --api
$ echo "development:
  secret_key_base: `rake secret`

test:
  secret_key_base: `rake secret`

production:
  secret_key_base: <%= ENV[\"SECRET_KEY_BASE\"] %>" > config/secrets.yml
```

ここまでの関係ファイルを一旦プッシュ。

```bash
$ gibo osx ruby rails vim >> .gitignore
$ git init
$ git remote add origin git://github.com/user/repo.git
$ git push origin master
```

## APIサーバの作成

RSSを取ってきて、title, URL, descriptionを返すAPIサーバを作ってみます。

```bash
$ rails g controller post
```

APIモードで作られているか確認してみる。

```bash

$ cd app
$ ls
channels  controllers  jobs  mailers  views
```

RSSフィードを取ってきて、JSONで表示してみます。

はてブのRSSフィードを `RSS:Parser#parse` で取れない件は、 [はてなのAPIを使う際に503エラーが出る問題と解決策](http://qiita.com/takuya-andou/items/97a68bdae1b7050d3fa5)を参考にしました。

```ruby:app/controllers/posts_controller.rb
def fetch
  url = 'http://b.hatena.ne.jp/dogwood008/rss'
  ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'
  rss = open(url, 'User-Agent' => ua)
  feed = RSS::Parser.parse(rss)
  rss.close
  items = feed.items.map do |i|
    { link: i.link, title: i.title, desc: i.description, date: i.dc_date }
  end
  render json: items
end
```


```ruby:config/routes.rb
Rails.application.routes.draw do
  get '/fetch', to: 'posts#fetch'
end
```

いざ、実行。

```bash
$ rails s -b 0.0.0.0
```

`http://localhost:3000/posts/fetch`

```json
[{
  link: "http://brevis.exblog.jp/24515937/",
  title: "トラブルには技術的原因と、マネジメント的原因がある : タイム・コンサルタントの日誌から",
  desc: "【要約と私感】“マネジメント的原因” = 仕組みの原因。手っ取り早く直すには技術的要因を取り除けば良いけど、仕組みがダメならまた似たような失敗をうむ。",
  date: "2016-07-10T15:33:53.000+09:00"
},
  {
  link: "http://qiita.com/teradonburi/items/a382a17e1e0245b7d831",
  title: "AWS料金早見表（サーバレスアーキテクチャ） - Qiita",
  desc: "一度自分で一通りやってみたい感ある。",
  date: "2016-07-10T15:27:49.000+09:00"
}, ...]
```

ヒューッ！！
その他いろいろ作ります。

# Herokuへのデプロイ

[Getting Started with Rails 5.x on Heroku](https://devcenter.heroku.com/articles/getting-started-with-rails5)を参考に進めていきます。

## 準備

herokuは PostgreSQL を推奨しているので、それに合わせる。

```ruby:Gemfile
group :production do
  gem 'pg'
end
```

heroku toolbeltをインストール。

```bash
$ wget -O- https://toolbelt.heroku.com/install.sh | sh
$ echo 'PATH="/usr/local/heroku/bin:$PATH"' >> ~/.bash_profile
$ source ~/.bash_profile
```

ログインします。予めアカウントは[Web](https://www.heroku.com/)から作っておく。

```bash
$ heroku login
heroku-cli: Installing CLI... 21.83MB/21.83MB
Enter your Heroku credentials.
Email: email@example.com
Password (typing will be hidden):
Logged in as email@example.com
```

## アプリケーションを作成～デプロイ

```bash
$ heroku create
Heroku CLI submits usage information back to Heroku. If you would like to disable this, set `skip_analytics: true` in /home/kit/.heroku/config.json
Creating app... done, ⬢ XXXXX-YYYYY-12345
https://XXXXX-YYYYY-12345.herokuapp.com/ | https://git.heroku.com/whispering-YYYYY-12345.git
```

origin の他に、リモートリポジトリ `heroku` が追加されます。

```bash
$ git remote show heroku
* remote heroku
  Fetch URL: https://git.heroku.com/XXXXX-YYYYY-12345.git
  Push  URL: https://git.heroku.com/XXXXX-YYYYY-12345.git
  HEAD branch: (unknown)
```

早速、 `Heroku` へプッシュしてみます。

```bash
$ git push heroku master
Counting objects: 9, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 828 bytes | 0 bytes/s, done.
Total 6 (delta 4), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Ruby app detected
remote: -----> Compiling Ruby/Rails
remote: -----> Using Ruby version: ruby-2.3.0
remote: ###### WARNING:
remote:        You have the `.bundle/config` file checked into your repository
remote:        It contains local state like the location of the installed bundle
remote:        as well as configured git local gems, and other settings that should
remote:        not be shared between multiple checkouts of a single repo. Please
remote:        remove the `.bundle/` folder from your repo and add it to your `.gitignore` file.
remote:        https://devcenter.heroku.com/articles/bundler-configuration
remote:
remote: -----> Installing dependencies using bundler 1.11.2
remote:        Running: bundle install --without development:test --path vendor/bundle --binstubs vendor/bundle/bin -j4 --deployment
remote:        Fetching gem metadata from https://rubygems.org/..........
remote:        Fetching version metadata from https://rubygems.org/...
remote:        Fetching dependency metadata from https://rubygems.org/..
remote:        Installing rake 11.2.2
remote:        Installing concurrent-ruby 1.0.2
remote:        Installing i18n 0.7.0
remote:        Installing thread_safe 0.3.5
remote:        Installing minitest 5.9.0
remote:        Installing builder 3.2.2
remote:        Installing erubis 2.7.0
remote:        Installing mini_portile2 2.1.0
remote:        Installing pkg-config 1.1.7
remote:        Installing nio4r 1.2.1 with native extensions
remote:        Installing rack 2.0.1
remote:        Installing websocket-extensions 0.1.2
remote:        Installing arel 7.0.0
remote:        Installing mime-types-data 3.2016.0521
remote:        Installing method_source 0.8.2
remote:        Using bundler 1.11.2
remote:        Installing puma 3.4.0 with native extensions
remote:        Installing thor 0.19.1
remote:        Installing tzinfo 1.2.2
remote:        Installing nokogiri 1.6.8 with native extensions
remote:        Installing websocket-driver 0.6.4 with native extensions
remote:        Installing rack-test 0.6.3
remote:        Installing sprockets 3.6.3
remote:        Installing mime-types 3.1
remote:        Installing activesupport 5.0.0
remote:        Installing mail 2.6.4
remote:        Installing globalid 0.3.6
remote:        Installing activemodel 5.0.0
remote:        Installing activejob 5.0.0
remote:        Installing activerecord 5.0.0
remote:        Installing rails-dom-testing 2.0.1
remote:        Installing loofah 2.0.3
remote:        Installing rails-html-sanitizer 1.0.3
remote:        Installing actionview 5.0.0
remote:        Installing actionpack 5.0.0
remote:        Installing actionmailer 5.0.0
remote:        Installing actioncable 5.0.0
remote:        Installing railties 5.0.0
remote:        Installing sprockets-rails 3.1.1
remote:        Installing rails 5.0.0
remote:        Installing pg 0.18.4 with native extensions                                                                           
remote:        Bundle complete! 9 Gemfile dependencies, 41 gems now installed. 
remote:        Gems in the groups development and test were not installed.
remote:        Bundled gems are installed into ./vendor/bundle.
remote:        Bundle completed (19.95s)
remote:        Cleaning up the bundler cache.
remote:
remote: ###### WARNING:
remote:        You have the `.bundle/config` file checked into your repository
remote:        It contains local state like the location of the installed bundle
remote:        as well as configured git local gems, and other settings that should
remote:        not be shared between multiple checkouts of a single repo. Please
remote:        remove the `.bundle/` folder from your repo and add it to your `.gitignore` file.
remote:        https://devcenter.heroku.com/articles/bundler-configuration
remote:
remote: ###### WARNING:
remote:        No Procfile detected, using the default web server.
remote:        We recommend explicitly declaring how to boot your server process via a Procfile.
remote:        https://devcenter.heroku.com/articles/ruby-default-web-server
remote:
remote: -----> Discovering process types
remote:        Procfile declares types     -> (none)
remote:        Default types for buildpack -> console, rake, web, worker
remote:
remote: -----> Compressing...
remote:        Done: 23.1M
remote: -----> Launching...
remote:        Released v6
remote:        https://XXXXX-YYYYY-12345.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/XXXXX-YYYYY-12345.git
 * [new branch]      master -> master
```

途中で、 `sqlite3.h is missing` みたいなメッセージが出たり、 「詳しくは `https://devcenter.heroku.com/articles/sqlite3` を見てね」的なメッセージが出たなら、 `Gemfile` の `group :development` の中に `gem 'sqlite'` を押し込めましょう。

```ruby:Gemfile
group :development do
  # Use sqlite3 as the database for Active Record
  gem 'sqlite3'
end
```

必要に応じて、DBのマイグレーションを行います。

```bash
$ heroku run rake db:migrate
Running rake db:migrate on ⬢ XXXXX-YYYYY-12345... up, run.2785                                                               
D, [2016-07-12T16:25:40.173919 #3] DEBUG -- :    (11.2ms)  CREATE TABLE "schema_migrations" ("version" character varying PRIMARY KEY)
D, [2016-07-12T16:25:40.187941 #3] DEBUG -- :    (9.3ms)  CREATE TABLE "ar_internal_metadata" ("key" character varying PRIMARY KEY, "value" character varying, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL)
D, [2016-07-12T16:25:40.190308 #3] DEBUG -- :    (1.0ms)  SELECT pg_try_advisory_lock(1753797496500759360);
D, [2016-07-12T16:25:40.200901 #3] DEBUG -- :   ActiveRecord::SchemaMigration Load (1.1ms)  SELECT "schema_migrations".* FROM "schema_migrations"
I, [2016-07-12T16:25:40.206248 #3]  INFO -- : Migrating to CreatePosts (20160710102422)
D, [2016-07-12T16:25:40.208344 #3] DEBUG -- :    (0.7ms)  BEGIN
== 20160710102422 CreatePosts: migrating ======================================
-- create_table(:posts)
D, [2016-07-12T16:25:40.216973 #3] DEBUG -- :    (7.4ms)  CREATE TABLE "posts" ("id" serial primary key, "title" character varying, "url" character varying, "comment" character varying, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL)
   -> 0.0085s
== 20160710102422 CreatePosts: migrated (0.0087s) =============================

D, [2016-07-12T16:25:40.224358 #3] DEBUG -- :   SQL (1.1ms)  INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version"  [["version", "20160710102422"]]
D, [2016-07-12T16:25:40.227280 #3] DEBUG -- :    (2.6ms)  COMMIT
I, [2016-07-12T16:25:40.227414 #3]  INFO -- : Migrating to AddDateToPost (20160710152551)
D, [2016-07-12T16:25:40.228671 #3] DEBUG -- :    (0.7ms)  BEGIN
== 20160710152551 AddDateToPost: migrating ====================================
-- add_column(:posts, :date, :datetime)
D, [2016-07-12T16:25:40.230074 #3] DEBUG -- :    (1.0ms)  ALTER TABLE "posts" ADD "date" timestamp
   -> 0.0013s
== 20160710152551 AddDateToPost: migrated (0.0014s) ===========================

D, [2016-07-12T16:25:40.231584 #3] DEBUG -- :   SQL (0.8ms)  INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version"  [["version", "20160710152551"]]
D, [2016-07-12T16:25:40.233287 #3] DEBUG -- :    (1.5ms)  COMMIT
D, [2016-07-12T16:25:40.240319 #3] DEBUG -- :   ActiveRecord::InternalMetadata Load (0.9ms)  SELECT  "ar_internal_metadata".* FROM "ar_internal_metadata" WHERE "ar_internal_metadata"."key" = $1 LIMIT $2  [["key", :environment], ["LIMIT", 1]]
D, [2016-07-12T16:25:40.248994 #3] DEBUG -- :    (0.7ms)  BEGIN
D, [2016-07-12T16:25:40.251805 #3] DEBUG -- :   SQL (1.0ms)  INSERT INTO "ar_internal_metadata" ("key", "value", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "key"  [["key", "environment"], ["value", "production"], ["created_at", 2016-07-12 16:25:40 +0000], ["updated_at", 2016-07-12 16:25:40 +0000]]
D, [2016-07-12T16:25:40.253633 #3] DEBUG -- :    (1.5ms)  COMMIT
D, [2016-07-12T16:25:40.254643 #3] DEBUG -- :    (0.8ms)  SELECT pg_advisory_unlock(1753797496500759360)
```

これでおしまい。うまくいっていれば、ログ中に出てきたhttp://~.herokuapp.comでアクセスできる。

## デバッグ

### プロセス状態

`heroku ps` で現在の状態を見ることができる。

```bash
$ heroku ps
Free dyno hours quota remaining this month: 1000h 0m (100%)
For more information on dyno sleeping and how to upgrade, see:
https://devcenter.heroku.com/articles/dyno-sleeping

=== web (Free): bin/rails server -p $PORT -e $RAILS_ENV (1)
web.1: up 2016/07/13 01:25:02 +0900 (~ 2m ago)
```

### ログ

ログを見るなら `heroku logs` 。 `--tail` オプションを付けると、リアルタイムで見られる。

```bash
$ heroku logs
$ heroku logs --tail
```

### 環境変数

```bash
# 環境変数の一覧
$ heroku config
=== XXXXX-YYYYY-12345 Config Vars
DATABASE_URL:             postgres://hogehogehoge:foobarbaz@ec2-NNN-NNN-NNN-NNN.compute-M.amazonaws.com:PPPP/fugapiyo
LANG:                     en_US.UTF-8
RACK_ENV:                 production
RAILS_ENV:                production
RAILS_LOG_TO_STDOUT:      enabled
RAILS_SERVE_STATIC_FILES: enabled
SECRET_KEY_BASE:          foobarbazhogefugapiyo
```

```bash
# 環境変数のうち、指定したものだけを表示
$ heroku config:get LANG                                                              
en_US.UTF-8
```

```bash
# 環境変数をセット
$ heroku config:set SECRET_KEY_BASE=`rake secret`                                     
Setting SECRET_KEY_BASE and restarting ⬢ XXXXX-YYYYY-12345... done, v8                                                       
SECRET_KEY_BASE: hogefugapiyofoobarbaz
```

### コンソール

```bash
$ heroku run rails console
Running rails console on ⬢ XXXXX-YYYYY-12345... up, run.4519
Loading production environment (Rails 5.0.0)
irb(main):001:0> 
```

### サーバの再起動

```bash
$ heroku restart
Restarting dynos on ⬢ XXXXX-YYYYY-12345... done
```

## Heroku 無料枠の範囲

1つのサーバプロセスを立ち上げると、 `1 dyno` を使用するということになります。 `1 dyno` を1時間使用すると、 `1 dyno hour` を消費したことになります。

各アカウントに対し `550 dyno hours` を、有効なクレジットカードを heroku に登録していれば、さらに `450 dyno hours（計 1000 dyno hours）` を一月に付与されます。

31日間の可動に必要な `dyno hours` は単純計算で `744 dyno hours (= 24 hours * 31 days)` ですが、 `heroku run console` や `heroku run rake db:migrate` などのコマンドを実行する `One-off Dyno` と呼ばれる `Dyno` の実行も、利用量に含まれるので注意。

以前は「18時間稼働＆6時間スリープ」のような制限がありましたが、今は上記の制限に代わりました。

### 参考

- [知っておきたい！Herokuを使う上では当たり前？の16の常識](http://blog.mah-lab.com/2013/05/16/heroku-commons-16/)
- [Herokuの無料枠の料金体系が2016年6月1日から変わった](http://tdenc.com/blog/2016/06/03/heroku-new-free-dyno/)
- [Flexible Free Dyno Hours](https://blog.heroku.com/announcing_heroku_free_ssl_beta_and_flexible_dyno_hours#flexible-free-dyno-hours)