LCL Engineers' Blog

夜行バス比較なび(高速バス比較)・格安移動・バスとりっぷを運営する LCLの開発者ブログ

RailsでPaperclipを利用して、画像に透かし(ウォーターマーク)を付与する

弊社で運営している「バスとりっぷ」というメディアでは、Paperclipを利用して画像のアップロードを行っています。Paperclipでは、サムネイルの作成やリサイズ・背景色の変更などを行っていましたが、透かし(ウォーターマーク)の付与にも対応しました。

www.bushikaku.net

ウォーターマークの付与イメージ

画像に透かし(ウォーターマーク)を付与するには、透かし画像を用意し元画像に重ね合わせます。

f:id:lcl-engineer:20170326200650p:plain

実装方法

paperclipを拡張してもできますが、paperclip-watermarkというgemを利用しました。

GitHub - vikewoods/paperclip-watermark: module PaperclipWatermark

gem 'paperclip-watermark'

paperclipの設定に、以下のようにwatermarkの設定を追加するだけです。これでアップロードしたファイルに、ウォーターマークが付与されます。

class SampleImage < ActiveRecord::Base
  has_attached_file : attachment,
                    :processors => [:watermark],
                    :styles => { 
                            :original => { :watermark_path => "#{Rails.root}/watermark.png",:position => "southwest"}
                     }

paperclipの利用方法については、下記のページを参照ください。

Thumbnail Generation · thoughtbot/paperclip Wiki · GitHub

ウォーターマーク付与位置の変更

ウォーターマークを付与する位置は、positionを指定することで変更できます。

:original => { :watermark_path => "#{Rails.root}/watermark.png",:position => "northwest"}

以下の9種類の位置が指定できるようになってます。

  • northwest 左上
  • north 中央上
  • northeast 右上
  • west 中央左
  • center 中央
  • east 中央右
  • southwest 左下
  • south 中央下
  • southeast 右下

さらに「geometry」を指定すれば、上記の基準位置からピクセル単位で調整できるようです。(未検証)

ウォーターマークを外す

ウォーターマークを付与したくない画像に対して付与してしまった場合など、ウォーターマークを外したい場合もあります。その時は、以下の手順で復元します。

  • ウォーターマーク付与時に、元画像を別名で保存する
  • ウォーターマークを外す場合は、元画像を付与済みの画像に上書きする。

別名保存は、paperclipの指定で簡単にできます。

:styles => { 
                   :original_file_backup => {},
                   :original => {
                         :processors => [:watermark],
                   ・・・
                 }
}

別名で保存した画像が、インターネット上で誰にでもアクセスされてしまうと、結局ウォーターマークが付与されていない画像を取得することが可能になってします。paperclipでは、S3の権限も簡単に制御可能なので、以下のようにアクセス制限を行っています。

:s3_permissions => {:original_file_backup => :private},

既にアップロード済み画像に付与する

既にアップロード済みの画像に対しても、paperclipで再処理することが可能です。処理対象のmodelに対して、reprocessを実行します。

sample_image = SampleImage.find(123)
sample_image.reprocess!

まとめ

実際には、ウォーターマークを付与する・しないを画像によって振り分けているため、細かい判定をロジックを行っています。 その辺は結構手間がかかってますが、ウォーターマークの付与は、paperclip-watermarkを使って簡単に実現できました。

GitHubへのpush時に、featureブランチ環境を自動作成する

弊社では、作業中ブランチの動作を誰でも確認できる仕組みを用意しています。今回の記事では、導入背景から運用方法までを紹介したいと思います。

解決したかった課題

弊社のWebサービス開発では、GitHub Flowを採用しています。 GitHub Flowでは、masterへマージ後にすぐプロダクション環境へデプロイするため、masterへのマージ後には問題が発見されないのが理想です。その為、他のメンバーにfeatureブランチの動作を早い段階で確認してもらう仕組みが必要でした。

開発者のローカル環境へアクセスすれば確認することはできるのですが、以下のような課題もあったためサーバ上にブランチ専用の環境を作成するようにしました。

  • 複数Issueの開発が並行で走るので、featureブランチ毎に独立した環境が必要
  • リモートワークをしているメンバーもいるため、インターネット上に環境が必要
  • 開発者の手間をかけず、自動での構築が必要

仕組みの概要

GitHubへfeatureブランチをpushすると、WebhookでJenkinsのJOBを実行し、EC2上のテストサーバにfeatureブランチ用のアプリをデプロイします。 アプリは、Nginx + Rails(Unicorn)で構成しています。デプロイ時には、baseとなるNginx/Unicornの設定ファイルをコピーし、featureブランチ用に一部を書き換えます。(DBは共通にしています)

f:id:lcl-engineer:20170326154533p:plain

2回目以降のPUSHは既に環境が作成されているため、新たに環境は作成されず、Gitからコードをpullし既存の環境を最新化するようにしています。

仕組みの構築にあたっては、以下の記事を参考にさせて頂きました。

URL

あらかじめ、AWS Route53に「 *.yyy.zzz 」というワイルドカード利用したドメインを割り当てておき、テストサーバのIPを設定しておきます。 ワイルドカード部分にブランチ名を設定し、ブランチ毎に以下のようなURLとなるルールにしています。

https:// branch-123.yyy.zzz/

なお、ドメインにアンダースコアは利用できないため、ブランチ名にアンダースコアが含まれている場合は、ハイフンなどの別の文字へ置換する必要があります。

Nginx設定ファイル

ベースとなる設定ファイルをコピーし、server_nameには各ブランチのドメインを設定します。

server_name  base.yyy.zzz;
↓
server_name  branch-123.yyy.zzz;

Nginx -> Unicornとのsocket通信のパスもブランチ毎に固有にするため書き換えます。

server unix:/tmp/base.sock
↓
server unix:/tmp/branch-123.sock

Unicorn設定ファイル

Unicornもベースとなる設定ファイルをコピーし、nginxとのソケットのパスを書き換えます。

listen "/tmp/base.sock", :backlog => 64
↓
listen "/tmp/branch-123.sock", :backlog => 64

Railsアプリ

該当のブランチからコードをcloneし、bundle install / assets precompileなどの通常のRailsアプリのデプロイ手順通りにデプロイします。

全ての環境が揃った段階で、Nginx/Unicornを起動すると環境へのアクセスが可能となり、ブランチ専用URLをチャットへ通知します。

環境作成のタイミング

当初は、hubotで任意に指定した場合のみ、環境を作成することも考えましたが、一手間かかってしまうためGitHubへのpushで環境を作成するようにしました。 GitHubへのpushで環境を作成すると、要不要問わず全てのブランチの環境が作成されてしまうので、後述する環境削除が重要となります。

環境削除のタイミング

この仕組みの場合は、環境がいくつかもできてしまうので、放置しているとサーバリソースをすぐに圧迫してしまいます。当初は、最大N個の環境のみ保持し、更新されていないものを自動削除するという運用にしていました。ただし、レビュー期間が長い案件の場合は「テストURLが見れなくなっている」とう状態になり、その都度もう一度環境を作り直す必要がありました。

現在では、該当ブランチがmasterへマージされたことをトリガーに、環境を削除するようにしています。masterへのマージ待ちブランチが多いと、リソースを圧迫してしまうため、N個を超えたらチャットへ通知し、人が不要な環境を判断して削除します。この運用も完璧ではないですが、人であればレビュー中のブランチは削除しないなど、精度の高い判断ができるので当初の問題は解消できました。

まとめ

現在の仕組みでうまく周っているため満足度は高いのですが、実装がシェルを駆使した泥臭い仕組みになってしまってまい柔軟性に欠けています。Docker等を利用すれば、もう少しシンプルな仕組みにできそうなので、今後取り組んでみたいと思っています。

RubyMineでよく使うキーボードショートカット

弊社が運営している夜行バス比較なび格安移動のサーバーサイドは主にRuby on Railsで実装されておりエンジニアのほとんどがIDEにRubyMineを使用しています。 IDEは触れる時間が長いツールなので使いこなしたいし、楽をしたいですね。そんな時に重要なのがキーボードショートカットです。キャレットの移動やモードの切り替えをマウス操作なしで行えるので開発効率もアップします。 他のIDEに漏れずRubyMineにもキーボードショートカットは星の数ほど存在しますが、その中でもコーディング中のファイル間の移動によく使用するものを紹介します。(修飾キーはMacのものです。)

クラスを開く(⌘ + o)

クラス名を検索して開きます。何を始めるにもまずはここからです。あいまい検索できるので例えばFooBarControllerを開きたい場合はfcoとタイプすれば簡単に開くことができます。

f:id:lcl-engineer:20170317095020p:plain

ファイルを開く(⌘ + Shift + o)

こちらはクラス名ではなくファイル名を検索して開きます。jsやyamlを開くときに役立ちます。

フォーカスを切り替える(Control + Tab)

開いているファイルやTool Windowへのフォーカスを切り替えることができます。

f:id:lcl-engineer:20170317095908p:plain

任意のTool Windowへフォーカスを移す(⌘ + 数字)

Control + Tabでも切り替えることができますが、特定のTool Windowへのアクセスはこちらのショートカットの方が早いです。 例えば⌘ + 1でProjectを開いたり閉じたりすることができます。割り当てられている数字はView > Tool Windowsで確認することができます。

f:id:lcl-engineer:20170317100409p:plain

リファクタリングする(Control + t)

変数、メソッド、選択したテキスト上で実行するとダイアログが表示され対象の名前の変更やメソッド化などの各種リファクタリングを実行をできます。名前の変更は参照先も全て置換されるので重宝します。

f:id:lcl-engineer:20170317101106p:plain

定義へジャンプする(⌘ + b)

クラス、メソッド、変数の定義へジャンプします。

戻る(⌘ + [)

定義へジャンプしたり新たに別のファイルを開いたときに直前のファイルへ戻ることができます。複数回実行して前の前のファイルに戻ることもできます。

ファイルを閉じる(⌘ + w)

開いているファイルを閉じます。そのままですね。

[独自定義]画面を縦に分割する(Control + \)

カスタマイズして設定しているショートカットです。エディターペインを縦に分割します。縦分割→定義へジャンプする→閉じるで作業している画面はそのままに詳細をドリルダウンしてまた元の場所に戻るという動きがスムーズにできます。

[独自定義]画面を横に分割する(Control + -)

これもカスタマイズです。エディターペインを横に分割します。

何でもアクションを実行(⌘ + Shift + a)

全てのアクションを検索して実行することができます。ファイルを開きたいけどショートカットがわかないといった時にfileと入力するとfileに関するアクションの候補が表示されます。ここからfile...を選択すればファイルを開くことができますし、ショートカットキーもわかります。アクションにショートカットキーが割当てられていない場合もここから実行することができるので困ったときはまずここで関連する単語を入力することをおすすめします。

f:id:lcl-engineer:20170317104529p:plain

おまけ

最後にあまり使わないのでショートカットキーを割り当てていないのですが、たまに使う便利な機能を紹介します。

プレゼンモード

プレゼン用にフォントを拡大表示してフォーカスしているエディタのみを表示してくれるモードです。わざわざフォントサイズを手動で変更する必要がなく無駄な表示も消してくれるので便利です。 ⌘ + Shift + aToggle Presentation mode(長いのでpresenくらいでOKです)と入力すると簡単に切り替えられます。

集中モード

これもプレゼンモードに似たような機能ですがフォントはそのままに開いているエディタのみを表示します。コードを書く領域を確保してとにかく集中したいときには役に立ちます。 ⌘ + Shift + aToggle Distraction Free mode(こちらはdistでOK)と入力すると切り替えられます。

おわりに

他にもRailsに特化したキーボードショートカットなど多種多様な設定が存在しますが今回紹介したショートカットあたりは押さえておくとコーディングが楽になるのではないでしょうか。

本番DBを開発環境に簡単にコピーする仕組み

こんにちは、エンジニアの森脇です。

弊社では、本番DBを開発環境にコピーして開発をしています。今回は、どのような仕組みで実現しているかを紹介します。

なぜ本番DBを利用するか

cookpad様の記事でも言及されていますが、弊社でも全く同じ認識を持っており、下記理由で本番DBを利用しています。

  • ユーザと同等体験での開発
  • パフォーマンス問題の早期発見
  • データ依存不具合の早期発見

開発環境のデータをできるだけ本番に近づける - クックパッド開発者ブログ

当初の運用

元々は、サーバへのログイン権限があるメンバーが、本番DBバックアップファイルを取得し、手動でリストアするという運用でした。工数もかかり、特定メンバーに依存するため、各メンバーが必要なときにすぐ準備できないため、誰でも準備できる仕組みを用意しました。

仕組みの概要

ローカル環境のShellを叩くと、本番DBのバックアップファイルを取得し、ローカル環境にリストアする仕組みです。cookpad様のような常に本番データが同期される仕組みではありません。( 今のところそこまでの必要性は感じていません。)

本番DB  ----> バックアップサーバ  ---->  ローカル環境

各ポイントについて少し詳しく説明します。

DBのバックアップ

PostgreSQLを利用しているため、pg_dumpを利用してバックアップを取得します。 バックアップ取得後に、一度バックアップサーバ上でリストアし、ログデータなどのデータ量が大きいテーブルのレコードを間引く処理をしています。その後再度dumpを取得します。

弊社では今のところ個人情報は扱っていないのですが、個人情報を扱うようになった場合は、ここでマスキング処理を行う予定です。

ローカル環境からバックアップファイル取得

バックアップサーバ上にWebサーバを立てて、ローカルにバックアップファイルを返す簡易アプリケーションを配置しています。 ローカルからwget等のコマンドを叩くと、指定された条件に応じたバックアップファイルを取得できます。

※ 本番DBだけではなく、テスト環境のDBや、特定日付のバックアップも取得できるようにしています。

ローカルDBへのリストア

バックアップファイルを取得すると、以下の処理を行いリストアします。

  1. ローカルDBに接続しているセッションを全て切断
  2. ローカルDBをdropする
  3. ローカルDBをcreateする
  4. 取得したバックアップファイルをリストアする

なお、pg_restore時には、jobsを渡すと並列でリストアを行うことができ高速化ができます。(適切な値は環境によって変わります)

pg_restore -v -d localdb --jobs=4 /tmp/backup.custom

以上が簡単な仕組みの流れです。バックアップ対象DBにもよりますが、早ければ1,2分でローカルに本番DB環境を準備できます。

まとめ

バックアップはいざ必要になったときに、うまく取得できていなかった・リストアができない(やり方がわからない)等の問題が発生することがあります。 定期的に本番DBのバックアップをリストアすることは、正しくバックアップが取得できているという確認にもなり、精神的にも安心できますので、是非おすすめします。

スクロールするタブはスライダーのプラグインで実装できる

フロントエンドエンジニアの岡田です。 本日、夜行バス比較なびでは、プレミアムフライデー用の特集ページをリリースいたしました。 www.bushikaku.net

こちらの特集でつかった、スクロールできるタブの実装についてご紹介します。 スマホ用のページの以下の部分です。
f:id:lcl-engineer:20170308174857g:plain

こちらは、FlexSliderというプラグインを使っています。 画像のスライダーやカルーセルを実装するときに使う、jQueryプラグインです。 woocommerce.com

オプションは以下のように設定しました。

$('#region-tab').flexslider({
  animation: "slide",
  selector: ".slides > li:visible",
  slideshow: false,
  animationLoop: false,
  controlNav: false,
  directionNav: false,
  itemWidth: 100
});

以下のオプションで、表示されている要素のみをスライダーの対象にすることができます。 今回はタブの数が動的に変わるために設定しています。

selector: ".slides > li:visible",

今回の特集では、スクロールするタブの他にもメインビジュアルのスライダーや、写真スライドにもflexsliderを使っています。 設定は以下のサイトを参考にさせていただきました。

【jQuery】超万能スライダー FlexSlider の使い方をマスターする。 - ONZE

他のスライダーでも同様のことはできると思います。 お手軽に実装できるのでおすすめです。

プレミアムフライデーにヨガLINE botを作った話

フロントエンドエンジニアの岡田です。 先週の金曜日は、プレミアムフライデーでしたね。 LCLでも従業員全員が15時に退社しました。 premium-friday.go.jp

私は今回はどこへも出かけられなかったのですが、せっかく時間ができたので、LINE botをつくってみました。 先日こちらでご紹介したとおり、LCLにはヨガ部があるので、 今回はおすすめのヨガ動画を返してくれるbotを作ることにしました。 f:id:lcl-engineer:20170227051900p:plain

機能

現時点のbotの機能は以下のとおりです。

  • ユーザーがキーワードを送る
  • botはそのキーワードを含む動画をYoutubeから検索して返す
  • 動画は特定のチャンネル内から検索して返す。いつもヨガ部で見ている以下のチャンネル内から返すようになっています。 www.youtube.com

環境

Node.js + Heroku + LINE BUSINESS アカウント

以下のチュートリアルにならって作りました。Part3までで環境は整います。 qiita.com 今回はAIは入ってないですが、AI入りのbotもおもしろいです。こちらの記事はとてもわかりやすいのでおすすめです。

Google Developers Console

Youtube APIに使います。以下の記事が参考になりそうです。
知っておきたい!YouTube APIの始め方 | D2Cスマイル

実際のコード

環境ができたら、index.jsを以下のように書き換えると完成です。

// -----------------------------------------------------------------------------
// 定数の設定
const LINE_CHANNEL_ACCESS_TOKEN = 'あなたのChannl Access Token';
const GOOGLE_API_KEY = 'あなたのGOOGLE API KEY';
const DOMAIN = 'あなたのサーバーのドメイン: https://xxxxxxxx.herokuapp.com/';

// -----------------------------------------------------------------------------
// モジュールのインポート
var express = require('express');
var bodyParser = require('body-parser');
var request = require('request');
var app = express();

// -----------------------------------------------------------------------------
// ミドルウェア設定
app.use(bodyParser.json());

// -----------------------------------------------------------------------------
// Webサーバー設定
var port = (process.env.PORT || 3000);
var server = app.listen(port, function() {
  console.log('Node is running on port ' + port);
});

// -----------------------------------------------------------------------------
// ルーター設定
app.get('/', function(req, res, next) {
  res.send('Node is running on port ' + port);
});

app.post('/webhook', function(req, res, next) {
  res.status(200).end();
  for (var event of req.body.events) {
    if (event.type == 'message') {
      var requestHeaders = {
        'Content-Type': 'application/json',
        'referer': DOMAIN
      }
      //オプションを定義
      var options = {
        url: 'https://www.googleapis.com/youtube/v3/search?key=' + GOOGLE_API_KEY + '&part=snippet&channelId=UCd0pUnH7i5CM-Y8xRe7cZVg&q=' + encodeURI(event.message.text),
        method: 'GET',
        headers: requestHeaders,
        json: true
      }
      console.log(options);

      var youtubeMovieUrl;
      //リクエスト送信
      request(options, function(error, response, responseBody) {
        //コールバックで色々な処理
        youtubeMovieUrl = 'https://www.youtube.com/watch?v=' + responseBody.items[0].id.videoId;
        console.log(youtubeMovieUrl);
        var body = {
          replyToken: event.replyToken,
          messages: [{
            type: 'text',
            text: youtubeMovieUrl
          }]
        }
        var url = 'https://api.line.me/v2/bot/message/reply';
        var headers = {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN
        }
        request({
          url: url,
          method: 'POST',
          headers: headers,
          body: body,
          json: true
        });
      })
    }
  }
});

2017/03/19 追記 Google Cloud PlatformのAPIキーの制限に「HTTP リファラー(ウェブサイト)」を使えるように、上記ソースコードのrequest時にrefererを追加しました。

youtubeの動画取得については、以下の記事を参考にさせていただきました。 http://xoyip.hatenablog.com/entry/2014/01/24/212903xoyip.hatenablog.com

はまったところ

Youtubeのキーワードをエンコード(encodeURI)しないとうまく結果が表示されません。 以下のような状態だったため解決に時間がかかってしまいました。

  • ブラウザからリクエストした場合はエンコードしなくても正しい結果が返る
  • キーワードによってはそれらしい結果が返ってくる

まとめ

3時間でできるところはこのへんまででしたが、まだまだ改良できるので育てていこうと思います。

BugsnagでRailsのエラーを漏れなく補足する

弊社では、以下のエントリで紹介したように、Bugsnagを利用してエラー監視をしています。

techblog.lclco.com

前回は、JavaScriptでの利用について紹介しましたが、今回はRailsでどのように利用しているのかを紹介します。

Bugsnagの選定理由

Railsのエラー検知サービスとしては、airbrakeやsentry等が有名ですが、Bugsnagは他と比較して費用が手頃でかつUIが見やすかったため選定しました。(それほど時間をかけて、比較検討したわけではありません)

導入手順

Gemfileに下記を追加して、bundle installを実行します。

gem 'bugsnag'

gemを追加すると、以下のコマンドでbugsnagのconfigファイルが生成できます。API KEYはbugsnagの管理画面から取得できます。

bundle exec rails generate bugsnag YOUR_API_KEY_HERE

これだけで、自動的に発生した例外を補足して、Bugsnagへ通知されるようになります。

任意場所に通知を仕込む

通常のケースでは通るはずがないコードにも、Bugsnagを仕込んで検知するようにしています。

例)必ず取得できるはずのデータが取得できなかった場合

Bugsnag.notify(
    RuntimeError.new('not found xxxx '),
    {
        :severity => 'error',
          :errro_detail => 'エラーの詳細・・・',
          :error_data => {:aaa => '・・・', :bbb => '・・・`},
    }
)

errro_detail,error_dataに各種データを設定することで、Bugsnag側で確認できます。

f:id:lcl-engineer:20170226185952p:plain

このように任意場所で通知を仕込むことで、データ不備によるエラーを早期検知することができるようになりました。

ChatWorkへの通知方法

Bugsnagがエラーを検知するとChatWorkへ通知するようにしています。残念ながら、チャットーワークとの連携はデフォルトでは用意されていないので、独自のAPIを用意して実現しています。

NewrelicとChatWorkの連携でも同じことを行っていますので、詳細は下記の記事を御覧ください。

techblog.lclco.com

まとめ

以前は、エラーログを定期的にチェックして、予期していないエラーが発生していないかを確認していましたが、Bugsnagを導入してからは不要になりました。 Bugsnagは導入も簡単で、費用を比較的安いので、非常におすすめのサービスです。