LCL Engineers' Blog

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

パフォーマンス計測ツール:SpeedCurveの使い方

フロントエンドエンジニアの岡田です。今回は、パフォーマンス計測ツール:SpeedCurveをLCLでどのように使っているかお伝えします。 f:id:lcl-engineer:20170628054829p:plain

設定について

計測タイミング(Times)

計測タイミングは多いに越したことはないとは思いますが、コストとのバランスで、今のところ1日1〜2回計測をしています。 1日分のリリースが終わるタイミングとアクセスが増えるタイミングに計測をしています。

計測回数(Checks)

3回以上で任意の回数が設定できます。今のところ最低ラインの3チェックにしています。 3回チェックすることで、標準よりも早かったり遅かったりする結果が排除され、結果の中央値が選択されるそうです。 What is a check and how many do I need? | SpeedCurve Support

ブラウザ

LCLのサービスは、スマホユーザーが多いため、スマートフォン向けページのみ計測しています。(1URLをスマホ・PC等複数デバイスで計測することも可能です)
ブラウザは、Chromeのエミュレート環境でiPhone6のサイズで設定しています。 回線については、Newrelic Browser(RUM)と比べて値が近かった、4G設定で計測をしています。 Newrelicの結果では、実際には4Gよりもやや早いような印象でした。 f:id:lcl-engineer:20170619064716p:plain

料金プラン

Synthetic Pay-As-You-Go plan. にしています。

SpeedCurve | Pricing

1チェック $0.01で、月額$20〜 です。 1日1回計測 × 1計測3回チェック × 1ブラウザの場合、21ページまでなら$20です。(31日間の月の場合)

結果の見方について

Siteタブ

設定したタイミングで計測したデータが表示されます。 Start Render, SpeedIndex, Visually Complete の他、Backendの時間も確認できます。 f:id:lcl-engineer:20170628054829p:plain

グラフにはnoteを追加することができるので、パフォーマンスに関連しそうなリリースの履歴を入力しています。 noteはAPI経由でも追加できるので、設定すると便利かもしれません。

https://api.speedcurve.com/#add-a-note

BROWSER WATERFALL

ファイルの読み込まれる順番が確認できます。 f:id:lcl-engineer:20170619095505p:plain Chromeのデベロッパーツールなどでも確認はできますが、以下の点が見やすくて良いと思います。

  • レンダリングブロックしている要素がわかりやすい(斜線マークがブロッキング要素)
  • 要素をクリックすると詳細情報が表示される

参考 SpeedCurve | SpeedCurve Waterfall

他のツールとの連携

データベースへ保存

テスト結果は社内ツール用のデータベースにも保存しています。 以下のAPIを使ってデータを取得しています。
SpeedCurve v1 API

Re:dashで表示

データベースへ保存したデータは、Re:dashで表示しています。
以下のグラフは、複数ページの結果を表示したものです。 SpeedCurveでは表示できない形式ですが、このように重ねると、どのページが遅いのかがひと目でわかります。 f:id:lcl-engineer:20170628112059p:plain

chatworkへ通知

first byte, start render, speedindex, visually complete, Google PageSpeed Insightsスコア をchatworkへ通知しています。 通知を出すようにしてから、日々の変化に気づきやすくなり、また、担当者以外にも関心を持ってもらえるようになったことが良かったです。 f:id:lcl-engineer:20170628052734p:plain

以上です。 まだ使えていない機能もあるのですが、しばらくはこちらで計測を続けていこうと思います。

Baltoを利用して素早くサービスを改善する

LCLでは、今年の初めから開発フィードバックのツールとしてGoodpatch様のBaltoをメインで活用しています。 先日、Goodpatch様へ事例として取り上げていただきましたが、より具体的な利用方法について紹介します。

goodpatch.com

Baltoとは

Webサイトやモバイルアプリに対して、簡単にスクリーンショット・動画付きのフィードバックを送れるツールです。フィードバックした内容は、スレッド別に管理され、それぞれにコメントやステータス管理ができます。

f:id:lcl-engineer:20170614100938g:plain

以前はIssueトラッカーを利用していましたが、スマートフォンからフィードバックはかなり面倒でした。(スクショを取って、チャット等を経由でPCに渡して、PCでIssueトラッカーへ添付 & コメント )

Baltoの場合は、チャットのように気軽に送れて、Issueトラッカーのようにステータス管理もできるので、それぞれのいいとこ取りのツールだと感じています。

利用フェーズ

開発中の中盤から最後にかけて利用するが多いですが、サイトの動きが大きく変わるような改善の場合は、初期段階から利用するケースもあります。また、プロダクションのサイト・アプリをBaltoに登録しておき、ふとした時に気になったことをフィードバックするという活用もしています。

Baltoにはいくつかプランがありますが、弊社が利用しているスモールプランの場合はプロジェクトが4つまでしか登録できません。そのため、完了したプロジェクトは、必要フィードバックがあればIssueトラッカーに転記し、プロジェクトは削除するという運用にしています。

導入方法

Webサイトの場合は、管理画面からURLを登録するだけで使えます。

iOS/Androidアプリの場合は、SDKを導入してアプリを配信します。

SDKの導入ページがあるため、こちらの通りにやれば特に詰まることなく、実装できるかと思います。

なお、Balto SDKはiOS9以上が対応ですが、弊社のアプリはiOS8から対応しているため、BaltoのSDKが導入できません。そのため、Balto専用のブランチを用意し、iOS9からがターゲットとなるように一時的に書き換えて導入しています。nアプリの配信はAPIが用意されているため、faslaneに組み込んでビルド・配信は自動化しており、一度導入してしまえば手間なく配信できます。

lane :balto do
  ・・ビルド部分省略・・
   system("curl -F project_token=[project_token] -F user_token=[user_toke] -F package=@./../[アプリ名].ipa -F ready_for_review=1 https://balto-api.herokuapp.com/api/v2/builds/upload")
end

テスト配信に関しては、Fabric/Crashlyticsも併用しています。使い分けとして、基本的にはBaltoを利用してフィードバックサイクルを回し、最終チェック段階でFabric/CrashlyticsでBalto SDKを組み込んでいないアプリ配信しチェックしています。

まとめ

Baltoを利用すること、ちょっとした隙間時間でフィードバックできるようになり、フィードバックの数が増えたと感じています。

サービスを良くするには、いかにフィードバックを集めて地道に改善していくかが、重要な要素の一つであると捉えています。気軽にフィードバックができ、漏れなく管理が行えるBaltoは、フィードバックサイクルを回すプラットフォームとして、適していると思ってます。

1ヶ月間の無料トライアルができるようなので、みなさんも是非使ってみて下さい。

www.balto.io

AWS EC2 パラメータストアを利用した秘匿情報管理

DBの接続情報・APIキーなどの秘匿情報は、git管理下に置くべきではないですが、皆さんはどのようなに管理しているでしょうか?

先日のAWS Summit Tokyo 2017のDMM様の事例で、EC2 System Managerパラメータストアの紹介がありましたので、使ってみました。

【資料公開】AWS Summit Tokyo 2017にてDMMのAWS移行について紹介してきました - DMM.comラボエンジニアブログ

EC2 SystemManager パラメータストアとは

簡単に言うと、key/value形式のパラメータをAWSで集中管理できる仕組みだと理解しています。

主に、以下の特徴があります。

  • AWS API、CLIを利用してアクセス可能
  • KMSを利用してパラメータ値の暗号化が可能
  • IAMを利用して、各パラメータへのアクセス権を細かく制御可能

管理コンソールからの登録

EC2 Managemtn Consoleの左下に、Parameter Storeのメニューが有ります。Parameter Storeへアクセスするには、事前に、IAM Policyの「AmazonSSMFullAccess」を付与しておきます。 ( AmazonSSMFullAccessは、他の権限も含まれているため、厳密に権限管理する場合は、独自にポリシーを作成したほうが良いかと思います)

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

登録画面は、至ってシンプルです。この画面で登録するだけで、AWS CLI等で参照できるようになります。

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

AWS CLIからの参照

EC2インスタンスから、AWS CLIを使ってアクセスする方法を紹介します。

「AmazonSSMReadOnlyAccess」PolicyをAttachしたIAM Roleを作成し、EC2に割り当てておきます。

以下のコマンドでパラメータの取得可能です。

aws ssm get-parameters --region ap-northeast-1 --name production.db.user
->
{
    "InvalidParameters": [],
    "Parameters": [
        {
            "Type": "String",
            "Name": "production.db.user",
            "Value": "db_user"
        }
    ]
}

valueだけ取得したい場合は、queryを利用して以下のようにとれます。

aws ssm get-parameters --region ap-northeast-1 --name production.db.user  --query "Parameters[0].Value" --output text
-> db_user

パラメータストアのアクセス制御

ポリシーを独自で作成することで、細かいアクセス制御が可能です。 以下のポリシーの場合は、productionで始まるパラメータのみにアクセス可能です。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1482841904000",
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameters"
            ],
            "Resource": [
                "arn:aws:ssm:[region]:[AWSアカウント]:parameter/production.*"
            ]
        }
    ]
}

パラメータの命名ルールとして、環境名やサービス名を入れておくと、環境やサービス単位で適切な制御ができると思います。

KMSを利用した暗号化

暗号化については、クラスメソッド様の記事が非常に参考になりますので、紹介だけに留めておきます。

AWS KMS で EC2 Systems Managerパラメータストアを暗号化 | Developers.IO

Railsで利用する

例えば、Railsのデータベース接続情報で利用する場合は、database.ymlで環境変数から読み込むようにします。

production:
  <<: *default
  database: db
  username: <%= ENV['DATABASE_USER'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  host: <%= ENV['DATABASE_HOST'] %>
  port: <%= ENV['DATABASE_PORT'] %>

Railsの起動ユーザの .bash_profileで、パラメータストアから値を取得して環境変数にセットしておけば、Railsアプリから参照できるようになります。

export DATABASE_USER=$(aws ssm get-parameters --region ap-northeast-1 --name production.db.user --query "Parameters[0].Value" --output text)
export DATABASE_PASSWORD=$(aws ssm get-parameters --region ap-northeast-1 --name production.db.password --with-decryption --query "Parameters[0].Value" --output text)
export DATABASE_HOST=$(aws ssm get-parameters --region ap-northeast-1 --name production.db.host --query "Parameters[0].Value" --output text)
export DATABASE_PORT=$(aws ssm get-parameters --region ap-northeast-1 --name production.db.port --query "Parameters[0].Value" --output text)

サーバプロビジョニングを自動化する場合は、Chef等で自動的に上記の内容をセットするようにしとけば良いと思ってますが、他にもっといい方法があるかもしれません。

まとめ

Railsでの秘匿情報を管理する方法としては、「yaml_vault + AWS KMS 」が良いと思ってましたが、EC2 System Managerのパラメータストアも手軽にセキュアな環境が構築でき非常におすすめです。EC2 System Managerは地味ですが、他にも良い機能があるので、今後は是非活用していきたいと思います。

参考

EC2 Systems Manager のパラメータストアを利用したアプリケーション環境設定の管理 | Developers.IO

AWS Key Management Serviceを使ってconfigファイルを暗号化すると便利 - Qiita

パフォーマンス計測、はじめました(フロントエンド編)

フロントエンドエンジニアの岡田です。 LCLでは、フロントエンドのパフォーマンスを計測するために、SpeedCurveを導入しました。 speedcurve.com

検討した計測サービス

導入にあたっては、以下のサービスも検討しました。
最初は、RUM (Real User Monitoring)ができるサービスを中心に探していたこともあり、以下の2社を検討しました。

site24x7

良い点
  • RUMの割には費用も低め(2,000万PVで$506〜 / 月額:調査当時)
  • 日本語対応もされている(ちなみに、サポートメールは日本語で送っても英語で返ってきました)
導入を見送った理由
  • RUMの結果は思ったほど詳細が見えなかった(見方がわからなかっただけかもしれませんが。。)
  • RUMなしのプランも検討したが、ユーザーエージェントの設定が効かないことがあり、スマホページの計測ができない場合があった(PCが計測されていた) www.site24x7.com

New Relic

New Relは、サーバーのパフォーマンス監視のためにすでに入っていましたが、このサービスでRUMを見ることができます。 RUMの見方は、以下のページがとてもわかりやすかったです。
New Relic Browser : ユーザーのパフォーマンス(RUM)も計測できるよ - Qiita

良い点

すでに導入済みのサービスで使える

導入を見送った理由

費用が高い(50万PVで$149〜 / 月額:調査当時。50万PVを超えるとどうなるのかは不明)

newrelic.com

SpeedCurve

良い点
  • RUMは高い($800〜 / 月額)がSynthetic Monitoringはお手頃($20〜 / 月額)
  • トライアル中の結果が安定している
  • 英語だけでもUIがわかりやすい speedcurve.com

RUM(Real User Monitoring)の導入を止めた理由

最初はRUMで計測できるサービスを探していましたが、以下の理由から今回はSynthetic Monitoringを選びました。

  • RUMはすでに導入済みのNew Relic(Lite)で見ることもできる(ただし見えるデータに限りがある)
  • RUMの場合、同じユーザーが毎日決まった時間にアクセスしてくれるわけではないため、定点観測しにくい(サイトの改修で問題が起きても気づきにくい)
  • RUMは高い

ということで、SpeedCurveを導入しました。

次回は、SpeedCurveを使って、どのようにパフォーマンス改善をしているかをお伝えしたいと思います。

2017/6/7 追記

New Relic には New Relic Synthetics という定点観測サービスがあり、無料〜で使えるそうです。 Twitterで教えていただきました。ありがとうございます。

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
        });
      })
    }
  }
});

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