読者です 読者をやめる 読者になる 読者になる

LCL Engineers' Blog

夜行バス比較なび・格安移動・高速バス比較を運営する 株式会社LCL開発者のブログ

Railsのデプロイ時にwebpackのビルドを実行する

弊社が運営しているサービス:夜行バス比較なびのサーバーサイドはRuby on Railsですが、フロントエンドの一部はReact & ES2015で書かれています。今回はデプロイにおけるRailsとwebpackの連携について紹介します。

www.bushikaku.net

React & ES2015導入の詳細もブログで公開しています。ぜひ読んでみてください。

techblog.lclco.com

これまでの問題

webpackの環境は、各自の開発PC(Mac)につくります。 もともとRailsのsprocketsを使っていたため、React化(ES2015化)完了までは今まで通りsprockets を使うことにしました。 つまり、webpackで書き出したファイルを、sprocketsで管理しているディレクトリ以下へコミットしてしまいます。 (良い方法ではないと思いますが、サーバーにnode.jsの環境を作るまでのつなぎです。)

このように上記の記事でも言及していますが、今までwebpackのビルドは開発者がローカルで実行して、生成されたjsファイルもGitで管理していました。ソースコードの二重管理は手間ですし、手順が多いということはミスの誘発につながるため避けたいところです。

解決策

タイトルのとおり、デプロイ時にサーバーでwebpackを実行するようにしました。具体的にはAsset PipelineのPrecompileにフックしてビルドを実行しています。ビルド後はsprocketsに任せてダイジェストの付与を行っています。

この方法は下記の記事を参考にさせていただきました。

WebPackを使ってRailsからJavaScriptを楽に良い感じに分離する - Qiita

設定の詳細は以下のとおりです。

各種バージョン

Ruby 2.1.2
Rails 4.1.5
node.js 6.9.1
babel 6.11.4
webpack 1.13.1

ディレクトリ構成

webpackの入出力のディレクトリを簡単に説明します。実際より簡略化しています。詳細はこちらをご覧ください。

React & ES2015のソースコードはfrontend/src/scripts/に配置しています。

frontend
├── src
    └── scripts
        ├── xxxx
        │   └── xxxx
        │       ├── xxxx.jsx
        │       └── xxxx.jsx
        ├── xxxx     
        │   ├── xxxx
        │   │   └── xxxx.js
        │   │   └── ...
...

ビルドしたファイルはapp/assets/javascripts/es/にbundleされて出力されます。

app
├── assets
    ├── javascripts
        ├── es
        │   └─ xxxx.js
        │
        ├── xxxx // 通常のjsファイルがあるディレクトリ
        │   └─ xxxx.js
...

gitignoreの設定

ビルドで生成されるjsファイルのバージョン管理は不要になるのでgitignoreに追加します。この場合はapp/assets/javascripts/es/配下を無視します。

Precompileへwebpackビルドのフックを追加

lib/tasksにRakeタスクを作成します。これでデプロイ時にwebpackのビルドが実行され、その後sprocketsで処理され配信されます。

task :build_frontend do
  cd 'frontend' do
    sh 'npm install --production'
    sh 'npm run build'
  end
end

Rake::Task['assets:precompile'].enhance(%i(build_frontend))

おわりに

この方法で煩わしい手動ビルドからも解放され、デプロイもスムーズに行うことができています。

ここから更にフロントエンドの環境を整えてより良いサービスを提供していきたいです。

ES5からWebpack管理下の関数を呼び出す方法

フロントエンドエンジニアの岡田です。 先日ご報告したとおり、LCLが運営する「夜行バス比較なび」では、jQueryで作られたサイトをReactへ置き換えています。 techblog.lclco.com

記事を書いた後も細々と置き換えを進めていますが、ページ単位での置き換えは、それなりにまとまった時間が取れないと進められません。 しかしSPAのように複数のjsがあるページでは、その時間が取れず、なかなか置き換えが進められませんでした。

そこで、LCLではもっと小さい単位で置き換える方法を試してみました。
手順は以下のとおりです。

  1. 既存の関数(ES5)を、ES2015へ変換してWebpack管理下のディレクトリへ移動
  2. 移動した関数をグローバル関数へ変更
  3. 既存のjsからグローバル関数を呼び出し

この方法を使うと、関数単位で置き換えが可能です。

ES2015への変換ツール

変換にはlebabを使いました。 lebab.io

具体的な方法

既存の関数(ES5)をlebabでES2015へ変換

// 変換後の関数
const hogehoge = () => {
};

移動した関数をグローバル関数へ変更

window.grobalHogehoge = hogehoge;

既存のjsからグローバル関数を呼び出し

// グローバルに露出している関数を呼び出し
window.grobalHogehoge();

グローバルに露出させるのであまりよくないかもしれませんが、あくまでも移行中の一つの手段としては使えます。この方法で関数をES2015化して、最後にきれいにReactへ置き換えたいと思います。

LINE botの返事をカルーセル形式にしてみました

フロントエンドエンジニアの岡田です。 先日作ったLINE botを改良しましたのでご紹介します。 techblog.lclco.com

前回の状態では、botはテキストメッセージを返すだけでした。 f:id:lcl-engineer:20170227051900p:plain:w382

これでは開いてみるまでどんなヨガ動画かわかりませんし、候補が1つしかないのもいまいちです。

そこで、Template messageを使って、候補の出し方を変えてみました。 Template messageには3種類あります。

  • Buttons
  • Confirm
  • Carousel

LINE API Reference

今回はCarouselを使いました。 右にスライドすると、最大5つのヨガ動画が表示されます。
f:id:lcl-engineer:20170330060532g:plain

見た目が一気に豪華になりましたね。
今回はURLの他に、動画のタイトルやdescription, thumbnail等も出しています。

ソースコードは以下のとおりです。

// -----------------------------------------------------------------------------
// 定数の設定
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
      }

      //リクエスト送信
      request(options, function(error, response, responseBody) {
        //コールバックで色々な処理
        var columns = [];
        for (var item of responseBody.items) {
          columns.push({
            "thumbnailImageUrl": item.snippet.thumbnails.medium.url,
            "title": item.snippet.title,
            "text": item.snippet.description ? item.snippet.description.substr(0, 60) : ' ', // title指定時は60文字以内,
            "actions": [{
              "type": "uri",
              "label": "動画を再生",
              "uri": 'https://www.youtube.com/watch?v=' + item.id.videoId
            }]
          });
          // carouselは最大5つのため、6つ以上の候補はカット
          if (columns.length === 5) {
            break;
          }
        }

        var body = {
          replyToken: event.replyToken,
          messages: [{
            "type": "template",
            "altText": "this is a carousel template",
            "template": {
              "type": "carousel",
              "columns": columns
            }
          }]
        }
        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
        });
      })
    }
  }
});

以上です。 更なるパワーアップを目指して、改良していこうと思います。

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のバックアップをリストアすることは、正しくバックアップが取得できているという確認にもなり、精神的にも安心できますので、是非おすすめします。