2018年版 Misoca に住んでいる便利な bot の紹介

f:id:mizukmb:20180223153144p:plain

初めまして、2月に入社した id:mizukmb です。

beatmania IIDX というゲームが好きなのですが、SP九段の道のりが遠くまだまだ鍛錬が必要と感じている今日この頃です。

私が Misoca に入社して驚いたことの一つに、 bot を利用した開発やコミュニケーションが活発 というのがあります 1時報やリマインダからリリースのオペレーションまで様々です。

今回は、 Misoca に住んでいる便利な bot をいくつか紹介したいと思います。

ちなみに、 2015年にも一度 id:mzp さんが bot を紹介した記事を公開しているので、そちらも合わせてご覧ください。

tech.misoca.jp

misocat

主に開発周りをサポートする bot です。

未リリース状態の GitHub PR の一覧を表示してくれたり、リリース用の GitHub PR を作ってくれます。 misocat リリース前misocat リリースしたい といった発言に反応します。

f:id:mizukmb:20180222182533p:plain

他にも、 GitHub の URL に反応して、自動でタイトルを教えてくれます。Slack では Private なレポジトリの場合は内容が展開されないので、こちらの機能は地味に便利で重宝しています。

f:id:mizukmb:20180223152350p:plain

misocat は Ruboty で動いていて、 sfruboty という名前でレポジトリを公開しています。他にも様々な機能がありますので、気になる方はそちらもご覧ください。

github.com

sfbot

GiHub 上の様々なことをやってくれる bot です。

先ほど紹介した misocat にmisocat リリースしたい とお願いした時、実際にリリース用 PR を作るのがこの sfbot です。

f:id:mizukmb:20180223120805p:plain

他にも、誰かが GitHub PR に approve した時に label をつけてくれたり

f:id:mizukmb:20180223143919p:plain

2つ以上の approve がつくと「マージしましょう!」とメンションしてくれます。

f:id:mizukmb:20180223180802p:plain

優しさですね。

自動 esa やり機

こちらは以前 id:kokuyouwind さんがブログにて紹介しています。

tech.misoca.jp

簡単に説明すると、定期的に esa 記事を WIP 状態で作成してくれる bot です。ハッシュタグをつけることで、決まった曜日に投稿するとか、特定の Slack チャンネルに通知するなどの操作が可能となっています。

私は平日のみ日報記事を作成して、自分の分報チャンネルに通知するように設定しています。

f:id:mizukmb:20180223145522p:plain

個人の日報や週次 MTG の議事録記事を作成するように設定されることが多いです。

Geekbot

Geekbot とは、朝会を非同期に行うためのチャットボットサービスです。

geekbot.io

Geekbot から決まった時間に DM でいくつか質問されるので答えていきます。

f:id:mizukmb:20180222183758p:plain

全ての回答を終えると、こんな感じに指定したチャンネルに投稿され、昨日やったこと・今日やったことなどがチームメンバー間で共有できます。

f:id:mizukmb:20180222184159p:plain

事前に朝会で話すことを共有できたり、非同期に朝会を行い、メンバーが集まる時間を無くしたりしています。リモートで働く方もいるので、テキストベースで朝会の内容を共有できるのは非常に便利に感じています。

Dependabot

Dependabot はパッケージ管理されたファイルの継続的なアップデートを支援する SaaS です。

dependabot.com

Misoca では Gemfile や package.json を対象としています。

下の図のように gem や npm パッケージごとにバージョンアップデートを行った GitHub PR を自動で作成してくれます。

f:id:mizukmb:20180223112516p:plain

こうしたこまめなアップデート、大切ですよね。

まとめ

Misoca に住んでいる便利な bot をいくつか紹介しました 2

自分達で運用してるものから、 SaaS として提供されている bot まで色々あります。 bot のおかげで単純作業は自動化されますし、オペミスも減らせるので安心できます。自分達はやるべきことに集中できるのでいい感じですね。

採用

Misoca の bot 事情がきになる方はお気軽にお尋ねください 🤖

Misoca採用情報


  1. 他にはみんな帰るのが早いとか

  2. 今回紹介できませんでしたが、他にも bot が住んでいます

Railsをお手軽に運用するためのAWS Elastic Beanstalk

こんにちは、@corocn です。

今年は例年より寒いですね。私がリモートワークしている部屋も、隙間風が強くて寒いです。

f:id:corocn:20180209101355j:plain:w500

※ 写真は自宅のニワトリ小屋です

新しいRailsのインフラ環境構築

先日この開発者ブログで、@mugi_uno が 新しいRailsのフロントエンドについて紹介してくれました。

Turbolinks、時々Vue.js - Misoca開発者ブログ

今回は、同アプリケーションのインフラ環境で活用しているAWS Elastic Beanstalkについて紹介したいと思います。

AWS Elastic Beanstalk とは

f:id:corocn:20180214142028p:plain:w300

AWSの各種サービスを組み合わせたPaaSです。 環境作成時に、ロードバランサ、オートスケーリング、ローリングアップデートなどの機能が自動設定され、Elastic Beanstalkから一括管理できるようになります。

ウェブサーバーはEC2インスタンス上に構築されますが、自動で作成されたインスタンスは、通常作成のEC2インスタンスと同様に、EC2マネジメントコンソールの一覧に表示されます。ロードバランサなども同様です。

Elastic Beanstalkを選択した理由

インフラ専任のエンジニアが存在しない状況で、 可能な限りインフラ設計に開発コストを割かないようにするためにPaaSを選択しています。 その時に検討したサービスが、以下の2サービスでした。

  • Heroku: DevOpsツールとして運用している実績がある
  • Elastic Beanstalk: 現在のMisocaのプロダクションはAWSで運用されている

手軽感はHerokuのほうが強いですが、 最終的にElastic Beanstalkを使わないAWSでの運用へ移行することを視野に入れてましたので、後者を選択しました。

既存のプロダクションのしがらみがない状態であれば、Herokuおよび別のPaaSを検討したかもしれません。

構成

Elastic Beanstalkには「アプリケーション」と「環境」という概念が存在し、1つの「アプリケーション」の下に、複数の「環境」を作成して運用します。

f:id:corocn:20180214140443p:plain

環境の種類にはWebTierとWorkerTierが選べます。

  • Web: ウェブサーバー用。ロードバランサなどが自動作成される。
  • Worker: ジョブサーバー用。メッセージキュー(SQS)が自動作成される。

shoryuken でSQS対応する選択肢もあるのですが、 今回は使い慣れているDelayedJobを利用しています。

RDSを環境内に含めることも可能ですが、 環境を削除した際にDBまで消してしまうのが怖かったので、手動で作成してアタッチする方法をとっています。

構築してみて

良かったこと、辛かったこと(現在進行形で辛いこと)を以下に並べてみました。

良かったこと

  • カスタマイズ性が高い
    • レポジトリの.ebextensions 以下にconfigを配置することで、設定やスクリプトの実行を行える
    • ファイルの配置やシェルスクリプトの実行ができるので、実質なんでもできる
    • yarn、fluentdの導入、cronの起動などebextensionを使用してコントロールしている
  • AWSの別サービス群との連携が簡単
    • Kinesis Firehoseでログ収集の設定が簡単に構築できた
    • Code Pipelineを使ったGithubとの連携(push時の自動デプロイなど)が簡単に構築にできた
  • 環境立ち上げが楽
    • コマンド一発でレビュー環境が立ち上がって素敵です

また、AWS全体の話になりますが、今回初めて技術サポートを活用してみました。 対応が早くて非常に助かりました。

辛かったこと

  • ドキュメントの少なさ
    • プラットフォーム全般のドキュメントが中心で、Ruby/Railsに特化したものが少ない(個人ブログ等も含めて)
    • AWSの技術サポート契約をオススメする
  • .ebextensionsのクセが強い
    • 慣れるまで多少時間がかかる
    • 環境作成時やデプロイ時のライフサイクルをちゃんと理解しておいたほうが良い
  • 最新バージョンへ追従が遅れる
    • 前提としてMisocaでは常に最新環境を使おうとしている(2.5.0の時はリリースの翌日に本番反映したほどです)
    • 現在はRuby 2.4.3までしか対応しておらず、2.5.0が使えない
    • 公式対応が待てない場合はPlatformを自作する方法があるが、試せていない

まとめ

導入時に少しだけ山がありますが、慣れてしまえばAWSという強力なサービス群の恩恵を受けることができます。 インフラ構築に大きく時間をかけれないような状況では強力なツールとなります。ぜひ検討してみてください。

また別の機会に.ebextensionsやデプロイ時の挙動を掘り下げた記事を書こうと思います!

採用

インフラ含め、Misocaの信頼性を一緒に支えてくれるエンジニアを大募集しています!

🦀松江オフィス勤務と蟹

mzpです。こんにちは。

名古屋にあるMisoca本社で勤務していることが多いが、松江オフィスで数日間勤務した。カニがいっぱい食べれて楽しかった。

f:id:mzp:20180208093045j:plain

🏢勤務の様子

応接エリアなどもあって部屋自体は広い。一人あたりの面積では名古屋の本社よりも広い。 松江オフィスにはあまり来ないので記念撮影をした。

f:id:mzp:20180209071819j:plain

普段は二人しかいないオフィスなので、外部モニタが足りない。

f:id:mzp:20180209071447j:plain

松江はお茶どころとして有名なので、オフィス近くの和菓子屋さんとお茶屋さんで買ってきておやつにしした。急須やコップなどはない。

f:id:mzp:20180209064213j:plain

近所の定食屋でぶりの照り焼きが安すぎて困惑した。650円だった。

f:id:mzp:20180207025630j:plain

そばも食べた。そば屋のカツ丼はうまいという法則がある。

f:id:mzp:20180208032004j:plain

🦀かに小屋

かに小屋松江駅の近くで開催されていたので、みんなで行った。

f:id:mzp:20180208095801j:plain

1杯づつかにに課金する。

f:id:mzp:20180208100705j:plain

もいで焼く。

f:id:mzp:20180208101105j:plain

かに以外にもあわびなどがある。

f:id:mzp:20180213151626j:plain

🌅宍道湖の夕日

仕事を途中で抜けて宍道湖に沈む夕日を見にいった。雪道に足を取られて日没後にしか着けなかったがキレいだった。 毎日はこれが見れるのうらやましい。

f:id:mzp:20180208084336j:plain

✈️移動

豪雪と予定がかぶったので、予定していた飛行機が飛ばず移動は苦労した。

f:id:mzp:20180206012557j:plain

新幹線と特急やくもを乗り継いで移動した。予定の5倍くらい時間がかかった。

f:id:mzp:20180211164830p:plain

宿からオフィスへの移動も雪道で大変だった。

f:id:mzp:20180211164957p:plain

👀松江オフィス見学会やってます

Misoca松江オフィスでは、エンジニアの方を対象に平日夕方の2枠で見学会を開催しています。

Misoca会社見学会 at 松江オフィスのお申し込み

Misoca の雰囲気や文化など、Web サイトなどでは知ることが難しい Misoca の情報を知っていただけると思います。興味のある方は、ぜひ上記よりお申し込みください!

松江オフィス|拠点紹介|Misoca採用情報

からもエントリーできます。

Turbolinks、時々Vue.js

こんにちは、@mugi_unoです。

今年は雪がスゴいですね。
暖かい我が家でリモートワークのありたがみを日々噛み締めています。

先日は雪だるまを作りましたが、想像とは違う仕上がりになりました。

娘は「これじゃない!」と不満そうでした。

f:id:mugi1:20180129173108p:plain

新しいRails&フロントエンドの環境構築

さて、最近Misocaでは、新規Railsアプリケーションのフロントエンド環境を整える機会がありました。

チームで検討した結果、Turbolinks&Vue.jsを採用しており、
その際に得られた知見を紹介したいと思います。

Turbolinksを使う理由

TurbolinksはGoogleで検索するとサジェストがこんな感じになるほど無効化されがちです。

f:id:mugi1:20180129173204p:plain

  • 利用するために理解すべき独自の挙動がある
  • ゴリゴリにフロントエンドを触っている人からすると
    「そこまでやらないで〜〜!」といった気持ちになる

といった具合に避けられがちだと予想していますが、上手く扱うと非同期通信をベースとした軽快に動くUXをお手軽に実現できます。

同様のものをSPAで構築するには、 フロントエンド周りで多くの知識が必要となります。

フロントエンドエンジニアが中心となってメンテナンスをしていけるのであれば問題ありませんが、サーバサイド/フロントエンドの双方を多くのエンジニアが面倒を見るような状況では、最低限のコード量で実現できることが大きいメリットになってきます。

Turbolinksで気をつけるべきこと

特に注意すべきなのが初期化周りです。

一般的にはDOMContentLoadedイベントやjQuery.readyなどで、適宜必要な初期化スクリプトを実行することが多いかと思いますが、Turbolinksの場合はbodyを差し替えるだけになるため、イベントが発火せずに上手く動作しなくなります。

画面遷移時に適宜documentに対してturbolinks:loadイベントが発火されるため、そちらをハンドリングしてあげることで上手く初期化できるようになります。

このあたりは、TurbolinksのREADMEにも説明があります。

https://github.com/turbolinks/turbolinks#full-list-of-events https://github.com/turbolinks/turbolinks#observing-navigation-events

画面ごとの初期化をどのようにするか?

実際には画面に応じて実行したい初期化処理が異なるケースが多く、都度適切なスクリプトを実行する必要があります。

今回は、RailsのController/Actionを元に、適宜必要な初期化スクリプトを判別してロードするような仕組みとしました。

実際のコード例は以下です。(要点のみ抜粋しています)

  • application.html.haml : bodyにControllerとActionを元にしたキーを埋め込む
%body{ 'data-js-initializer': "#{controller.controller_name}_#{controller.action_name}" }
  • application.js : headに埋め込み、ページ全体のロード時一度だけ実行する
import Turbolinks from 'turbolinks';
import initialize from './initializers';

Turbolinks.start();

// Turbolinksで遷移した場合の初期化処理
document.addEventListener('turbolinks:load', initialize);
  • initializers/index.js : 画面ごとの初期化処理を実行する
import * as initializers from './';
import camelcase from 'camelcase';

export default function () {
  // bodyに埋め込まれたキーを取得
  const initializerName = $('body').data('js-initializer');

  // 対応する初期化スクリプトを取得し、存在すれば実行する
  const initializeScript = initializers[camelcase(initializerName)];
  if (initializeScript) {
    initializeScript();
  }
}

これにより、たとえば OrdersController#editに遷移した場合には、
initializers/ordersEdit.jsがコールされるようになります。

#new, #edit などで同じスクリプトとなる場合は、 共通ファイルを1つ作成し、別名でエクスポートすることで対応しています。

export {
  ordersForm as ordersNew,
  ordersForm as ordersEdit,
} from './ordersForm';

Controller/Actionを元に初期化するのは好みが別れるところもありそうですが、

  • 初期化時にファイルをどこに置くか考えなくていい
  • 命名規則が強制的に揃う
  • どの画面でどの初期化スクリプトが実行されてるかすぐにわかる

といったメリットがあるため、今回はこのような形としました。

Vue.jsと組み合わせる

Vue.jsと一緒に利用する場合は、Turbolinksによる画面遷移時のキャッシュとの兼ね合いや、インスタンスの破棄を適切に行えるよう考慮してあげる必要があります。

このあたりについては、Turbolinksリポジトリwikiに説明と対処用のコード例が記載されており、そちらを参考にすることで実現できます。

VueJs and Turbolinks · turbolinks/turbolinks Wiki · GitHub

内容的には、TurbolinksAdapterというmixinを作り、turbolinks:visitのタイミングで1度だけイベントが発火するような仕組みのようです。

TurbolinksAdapter の適用し忘れが怖い

当然と言えば当然ですが、上記TurbolinksAdapterは Vue.jsに適用してあげないと正常に動作しません。つまり、TurbolinksAdapterを適用し忘れて、単純にVue.jsのみをimport/requireしてしまった場合には期待通りの動作とはなりません。

かといって、利用する箇所すべてで意識したくはないですよね。

ローカルパッケージ化しておく

というわけで、Vue.jsをローカルでパッケージ化し、単純にvueをimport/requireした場合にはTurbolinksAdapterが適用されたものが得られるようにしました。

このあたりは、以前Vue.jsのバージョンを0.12→2.4にアップグレードした際と同様の手法です。

tech.misoca.jp

  • 内部にパッケージを作ります。

frontend/private_modules/vue/package.json

{
  "name": "vue",
  "version": "1.0.0",
  "description": "Vue with turbolinks",
  "main": "index.js",
  "dependencies": {
    "vue": "^2.5.13"
  }
}

frontend/private_modules/vue/index.js

const Vue = require('vue').default;
// https://github.com/turbolinks/turbolinks/wiki/VueJs-and-Turbolinks
const TurbolinksAdapter = require('./TurbolinksAdapter');

Vue.use(TurbolinksAdapter);

module.exports = Vue;
  • アプリケーション側からのVue.jsの参照をローカルパッケージに向けます

package.json (抜粋)

{
  "dependencies": {
    "vue": "file:./frontend/private_modules/vue"
  }
}

これで、Turbolinksでの画面遷移時のVueインスタンスの処理を、開発時に特に意識せずに行えるようになりました。

Turbolinksの外し方

Turbolinksを使うのは少し冒険だな〜と感じる方もいるかもしれませんが、上記構成の良いところとして、「Turbolinksを外そうと思えば、わりと簡単に外せる」という側面があります。

「外すのかよ!」という声が聞こえてきそうですが、出来る限り依存を浅くしておくのはTurbolinksに限らず大事なことだと思ってます。

もし外したい場合には以下の手順を踏むだけです。

  • application.jsの初期化処理をDOMContentLoadedのバインドに変える
  • TurbolinksのimportとTurbolinks.start();を消す
  • package.json上のvueの参照をローカルパッケージから、通常のVue.jsに戻す

これで安心して使うことができます。


というわけで、TurbolinksとVue.jsを使った環境構築時の話でした。

興味が湧いた方は、一度試してみてはいかがでしょうか?


Misocaでは、いろんなフレームワークにチャレンジしたいエンジニアを募集中です!

デプロイ時に Bugsnag にソースマップをアップロードして人に優しい Stacktrace にする

こんにちは、 Misoca 開発の @lulu-ulul です。

ニュースを見ていると各地で例年以上の積雪を記録しているみたいですね。皆様も引き続き御気を付けください。

私もそれなりに雪が降る場所に住んでいて当然積雪しているのですが、今年は雪が積もっても早起きして車が出られるまで行う雪かき・倍以上になる通勤時間から解放されてほぼ普段通りの生活ができています。リモート勤務の良さを改めて実感しています。

背景

Misoca ではクラッシュレポートの蓄積・解析サービスとして Bugsnag を使っています。

www.bugsnag.com

Rails のエラーログだけでなく、JavaScript のエラーログも対象にしています。

しかし JavaScript ファイルは minify されていますので、 bugsnag の stacktrace はデフォルトではこんな感じになってしまいます。

f:id:lulu-ulul:20180126104231p:plain

つらい(つらい)。

解決方法

Bugsnag 側でソースマップをアップロードする機能が提供されています。

Bugsnag docs › API › JS source map upload

デプロイ時にソースマップファイルを自動的にアップロードさせれば幸せになれそうですね。

方針

Misoca では本番環境には map ファイルを出力しない様にしていました。

また、デプロイ処理は CodeBuild を利用しています。

それを踏まえると以下の様なステップで進める事になります。

  1. 本番環境でのビルド時にソースマップ(以下 map ファイル)を生成させる様にする
  2. CodeDeploy で実行させる以下の処理を行うスクリプトを実装する
    1. map ファイルのパス一覧を取得する
    2. map ファイルを Bugsnag にアップロードする
    3. map ファイルを削除する

1. 本番環境でのビルド時にソースマップを生成させる様にする

Misoca では現在 Webpacker で管理されているものと、 gulp で管理されているものの2つが共存しています。

Webpacker

Misoca では環境毎に分割しているので本番環境の設定ファイルを変更するだけです。

ソースマップの生成に関しては いくつか種類がありますが、今回は map ファイルを別ファイルとして生成したいので source-map を指定する様にしました。

gulp

gulp の方は Browserify のファイルストリームを vinyl オブジェクトにして扱っています。 構成は少し違うのですが、 gulp が提供している recipe があるので参考にして設定します。 ソースマップ生成にあたってストリームを一度バッファに貯める必要がある様でなるほど!という感じです。

gulp/browserify-uglify-sourcemap.md at master · gulpjs/gulp · GitHub

実際は Babel 使う様にしたり条件に応じて watchify で wrap したりと色々いい感じにしてるのですが、煩雑になるので省くと以下の様な形になります。

var gulp = require('gulp');
var util = require('gulp-util');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
var sourcemaps = require('gulp-sourcemaps');

function build_js(development) { // devlopment でどの環境かを管理
  var bundler = browserify({
    // options
  });; 

  return bundler
    .bundle()
    .pipe( source( 'main.js' )) // vinyl のオブジェクトに変換
    .pipe(development ? util.noop() : buffer()) // buffer を使う
    .pipe(development ? util.noop() : sourcemaps.init())
    .pipe(development ? util.noop() : sourcemaps.write('./'))
    .pipe( gulp.dest( '../public/js/' ));
}

gulp.task('build:js', function() {
  return build_js(false);
});
 
gulp.task('build:js:dev', function() {
  return build_js(true);
});

gulp-utilnoop() を使うと「何もしない」という処理で chain できるよ、と教えてもらったのですっきり書けました。

2. CodeDeploy で実行させるスクリプトを実装する

EC2インスタンスで実行させるための準備がほぼ不要ですし、既存のスクリプトと統一したかったのでシェルスクリプトで記述する事にしました。

ここで気をつけたのは以下の点です。

  • ソースマップのアップロードで失敗してもデプロイ自体は失敗させない
    • AppSpec 側の設定では難しそうなので、シェルスクリプト側で例外処理を行い失敗時に非常終了のシグナルを外に投げない様にします
  • 上記の場合に map ファイルを残したままアプリケーションを立ち上げない様にする
    • 例外処理の周りで必ず削除される様にする
  • CodeDeploy で複数ノードを立ち上げるがその内の一つでのみ実行する様にする
    • EC2 のインスタンスのタグを使い特定のノードのみでアップロード処理が行われる様にする

以上を踏まえて作成したシェルスクリプトは以下のようになります。マスクしたり削ったりしている場所もあります。

#!/bin/bash

source ~/.bash_profile
export AWS_CONFIG_FILE=AWS_CONFIG_FILE_PATH
cd APPLICATION_ROOT_PATH

# trap で終了のシグナルを拾って map ファイルを削除する
trap 'find ./public/{js,packs} -name "*.js.map" | xargs -n 1 rm -f --' EXIT

# 環境変数からBugsnag の API key が取得できない場合はスキップ
BUGSNAG_JS_API_KEY=$(bundle exec dotenv bash -c 'echo $BUGSNAG_JS_API_KEY')
if [ -z "${BUGSNAG_JS_API_KEY}" ];then exit 0;fi


# 指定のROLE-DEPLOYMENT_ROLEの時だけ実行する
# EC2 のインスタンスメタデータの中からタグの情報を取得する
INSTANCE_ID=`curl -s http://169.254.169.254/latest/meta-data/instance-id`

TAG_ROLE=` aws ec2 describe-instances --instance-ids ${INSTANCE_ID}  --query 'Reservations[].Instances[].Tags[?Key==\`Role\`].Value' --output text`
TAG_DEPLOYMENT_ROLE=` aws ec2 describe-instances --instance-ids ${INSTANCE_ID}  --query 'Reservations[].Instances[].Tags[?Key==\`DeploymentRole\`].Value' --output text`

if [ "${TAG_ROLE}" == "Job" ] && [ "${TAG_DEPLOYMENT_ROLE}" == "Utils" ]
then
  # 別のシェルスクリプトでアップロード処理を行う
  # タイムアウトもしくは他の例外が発生すると 0 以外のシグナルが飛ぶ
  timeout -sKILL 300 bash ./script/codedeploy/upload_sourcemaps_to_bugsnag.sh

  # 0 以外のシグナルの時 = タイムアウト等による失敗した時
  if [ $? != 0 ]
  then
    # slack にエラー通知
    MESSAGE="BugsnagへのSourceMapアップロードに失敗しました…"
    RAILS_ENV=production bundle exec thor utils:slack:ping --channel "#target_channel" --message "${MESSAGE}"
    # 0 でシグナルを投げる事で例外発生時も trap で拾える様にする
    exit 0
  fi
fi

アップロード処理部分はタイムアウト処理を行いたいため別ファイルに逃がしています。

EC2 インスタンス内から http://169.254.169.254/latest/meta-data/instance-id を叩く事で INSTANCE_ID を取得しています。 それを元に ec2 にリクエストを投げ、クエリにより指定したタグの値を取得します。

$? で直前のコマンドの終了シグナルを取得できるので、正常終了の 0 以外の場合は Slack への通知を行い、 exit 0 で正常終了のシグナルを投げることで trap で検知される様にしました。 これにより Ruby における ensure の様な処理を実現し、このスクリプト内で完結する様にしています。


次に実際にアップロード処理を行っているスクリプトです。

upload_sourcemaps_to_bugsnag.sh

#! /bin/bash

source ~/.bash_profile
export AWS_CONFIG_FILE=AWS_CONFIG_FILE_PATH
cd APPLICATION_ROOT_PATH

# dotenv で管理している API KEY を取得
# dotenv が引数にコマンドかファイル以外を想定していないため `bash -c` により実行
API_KEY=$(bundle exec dotenv bash -c 'echo $BUGSNAG_JS_API_KEY') 

# EC2 インスタンスのIDを取得
INSTANCE_ID=`curl -s http://169.254.169.254/latest/meta-data/instance-id`

# EC2 インスタンスのタグから現在のインスタンスのサービス名等を取得
SERVICE_NAME=` aws ec2 describe-instances --instance-ids ${INSTANCE_ID}  --query 'Reservations[].Instances[].Tags[?Key==\`ServiceName\`].Value' --output text`
URL_PREFIX="https://${SERVICE_NAME}/"
SOURCE_DIR="./public/"

for ORIG_PATH in $(find ./public/js ./public/packs -name "*.js.map")
do
  MAP_PATH=${ORIG_PATH#*./public/}
  JS_PATH=${MAP_PATH%.map}
  # fingerprint の部分をワイルドカード指定に変更
  MINIFIED_URL=$URL_PREFIX${JS_PATH/-*.js/-*.js}

  curl https://upload.bugsnag.com/ \
   -F apiKey="$API_KEY" \
   -F minifiedUrl="$MINIFIED_URL" \
   -F sourceMap="@$SOURCE_DIR$MAP_PATH" \
   -F minifiedFile="@$SOURCE_DIR$JS_PATH" \
   -F overwrite=true
done

find コマンドで map ファイルを検索し、それぞれを以下のドキュメントに従ってアップロードしています。 ローカルのファイルパスから URL を生成しパラメータを用意します。

https://docs.bugsnag.com/api/js-source-map-upload/

URL にはワイルドカードが指定できますので、fingerprint の部分は置換えてすっきりさせています。こうすることで bugsnag の Uploaded source maps に fingerprint 違いのファイルが別々に登録されていくのを防ぐ事ができます。

ステージング環境等複数の環境から同じ Bugsnag の JSプロジェクトを使いまわしている場合は fingerprint を残しておくとそれぞれに対応した map ファイルが参照されます。

結果

f:id:lulu-ulul:20180126104259p:plain

上がソースマップが反映されたもので、下がソースマップが適用されてない状態のものです。

人間に優しい表示にすることができました!


Misoca 社では、日々開発環境を改善していく事に興味があるエンジニアを募集しています。

Ruby 2.5.0 までの道のり

こんにちは、id:eitoball です。年末・年始は、特別なことをすることなく、自宅で家族とまったりと過ごしていました。正月らしいことは、歩いて5分ぐらいの近所の神社へ初詣に行ったことでした。

昨年2017年12月25日に Ruby 2.5.0 が リリース されました。コミッタの皆様ありがとうございます。Misocaは、翌日26日に2.5.0を使うように更新をしました。今回は、2.5.0 への更新について何かを書こうとしていたのですが、特に苦労することなく更新することができてしまったので、Misoca で Ruby のバージョンの変遷を調べてみました。

f:id:eitoball:20180112100730p:plain

Ruby 1.9.3の時代(2013年から2014年頃)

サービス開始当初、2013年頃は、Ruby 1.9.3-p194 を使っていたようです。1.9.3-p194 は、2012年04月20日に リリース されていました。Misoca の開発が始まった頃の最新のバージョンで、しばらく、このバージョンを使い続けていたようです。

次は、2014年01月頃に Ruby 1.9.3-p484 へ更新しました。2013年12月25日には、バージョン 2.1.0 が リリース されていました。しかしながら、とりあえず、1.9.3 系の最新バージョンへ更新を選択しました。

Ruby 2.x へ(2014年から2016年まで)

Misoca が、Ruby 2.x を使うようになったのは、2014年04月24日に Ruby 2.1.1 へ更新したときでした。2.1.1 の リリース は、2014年02月24日で、この頃に 2.0 系や1.9.3 系もリリースされていましたが、最新の Ruby を使うことになりました。

2014年12月05日には、2.1.5 を使用するようになりました。2015年03月10日に 2.2.0 を使うようになりました。この頃には、2.2.1 が リリース されてたので、2.2.1 を使おうとテストをしていたら、 GC 周りに問題があることがわかり、2.2.0 を使うようにするか、 2.2.2 がリリースするまで待つか迷いましたが、2.2.0 を使うことにしました。

Ruby 2.2.3 を使うようになったのは、2015年08月20日でした。これ以降、以下のように最新のRubyを使うように追従していきました。

  • 2015年12月17日 2.2.4
  • 2015年12月26日 2.3.0
  • 2016年05月06日 2.3.1
  • 2016年11月21日 2.3.2
  • 2016年11月28日 2.3.3

Ruby 2.4 (2017年)

前述のように Ruby 2.2 後半から 2.3 を使うように順調に更新していましたが、2.4.0 を使うようになったのは、2017年03月03日でした。遅くなった理由は、Misoca 社では、前年から rrrspec を使用してテストを実行するようになりましたRuby 2.4.0 を使うように変更してから、ローカルで rspec を使って成功する spec が、時々、失敗するようになり結果が不安定でした。この原因の調査などに手間取ったためでした。

以降は、以下のように新しい Ruby を使うように更新していきました。

  • 2017年03月27日 2.4.1
  • 2017年10月03日 2.4.2
  • 2017年12月15日 2.4.3

Ruby 2.5(2018年)

最初に書いたように昨年末の2017年12年26日にリリースされた翌日に 2.5.0 を使うように更新しました。2.5.0 への更新のために2017年10月頃から、その頃リリースされた2.5.0-preview1 を使ってテストをして準備をしていました。

現在のところ、安定して稼働しており、この記事 で説明されているような改善による効果も確認されているようです。

さいごに

今回、初めて、Misoca が使用する Ruby の更新を振り返ってみました。振り返るにあたり、git のコミットログや GitHub のプルリクエストから、Misoca 社の変化を垣間見ることができ、楽しい経験でした。

Misoca 社では、Ruby の更新などにも興味があるエンジニアを募集しています。

CI実行時間を11分→5分に短縮する

メリークリスマス!🎄

Misoca開発の@kokuyouです。

Misoca Advent Calendar 2017、遂に最終日です。 昨日の記事は@eitoball開発の小ネタ劇場でした。

本日 Ruby 2.5 が出る予定ですが、特に関係ないCIの話をしていきたいと思います。

どうでもいいんですが、CIという略語を見ると Continuous IntegrationなのかCANDY ISLANDなのか迷う今日このごろです。

⏳ ビルド時間の推移

以前の記事で紹介したとおり、MisocaではCIでのRSpec分散実行にrrrspecを使用しています。

これにより20分ほどかかっていたビルドを11分程度まで短縮することができたのですが、それでも11分は結構待たされるな―、という気持ちになります。スピーチとCIのビルド時間は短いほうが良いのです。

そんなわけで、頑張って短縮してみました。

以下は7月~9月ごろのビルド実行時間推移を表したグラフです。

f:id:kokuyouwind:20171225142005p:plain

青線がビルドごとの実行時間、赤線が直前10ビルドの移動平均になっています。

y軸の上限を1000秒(=16分40秒)に取っていますが、初期はこの上限を軽々と突き抜けていることがわかります。 全体的にも600秒(=10分)はまず下回らない感じです。

一方で、対処を始めた8月ごろからグラフが右肩下がりになり、終盤では400秒を下回っています。

直近(10月~12月)では以下の通り、ほとんどのビルドで400秒(6分40秒)を下回り、軽いビルドでは300秒(=5分)程度で完了しています。 なお、たまに飛び跳ねてるのもDependabotで一気にビルドキューが積まれたときのものでした。

f:id:kokuyouwind:20171225142135p:plain

ここまで実行時間が短縮されたことで、待たされていると感じることがだいぶ少なくなりました。

💪 やったこと

💰 金の弾丸

最初は「rrrspecの並列数増やせばスケールするでしょ」と思いmax_workerを5から10に上げてみたのですが、ほとんど短縮せず悲しい気持ちになりました。

仕方がないので、ここからは時間がかかってる処理をひとつずつ調べて潰していく戦略にシフトしました。

🖥 MySQLのローカル同居

当初は レシピに組み込むのがめんどい MySQLの起動オーバーヘッドやメモリ量などを考慮して、rrrspec workerノードとは別にMySQLノードを立てていました。

しかしMySQLノードを固定で立ち上げているとスケールしないことや、接続数・転送量が逼迫していることから、MySQLをrrrspec workerノード内に同居させることにします。

datadirをtmpfsに向けるのに苦戦したり、AppArmorにMySQLプロセスの起動を阻まれたりしたのですが、なんとか完遂し、なぜかこれだけで1分程度実行時間が短くなりました。

帯域とか転送速度の問題でこれまでの戦略がイケてなかったという話なんですが、まぁ結果オーライです。

(この時点でjob全体10min、うちrrrspecが8min)

🔗 rsyncの設定見直し

さらにrrrspecの実行ログを確認したところ、実行対象をrrrspec master -> rrrspec workerへ同期するrsyncが1min以上かかっていました。

以下の設定を見直し、rsyncにかかる時間を5sec程度まで短縮しました。

  • Jenkinsではjobごとにworkspaceを持ちタイムスタンプが異なるため、 --timesではなく--checksum を使う
  • node_modulestmp/cacheなど不要なディレクトリが同期されていたため、--exclude指定に追加する

(この時点でjob全体9min、うちrrrspecが7min)

🔪 specの分割

MySQLがスケールするようになったためmax_workerを再度上げていったのですが、やはりあまり実行時間は短縮されませんでした。

個別に実行時間を見てみると、なんと単独ファイルで7minかかっているfeature specが……

rrrspecはファイル単位で分散実行するため、そりゃ7minは下回れないなぁ…… という気持ちになり、そのspecを幾つかのファイルに分割しました。

その後もボトルネックになったspecを分割したり不要なテストを消したりと地道に潰していった結果、10workerでのrrrspec実行時間を5minまで短縮することができました。

(この時点でjob全体7min、うちrrrspecが5min)

🏗 package install & build

オーバーヘッドの比率が上がってきたため、そちらで時間がかかっている処理に目を向けていきます。

オーバーヘッドのうちbundle installが20sec、yarn installが30sec、yarn buildが1min程度で、依存パッケージ解決と静的ファイルビルドに殆どの時間を使っていました。

ここで依存パッケージは Gemfile.lockyarn.lockが一致していれば同じものを使いまわせますし、yarn buildJavaScriptのソースなどに変更がなければ同じものを使えます。

そこでCircleCI 2.0のキャッシュ機構を参考に、Jenkinsfileでキャッシュを行う関数を用意しました。

def yarn_install() {
  cache(
    ['node_modules'],
    get_cache_key(['package.json', 'yarn.lock']),
    { bash "yarn install --no-progress --pure-lockfile" }
  )
}

def cache(paths, key, callback) {
  if (!fileExists(cachedir(key))) {
    callback()
    for(def path: paths) {
      // node_modulesのrequireの仕様上、ディレクトリ構造の保持が必要なので path/to/file ような場合に path/to までを作成したい
      sh("dirname ${cachepath(path, key)} | xargs mkdir -p")
      sh("mv ${path} ${cachepath(path, key)}")
    }
  }
  for(def path: paths) {
    sh("rm -rf ${fullpath(path)}; ln -s ${cachepath(path, key)} ${path}")
  }
}

cache関数では依存ファイルのパスを元にキーを生成しており、そのキーを持つキャッシュが存在する場合はsymlinkを貼るだけで処理を終わります。

これにより、Gemfileなどに変更がない場合はこれらの処理を合わせて10secちょっとで終わるようになりました。

なおキャッシュを利用するのはfalse positiveが怖いのですが、古いキャッシュはfalse negativeのほうが起こりやすいはずですし、定期的に削除して再生成させているため大きな問題はないだろうと思っています。

✅ 結果

これらの改善処理によって、全体で5min、オーバーヘッドは10sec程度まで短縮することができました🎉

あとは不要なspecを削ったり統合したり、あるいはfixtureを減らしたりcreateからbuildに変えたり、といった地道な改善を続けて少しずつ実行時間を削ることになります。

こういった地道な改善を進める上でも、ひとまずそれ以外の部分で実行時間を大幅に削れたのは効率が良かったと思います。

🎅 Misoca Advent Calendar 2017

最終日がだいぶギリギリになってしまいましたが、無事Misoca Advent Calendar 2017を完走することができました。

個人的には社長の記事が一番バズってるという事実に微妙な気持ちを抱いていますが、全員でいろいろな記事を書くことができて楽しかったです。

来年も気が向いたらアドベントカレンダーを立てたいなーと思います。

📣 宣伝

Misocaでは2018年のアドベントカレンダー記事を一緒に書く仲間を募集しています!!!