オープンソースのPaaSソフトウェア CloudFoundry の技術情報やイベント告知などを掲載します

2015-07-29

Feedbin を Cloud Foundry で動かす

「Cloud Foundry 百日行」第39日目は,Ruby-on-Rails ベースのフィード・リーダー Feedbin です。今個人的にフィード・リーダーを探していて,その一環で試してみたプロダクトです。 Alternativeto.net においても,同種のオープンソース・ソフトウェアの中ではかなり人気が高く,実際動作確認時にさわってみた範囲ではかなり使えそうな感じでした。なお Alternativeto.net ではライセンスが Commercial に分類されていて,実際公式サイトで商用サービスも提供されていますが, MIT ライセンスのオープンソース・ソフトウェア としても提供されています。

基本情報

このアプリのデプロイは,これまで私が百日行で扱ってきた中では最も難敵でした。以下に示す手順も一見複雑ですが,個々のステップはシンプルで,一度わかってしまえばそれほど難しくはないので,恐れずに試してみていただきたいと思います。

  • 1) ソースコードの取得
  • 2) ソースコードの修正
  • 3) デプロイ準備
  • 4) サービスの作成とアプリとのバインド
  • 5) 起動スクリプトの作成
  • 6) manifest.yml の作成
  • 7) アプリの更新・起動
  • 8) 動作確認

1. ソースコードの取得

GitHub からソースコードをクローンします。この辺はいつも通りの手順です。

$ git clone https://github.com/feedbin/feedbin.git
$ cd feedbin/

2. ソースコードの修正

本アプリを Cloud Foundry 上で動かすには,以下の3点の修正が必要でした。

  1. 環境変数 MEMCACHED_HOSTS の条件付き読み込み
  2. 環境変数 FORCE_SSL の追加
  3. 環境変数 SERVE_STATIC_ASSETS の追加

いずれも,環境変数が関わる修正になります。このあたりは PaaS 上でアプリを動かす上での基本原則ですね。

2.1. 環境変数 MEMCACHED_HOSTS の条件付き読み込み

本アプリは Memcached も使うことができるようなのですが,

などから,必須ではないと考えられます。

しかし, ソースコード的には,プロダクション環境では環境変数 MEMCACHED_HOSTS が設定されていることが必須になっていた ので,これを条件付き読み込み (環境変数 MEMCACHED_HOSTS が設定されていたらそれを読み込む) に改めました。

$ git show 94d7fc6
commit 94d7fc64781d47b3474ed97a8e240ede5f1c9f48
Author: Noburou TANIGUCHI <dev@nota.m001.jp>
Date:   Tue Mar 10 23:44:10 2015 +0900

    Runnable without memcached

diff --git a/config/environments/production.rb b/config/environments/production.rb
index 320ab21..259d46b 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -68,7 +68,7 @@ Feedbin::Application.configure do
   # Disable automatic flushing of the log to improve performance.
   # config.autoflush_log = false

-  config.cache_store = :dalli_store, ENV['MEMCACHED_HOSTS'].split(',')
+  config.cache_store = :dalli_store, ENV['MEMCACHED_HOSTS'].split(',') if ENV['MEMCACHED_HOSTS']

   # Enable serving of images, stylesheets, and JavaScripts from an asset server.
   config.action_controller.asset_host = ENV['ASSET_HOST']

2.2. 環境変数 FORCE_SSL の追加

オリジナルのソースコードは, プロダクション環境では HTTPS の利用が必須 になっていました。しかし Cloud Foundry のような PaaS 環境では,リクエストがアプリに届く前に HTTPS が終端されることが多いので,これを設定で変更できるようにしました。

$ git show 77550ee
commit 77550eefb722f5cb0a778b4d2f247d7e0a474593
Author: Noburou TANIGUCHI <dev@nota.m001.jp>
Date:   Wed Mar 18 20:01:32 2015 +0900

    Make force_ssl configurable

    SSL might be terminated before the applicaton receives request.

diff --git a/config/environments/production.rb b/config/environments/production.rb
index 259d46b..36fa004 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -40,7 +40,7 @@ Feedbin::Application.configure do
   config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx

   # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
-  config.force_ssl = true
+  config.force_ssl = true unless (ENV["FORCE_SSL"] && (ENV["FORCE_SSL"].downcase == "false"))

   # Less verbose logs
   config.lograge.enabled = true

2.3. 環境変数 SERVE_STATIC_ASSETS の追加

Ruby-on-Rails では,静的コンテンツの提供をアプリとは別の専用 proxy cache を用いて行うことが一般的なようで, オリジナルのソースコードでもそういう設定 になっています。しかし,今回の Cloud Foundry 上へのデプロイではそうした proxy cache を使わないので,環境変数 SERVE_STATIC_ASSETS が設定されていて,かつ “false” でなければ, config.serve_static_assets を true に設定できるようにしました。

$ git show fdf3c14
commit fdf3c14ac4bf31d2fe5447f61d5e587466951ff2
Author: Noburou TANIGUCHI <dev@nota.m001.jp>
Date:   Wed Mar 18 19:59:30 2015 +0900

    Make "serving static assets" configurable

    Because we may not use any static content server on Cloud Foundry environment.

diff --git a/config/environments/production.rb b/config/environments/production.rb
index 36fa004..44c7328 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -20,6 +20,9 @@ Feedbin::Application.configure do
   # config.action_dispatch.rack_cache = true

   config.serve_static_assets = false
+  if ENV["SERVE_STATIC_ASSETS"] && (ENV["SERVE_STATIC_ASSETS"].downcase != "false")
+    config.serve_static_assets = true
+  end

   # Compress JavaScripts and CSS
   config.assets.compress = true

3. デプロイ準備

3.1. bundle install と .cfignore の作成

この後何度かローカル環境で bundle exec を実行する場面が出てくるので,その準備として bundle install を実行しておきます。また bundle install によって vendor/bundle に gem ファイルが保存されますが, 以前 Redmine の記事でも書いた ように,Cloud Foundry 上でも bundle install が実行されるためこれらのファイルのアップロードは不要なので,.cfignore ファイルを作成してアップロードを抑止するようにしておきます。

bundle install

$ bundle install --path vendor/bundle

Native extension を含む gem のコンパイル時に必要なライブラリーがないとエラーで止まってしまうことがありますが,これは環境によって異なるので本記事では詳細に記述しません。一応参考情報として,私が使っている検証環境では curb, pg, rmagick の3つの gem のインストール/コンパイル時にエラーが出て,それぞれ以下のページを参考に解決しました。

  • curb: http://stackoverflow.com/questions/16162266/unable-to-install-curb-gem
  • pg: http://stackoverflow.com/questions/3116015/how-to-install-postgresqls-pg-gem-on-ubuntu
  • rmagick: http://stackoverflow.com/questions/3704919/installing-rmagick-on-ubuntu

最終的に,上で示した bundle install --path vendor/bundle コマンドがエラーなしに最後まで走りきればOKです。

.cfignore の作成

先の bundle install でリポジトリー内に作成されたファイルのアップロードを抑制するために,以下の内容で .cfignore ファイルを作ります。

$ cat .cfignore
/.bundle/
/vendor/bundle/

3.2. アプリの事前プッシュ

サービスとのバインドや環境変数の設定を行うために,予めアプリを停止状態でプッシュしておきます。

$ cf push feedbin --no-start

4. サービスの作成とアプリとのバインド

本アプリを動かすには,PostgreSQL と Redis の2つのデータ・サービスが必要 です。

4.1. PostgreSQL

本アプリは,PostgreSQL にプラグインをインストールして使います。通常,プラグインのインストールには PostgreSQL インスタンスの管理者権限が必要なので,Cloud Foundry で提供されている通常の PostgreSQL サービス (例: postgresql-cf-service-broker) は使えません。そこで今回は自分で作成した PostgreSQL インスタンスに直接接続する方法をとります。

Docker で PostgreSQL を起動

まず PostgreSQL インスタンス を Cloud Foundry 内から接続可能な環境に構築します。今回は postgresql-cf-service-broker の時と同様,Docker を使って構築します。手順の詳細は そちら をご覧になってください。ここでは「Docker Image の取得 (docker pull)」までは終わっているものとして,「Docker Image の起動 (docker run)」から記述します。

$ sudo docker run --name feedbin-postgres -e POSTGRES_PASSWORD=xxxxxxxxxx -e POSTGRES_USER=feedbin -e LC_ALL=C.UTF-8 -p 0.0.0.0:5442:5432 -d postgres:9.4.2
3d1db1a8ed6cfc5b4f5d8ab20448194585310f5fff95544ad087ec4c28c8c734

PostgreSQL インスタンスの動作を確認します。

$ psql -U feedbin -W -l -h 192.168.15.91 -p 5442
Password for user feedbin:
                              List of databases
   Name    |  Owner   | Encoding | Collate |  Ctype  |   Access privileges
-----------+----------+----------+---------+---------+-----------------------
 feedbin   | postgres | UTF8     | C.UTF-8 | C.UTF-8 |
 postgres  | postgres | UTF8     | C.UTF-8 | C.UTF-8 |
 template0 | postgres | UTF8     | C.UTF-8 | C.UTF-8 | =c/postgres          +
           |          |          |         |         | postgres=CTc/postgres
 template1 | postgres | UTF8     | C.UTF-8 | C.UTF-8 | =c/postgres          +
           |          |          |         |         | postgres=CTc/postgres
(4 rows)

データベース一覧が取れました。問題ないようです。

データベースのスキーマ作成

次に,今起動した PostgreSQL の feedbin データベースにスキーマを作成します。

普通,Rails アプリのスキーマ作成には db:migrate という rake タスクを使うのですが,このアプリでは db:setup というタスクを使います。このタスクでは SQL をそのまま扱うこともできるので,db:migrate よりも素の DBMS に近い複雑な操作が行えます。前述のプラグインのインストールも,このタスクの中で行われています。

本アプリでは,このタスクは実際には

  • db/structure.sql の実行によるスキーマ作成
  • rake タスク db:seed による初期データ投入

という2段階で構成されています。ここではまず前半の「db/structure.sql の実行によるスキーマ作成」を実施します。

$ psql -U feedbin -W -h 192.168.15.91 -p 5442 -d feedbin -f db/structure.sql

途中でエラーが発生せずに最後まで走りきればOKです。

一応結果を確認してみます。

$ psql -U feedbin -W -h 192.168.15.91 -p 5442 -d feedbin -c '\d'
Password for user feedbin:
                        List of relations
 Schema |               Name                |   Type   |  Owner
--------+-----------------------------------+----------+---------
 public | actions                           | table    | feedbin
 public | actions_id_seq                    | sequence | feedbin
..
 public | users                             | table    | feedbin
 public | users_id_seq                      | sequence | feedbin
(47 rows)

テーブルができていることが確認できました。

初期データの投入

rake タスク db:seed で初期データを投入します。この初期データには 動作確認に使う初期ユーザーが含まれています

$ DATABASE_URL=postgres://feedbin:xxxxxxxxxx@192.168.15.91:5442/feedbin RAILS_ENV=production bundle exec rake db:seed
W, [2015-07-27T23:16:34.421389 #20297]  WARN -- : ** [Honeybadger] Unable to start Honeybadger -- api_key is missing or invalid. level=2 pid=20297
Skipping index creation, cannot connect to Elasticsearch
(The original exception was: #<Errno::ECONNREFUSED: Connection refused - connect(2) for "localhost" port 9200>)

今回 Honeybadger と Elasticsearch を使わない=未設定のため警告が出ますが,無視して問題ありません。

4.2. Redis

cf-redis-release を使って Redis サービスを作成し,アプリにバインドします。

$ cf marketplace
Getting services from marketplace in org nota-ja / space 100 as nota-ja...
OK

service      plans                     description
PostgreSQL   Basic PostgreSQL Plan*    PostgreSQL on shared instance.
p-mysql      100mb, 1gb                MySQL databases on demand
p-redis      shared-vm, dedicated-vm   Redis service to provide a key-value store

* These service plans have an associated cost. Creating a service instance will incur this cost.

TIP:  Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.
$ cf create-service p-redis shared-vm redis4feedbin
Creating service instance redis4feedbin in org nota-ja / space 100 as nota-ja...
OK
$ cf bind-service feedbin redis4feedbin
Binding service redis4feedbin to app feedbin in org nota-ja / space 100 as nota-ja...
OK
TIP: Use 'cf restage feedbin' to ensure your env variable changes take effect

VCAP_SERVICES 環境変数はこうなります。

$ cf env feedbin
..
 "VCAP_SERVICES": {
  "p-redis": [
   {
    "credentials": {
     "host": "10.244.3.46",
     "password": "c227fdd7-ed24-4e62-aa27-0a11765edf54",
     "port": 39039
    },
    "label": "p-redis",
    "name": "redis4feedbin",
    "plan": "shared-vm",
    "tags": [
     "pivotal",
     "redis"
    ]
   }
  ],
..

5. 起動スクリプトの作成

本アプリは foreman を使って worker プロセスを起動するのですが,Cloud Foundry の起動コマンドでは foreground プロセスは同時に1つしか許されないため,foreman で起動されるプロセスを background に持っていく必要があります。1-liner でそれができないか試みたのですが難しかったので,Cloud Foundry 用の起動スクリプトを作成して,その中で起動するプロセスの制御を行うことにしました。

$ cat cf-startup.sh
#!/usr/bin/env bash

set -x

bundle exec foreman start &
sleep 10
bundle exec rackup --port $PORT

ファイルに実行可能フラグをつけるのを忘れないようにしてください。

$ ls -l cf-startup.sh
-rwxr----- 1 nota-ja nota-ja 98 Jul 29 04:20 cf-startup.sh

6. manifest.yml の作成

最後に,このアプリでは設定する環境変数が多くて cf set-env コマンドでいちいち設定するのが大変なので,manifest.yml ファイルを作りました。

$ cat manifest.yml
---
applications:
- name: feedbin
  memory: 1344m
  command: './startup.sh'
  env:
    DATABASE_URL: postgres://feedbin:xxxxxxxxxx@192.168.15.91:5442/feedbin
    REDIS_URL: redis://redis:c227fdd7-ed24-4e62-aa27-0a11765edf54@10.244.3.46:39039
    FORCE_SSL: false
    SERVE_STATIC_ASSETS: true
    PUSH_URL: https://feedbin.10.244.0.34.xip.io
    SECRET_KEY_BASE: 3ccac264399989430f06deafaf796faf1a357fcb505125c9c783b200ef70c02451e250a54d288edb4340b6af4a688b65e04fdb39f4b9318199ec4c0a34569ad6

以下簡単に説明します。

- name: feedbin

↑ Cloud Foundry 上でのアプリ名。当然ながら, 3.2. アプリの事前プッシュ の時に指定した名前と一致していなければなりません。

  memory: 1344m

↑ worker プロセスもいるので,多めに指定しておいたほうが無難です。

  command: './startup.sh'

↑ 起動コマンドとして,先ほど作成したスクリプトを指定します。

env: 階層の下は環境変数です。

    DATABASE_URL: postgres://feedbin:xxxxxxxxxx@192.168.15.91:5442/feedbin

↑ PostgreSQL の接続情報を,DATABASE_URL 形式で記述したものです。

    REDIS_URL: redis://redis:c227fdd7-ed24-4e62-aa27-0a11765edf54@10.244.3.46:39039

↑ Redis の接続情報です。 cf env feedbin で取得した VCAP_SERVICES 環境変数を元に作成して設定します。

    FORCE_SSL: false

2.2. 環境変数 FORCE_SSL の追加 で定義した環境変数です。今回は SSL/TLS を強制しません。

    SERVE_STATIC_ASSETS: true

2.3. 環境変数 SERVE_STATIC_ASSETS の追加 で定義した環境変数です。今回は Rails プロセス自身で静的コンテンツも提供します。

    PUSH_URL: https://feedbin.10.244.0.34.xip.io

↑ worker プロセスがフィードの更新を POST する URL のようです (参考) 。Cloud Foundry 上でのアプリの URL を設定します。

    SECRET_KEY_BASE: 3ccac264399989430f06deafaf796faf1a357fcb505125c9c783b200ef70c02451e250a54d288edb4340b6af4a688b65e04fdb39f4b9318199ec4c0a34569ad6

↑ 秘密鍵の元です。 bundle exec rake secret で作ることができます。

7. アプリの更新・起動

必要な設定は全て終わったので,アプリを更新&起動します。

$ cf push
..
requested state: started
instances: 1/1
usage: 1.3G x 1 instances
urls: feedbin.10.244.0.34.xip.io
last uploaded: Mon Jul 27 21:28:40 UTC 2015
stack: cflinuxfs2
buildpack: Ruby

     state     since                    cpu    memory           disk      details
#0   running   2015-07-28 06:32:40 AM   0.0%   642.8M of 1.3G   0 of 1G

この後述べる Ruby のバージョン問題がなければ,正常に起動するはずです。

なお,使用する Cloud Foundry のリリースによっては,以下のようなエラーが出て起動に失敗することがあります。

..
-----> Compiling Ruby/Rails
       Could not get translated url, exited with: DEPENDENCY_MISSING_IN_MANIFEST: https://s3-external-1.amazonaws.com/heroku-buildpack-ruby/ruby-2.1.4.tgz
 !
 !     exit
 !
Staging failed: Buildpack compilation step failed


FAILED
BuildpackCompileFailed
..

これは,アプリが要求している Ruby のバージョン (2.1.4) が,使用している Cloud Foundry の Ruby buildpack にないために起きる現象です。古い Ruby buildpack を指定するなどの方法で回避することもできますが,今回私はアプリが要求している Ruby のバージョンを書き換えることで対処しました。

$ git show abb8ec5
commit abb8ec5625bee615861991d7677c076690704618
Author: Noburou TANIGUCHI <dev@nota.m001.jp>
Date:   Tue Jul 28 08:38:16 2015 +0900

    Use Ruby 2.1.6

    Because the recent Cloud Foundry Ruby buildpack doesn't have 2.1.4.

diff --git a/.ruby-version b/.ruby-version
index c346e7a..b6da512 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.1.4
\ No newline at end of file
+2.1.6
\ No newline at end of file
diff --git a/Gemfile b/Gemfile
index f99e7b9..166053a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,5 +1,5 @@
 source 'https://rubygems.org'
-ruby '2.1.4'
+ruby '2.1.6'

 gem 'rails', '= 4.1.11'
 gem 'rest-client', '= 1.6.7'

8. 動作確認

ブラウザーでアクセスしてみます。

起動直後の画面:

db/seeds.rb に書かれた初期ユーザーでログインしてみます:

ログイン直後の画面:

試しに 本ブログのAtom を登録してみます:

Atomが読み込まれ,フィード一覧が表示されました:

フィードを選択すると,中身も読めます:

画像もインラインで表示されます:

背景やフォントも変えられますし,普通に使えそうです。

今回使用したソフトウェア