メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックリーディングカンパニーのエンジニアブログです。PHPからRubyへ絶賛移行中!継続的にアウトプットを出し続けられるようにみんなでがんばりまっす!

滞りなくサービスをクローズするために必要なこと

メドピアエンジニアの難波です。

医師専用コミュニティサイト「MedPeer」では、今年の8月にMedPeer Journalというサービスのクローズを行いました。今回の記事ではその時に行った作業の紹介をしたいと思います。

サービスの新規開発に関する記事というものは世の中にたくさんあれど、大規模なサービスにおける一部機能(サービス)の終了に関する経験や知見は中々オープンにされにくいものです。しかしサービスを滞りなく素早く終了させることは新しいサービスを作るリソースの確保という観点でも大事なことであり、今回の記事が将来の参考になればと思い1つの事例としてここに認めます。

MedPeer Journalについて

MedPeer Journal(以下Journal)とはPubMed(Wikipedia)という医学を中心とする生命科学の文献情報を収集したオンラインデータベースへの検索エンジンを利用して、MedPeer会員が世界中の論文に対して議論したりコメントしたりすることができるサービスです。2018年の夏にスタートしたのですが一年ほどの運用を経て検討した結果、様々な理由により2019年8月にサービスを終了することとなりました。

事業的な観点による反省や改善点などは色々とあるものの今回はそれは置いておいて、本記事ではMedPeerのような内部で様々なサービスが運営されているWebサービスにおける、一部サービスの終了とそれに伴う開発的なフローについてまとめます。

クローズの計画づくり

一言でサービスを終了するといっても、サービスを終了するために必要な実装というものが存在します。提供しているのが単一のサービスでありそれを終了するなら極論サーバを落としたりドメインの向き先をS3に置いた「サービス終了のお知らせ」という一枚のHTMLにすることも可能ですが、JournalのようにMedPeerというサービスの1コンテンツとして運用しているものはそうもいきません。

よってまずサービスの終了に必要な作業をまとめ、見積もりを行います。その見積もりを見て終了しない場合に毎月かかるコストやリスク、終了にかかる実装コスト、終了に伴うユーザへの影響などを総合的に鑑みて判断が行われます。この部分はサービスの性質によって内容が大きく異なる部分かと思いますが、Journalでは終了に必要な作業として大まかに以下のようなタスクを挙げました。

  • 事前に必要なこと
    • ユーザへの告知
    • 関係者への連絡
    • データの削除に関する方針の決定
  • 終了日に必要なこと
    • ルーティングの削除
    • 一部URLにアクセスした場合のリダイレクト処理
  • 終了後に必要なこと
    • ソースコードの削除
    • 関連サービスの停止
    • データの移行や削除
    • 移行データの管理画面作成

実装作業

事前に必要なこと

開発が必要なものとしてまずはユーザへの告知です。Journalのトップ画面にお知らせという形で表示しました。

f:id:kyoheinamba:20190930134228p:plain

またJournalでは一部の論文に関してお手伝いいただいている医師の先生に論文解説を書いていただいておりました。これらを含め他のサービスで活かせる情報も多く、具体的に必要な移行作業などを検討して方針を決めました。

終了日に必要なこと

終了日に最低限必要なことはユーザが正常にサービスにアクセスできなくなることです。そのためにまずRailsのroutes.rbから該当するルーティングを削除しました。また一部URLについてはトップ画面へリダイレクトするという処理をNginxの設定ファイルに追記しました。

ここで大変だったのは社内の別のページからJournalへリンクしているかどうかの調査です。MedPeer内の別ページからのリンク(ヘルプページなど)、更にはコーポレートサイトにJournalの紹介などのリンクがないかをチェックする必要もありました。基本的には社内の各サービス担当者にヒアリングを行い、各種リポジトリ内でgrepすることで抜け漏れを探す作業になります。

終了後に必要なこと

ユーザがサービスにアクセスできなくなってもソースコードはまだ残っており、それを消さなければRailsや各種gemfileのアップデート時の負債になり続けます。Journalにおいては上記で書いたように一部データについては移行を行う予定だったため、ViewとController、テストについてはほとんど全てを、Modelについては一部を残して削除しました。今回はRubyの世界で移行作業を行うことにしたためこういう方針にしましたが、別の方針としてデータベースの当該テーブルをダンプしてS3等に保存、その後新規に作成したテーブルにSQLでデータを流し込むといった方針もあったと思います。

またJournalでは検索機能を提供しており、そのためにElasticsearch on Elastic Cloudを使用しておりました。こちらについては他のサービスも使用しているため全面的にストップということにはなりませんでしたが、データの削除、インスタンスタイプの変更などを行いました。

また他サービスに移行して使用することになったデータを管理するための管理画面の作成などを行いました。

注意しておくべき点

実際に起きたヒヤリハットなのですが、大規模な機能改修が頻繁に行われているリポジトリでは様々なブランチで編集が行われるファイルがあります。Railsでは routes.rbschema.rb が該当するものでしょう。

そういう時に注意してレビューを行わないと、クローズ時に消したはずのコードが復活することがあります。クローズ担当者は現在アクティブに動いているPull Requestについても一通り確認しておきましょう。

まとめ

今回のJournalクローズでは終了後のトラブルといったこともあまりなく、滞りなく作業を行うことができました。

サービスをクローズすることは残念なことですが、それによって生まれた開発的な正の変化としては以下のようなものがあります。

  • bundle update 時の確認コスト減少
  • Rubyアップデート時の確認コスト減少
  • 不要になったgemの削除
  • CIの完了までにかかる時間の減少

終了しないに越したことはありませんが、終了するとなったら後顧の憂いを残さぬように予定を決めて速やかに削除しましょう。


(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

最近のheadless chromeを利用したファイルダウンロードのテスト方法について

こんにちは。メドピアのRuby(Rails)化をお手伝いしている@willnetです。最近大阪Ruby会議02に妻子を連れて参加したのですが、👶が行き帰りの新幹線に合わせて寝てくれたおかげで大変スムーズに移動できました。

さて、以前poltergeistからheadless chromeへ移行する時に気をつけることというブログエントリを書きました。

その中で、ファイルダウンロードのテストをheadless chromeで実行するための設定について書いています。しかし、この設定では最近のchrome(chromedriver)では動かなくなってしましました。このエントリでは最新のやり方について紹介します。

これまでの設定例

以前のブログエントリに掲載したコードを一部再掲します。

Capybara.register_driver :headless_chrome do |app|
  driver = Capybara::Selenium::Driver.new(
    app,
    browser: :chrome,
    desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome(
      login_prefs: { browser: 'ALL' },
      chrome_options: {
        args: %w(headless disable-gpu window-size=1900,1200 lang=ja no-sandbox disable-dev-shm-usage),
      }
    )
  )
  bridge = driver.browser.send(:bridge) # ここからがファイルダウンロード用の設定
  path = "session/#{bridge.session_id}/chromium/send_command"
  bridge.http.call(
    :post, path,
    cmd: 'Page.setDownloadBehavior',
    params: {
      behavior: 'allow',
      downloadPath: DownloadHelper::PATH.to_s,
    }
  )
  driver
end

Capybara.javascript_driver = :headless_chrome

これまで、headless chromeでのファイルダウンロード機能はデフォルトで無効だったので、有効にするために上記のようなコードを書く必要がありました。

しかし最近のchromedriver(v77以降)の仕様変更により、上記のコードは動かなくなってしまいます。

新しいchromedriverでは、上記のような設定をせずともデフォルトでファイルのダウンロードが有効になっています。このとき、デフォルトではカレントディレクトリがダウンロード先になります。上記の設定がこのデフォルトの挙動に置き換わってしまうため、DownloadHelper::PATHにファイルがダウンロードされることを期待しているすべてのダウンロード関連のテストが失敗するようになります。

(追記)この現象はv77時点ではlinux版のchromedriverでのみ起こるようです。macの場合はこれまでの書き方か、後述しているSelenium::WebDriver::Chrome::Driver#download_path=を利用した書き方でのみテストが通り、次の解決策で紹介した書き方だとテストが失敗します(ややこしいですね…)。

解決策

次のように修正すると、ダウンロード先をDownloadHelper::PATHで設定したディレクトリに変更できます。

Capybara.register_driver :headless_chrome do |app|
  browser_options = Selenium::WebDriver::Chrome::Options.new
  browser_options.args << '--headless'
  browser_options.args << '--disable-gpu'
  browser_options.args << '--no-sandbox'
  browser_options.args << '--disable-dev-shm-usage'
  browser_options.args << '--lang=ja'
  browser_options.args << '--window-size=1920,1200'
  # この行がメインの変更
  browser_options.add_preference(:download, default_directory: DownloadHelper::PATH.to_s)
  Capybara::Selenium::Driver.new(
    app, browser: :chrome, options: browser_options
  )
end

Capybara.javascript_driver = :headless_chrome

以前の設定と比べて、selenium-webdriverに対するオプションの渡し方が新しいものに変わっています。が、そこは本筋ではないので置いておいて、browser_options.add_preference(:download, default_directory: DownloadHelper::PATH.to_s)がメインの変更点です。これによりchromedriverでのダウンロードディレクトリの設定を変更することができます。

ちなみにselenium-webdriverには3.13.0以降でSelenium::WebDriver::Chrome::Driver#download_path= メソッドが生えているため、v77未満のchromeを利用している場合は、bridge = driver.browser.send(:bridge) ...のようにせずとも次のように書くことができるようになっています(内部でやっていることは一緒です)。こちらのほうが簡潔で良い感じですね。

# 略
  driver = Capybara::Selenium::Driver.new(
    app, browser: :chrome, options: browser_options
  )
  driver.browser.download_path = DownloadHelper::PATH.to_s
  driver
end
# 略

お手持ちのchromeのバージョンに合わせてご利用ください。

謝辞

このエントリで紹介した内容について、@jnchitoさんに情報提供いただきました。ありがとうございました(\( ⁰⊖⁰)/)


(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

v-onから辿るVueの細道

みなさんこんにちは、フロントエンドピラティストの小宮山です。
しばらく休養していたランニングを再開し、ハムストリングスの探求に勤しんでいるのが近況です。

v-onの不思議

templateでのv-onの書き方にはいくつかバリエーションがあります。
なんとなく書いてもVueがいい感じに解釈してくれてしまうので普段はあまり気にしていないんですが、よくよく考えてみると不思議な挙動をしているようにもみえてきます。

最もオーソドックスなのはこれではないでしょうか。

<input type="text" :value="value" @input="input" />

methodsとして定義しておいた関数をそのままイベントハンドラとしてtemplateに埋め込む形です。

methods: {
  input() { ... }
}

こう書いても動作は同じです。

<input type="text" :value="value" @input="input()" />

見慣れたtemplate構文だと思いますが、@input="~~"~~に書かれた処理はJavaScriptの構文としては全くの別物です。一方は関数の参照であり、もう一方は関数を実行した戻り値であるはずです。
なのにVueのイベントハンドラとしては両者の動作は同じです。

なんででしょう?不思議に思いますよね、思ってください。思ってくれたことにしてこのまま話を続けていきます。

v-onの書き方バリエーション

まずはv-onの書き方を種類分けしていきます。これは特に公式にそういう区分があるわけではなく、勝手に分類してみただけです。

methods埋込み型

<input type="text" :value="value" @input="input" />

オーソドックスな書き方。

methods実行型

<input type="text" :value="value" @input="input('hoge')" />

引数を指定したいときに使う書き方。

③ 関数埋め込み型

<input type="text" :value="value" @input="(val) => $emit('input', val)" />

親コンポーネントにイベントを渡して行きたいときによく使う書き方。

④ 式埋め込み型

<input type="text" :value="value" @input="value = value + 'a'" />

methodsにするのが面倒なときに使う書き方。

おそらくこの4種類が代表的な書き方ではないでしょうか。
例示のために微妙に処理内容を変えてしまいましたが、どの書き方をしてもイベントハンドラとしては期待通りの動作をしてくれるのはみなさん御存知の通りです。

v-onの書き方によるパフォーマンスの違い

4種類それぞれがパフォーマンスに与える影響が気になるところです。

実は先日社内フロントエンド勉強会の場でReact入門が実施され、hook周りの仕組み、特にuseCallbackによるイベントハンドラ最適化の努力にとても興味を惹かれました。useCallbackを使わない場合と比べてコードが冗長になるのは間違いないのに、それを受け入れてまで最適化に不断の努力を行うReactの姿勢には鬼気迫るものがあります。

Reactがここまでやっているんだから、じゃあVueはどうなのよというのは当然の疑問です。実はこの疑問から始まってv-on周りの挙動やコードを調べて回った結果がこの記事だったりします。

本題に戻り、v-onの書き方によるパフォーマンスを検証していきたいと思います。

パフォーマンスの差はありません。

結論がでました、この記事の本題は以上です。
すみません終わりません。ちゃんと根拠を提示する義務を果たします。

v-onのコードを読む

実は当初はパフォーマンスの差があるだろうと決めつけ、ブラウザの開発者ツールでメモリ利用量とにらめっこしたりしていました。
ただどう頑張っても、有意に差があるだろうと見て取れるような状況は発生していませんでした。

そしてv-onの書き方ごとのパフォーマンスグラフを貼り付けて、「こんなにパフォーマンスに差が出る!」「こういう書き方をするVue使いは素人」「これからはこの書き方一択」というマウンティングをかましていくという目論見は見事に崩れ去りました。

f:id:robokomy:20190912140827p:plain
変わり映えしない!

マウンティングは失敗でしたが、なぜ差がでないのかという新たな疑問を抱いてしまうのがエンジニアの性です。差がないと性がかかってしまったなと気にし始めるのもきっとエンジニアの性です。

パフォーマンス差がでない理由をパフォーマンス計測から見つけるのは難しいので、Vueの実装を探索していきます。

以下ではv2.6.10タグのコードベースで紹介していきます。

github.com

はい、見るべきコードはここです。

これが何かというと、@input="~~"~~に書かれた文字列をイベントハンドラとして実行できる関数に変換している部分の実装です。

まずはこの分岐に、先に上げたv-on記法の①と③が突入します。細かい説明は省きますが、handler.valueに上述の~~に書かれた文字列がそのまま入っています。
そのままreturnしているので、イベントハンドラとして~~に書かれた関数がそのまま実行されます。①と③は関数の参照なのでそのまま実行するだけというわけです。

ちなみに$emitしたときはこんな感じでイベントハンドラを実行しています。使う側には魔法に見えても、Vueの内部実装はもちろん魔法ではなく地続きの実装です。

さらに脇道にそれると、thisを付けなくてもtemplate内でmethodsを使えたりするのはここでwith(this)とされているからでした。改めて実装を探ってみると、なるほどなぁという発見がたくさんあります。

(へー便利そうと思っても軽い気持ちでアプリコードにwithを使うのは絶対にやめましょう。)

再び本道に戻ります。

残りの②と④はこちらの分岐で、②だとisFunctionInvocationtrueになり、④だとfalseです。returnされるかという違いはありますが、function($event){ .. }でラップすることで、どちらも~~に書かれた処理がそのまま実行されるのが特徴です。

function($event){ .. }というラップを利用し、イベント引数を$eventという変数として受け取るなんて小技もあったりします。実は今回始めて知りましたが、ちゃんとドキュメントのこのあたりにも書いてあります。

ドキュメントだとネイティブのDOMイベント用っぽいですが、非ネイティブなコンポーネントについても同じイベントハンドラの文字列パースがされているので共通で使うことができます。
イベントをそのまま親コンポーネントに渡したいときのショートハンドとして使えなくもないかもしれません。

<input type="text" :value="value" @input="$emit('input', $event)" />

ただし受け取れるのは第一引数のみです。複数の引数が欲しい場合は素直に(a, b) => hoge(a, b)という形にするか、argumentsを使う必要があります。

@input="hoge(...arguments)"という書き方をするとbabel変換とVueコンパイラの相性が悪いのかエラーになってしまうんですが、この書き方ができる詳しい条件ご存じの方いたら教えて下さい。

v-onの書き方によるパフォーマンス結論

Vueの実装をざっと眺めてもうお気づきだと思いますが、今回紹介した4種類のv-on記法はいずれも同じような加工とパース処理を経て実際に実行されるイベントハンドラへと変換されます。
加工とパースに若干の差異はあるものの、その後の処理に差はありません。

そして加工とパース処理は基本的にビルド時に行ってしまいますので、ランタイムにおいてパフォーマンスに差がでることもないというわけです。function($event){ .. }によるラップで関数呼び出しのネストが増えるのは確かです。とはいえさすがにその差を気にする必要に迫られる環境は皆無だと思います。

今回はv-onについての調査でしたが、render関数を作って直接onの設定をした場合は状況が別物です。この場合はrenderが評価される度にそこに書かれた処理が実行されるので、ReactがuseCallbackで最適化を目指したのと同じような状況が生まれます。

Vueのtemplateを使っている限りv-onのパフォーマンスを気にする必要はない、というよりしても何もできないが正しいですが、renderを使う場面に遭遇したらパフォーマンスについても気にしておくことをおすすめします。

まとめ

v-onを使うときに気にするべきはパフォーマンスではなく、可読性。

もう締めに入っているのに唐突に可読性という主張を始めてしまいました。
templateに複雑な処理を書かないようmethodsに切り出していくことはもちろん重要です。 重要ですが、その理由はパフォーマンス観点から来るものではないというのはこれまで見てきた通りです。

では何の観点かというと、やはり可読性ではないでしょうか。
ただフラグをトグルだけの処理や、ただ親に$emitするだけの処理までmethodsに切り出すべきか判断に迷ったときは是非ともパフォーマンスではなく可読性に重きを置いていきましょう。


(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

IT x 社会貢献 ~レアジョブ・メドピア 開発事例公開~

こんにちは。サーバーサイドサウナーの川井田(@tamamushi_2)です

先日、レアジョブさんと勉強会を開催し、同期の櫻井(@shibadog39)と登壇してきたので、資料と一言コメントをお送りしますm(_ _)m

medpeer.connpass.com

 

資料・コメント

川井田

私は、Sidekiq Enterpriseの導入事例として、メール配信JOBの改善を例にだして、Enterprise + Pro で使える機能紹介をメインに、発表しました!

機能紹介に力を入れすぎて、何を言いたいのかわからない資料になっていますが、この資料をみてSidekiq Enterprise導入したよって声が聞こえてきたら、嬉しいです。

speakerdeck.com

櫻井

こんにちは。メドピア筋トレ部、幽霊部員の櫻井です。

自分は、WebAPI開発におけるスキーマ駆動開発をテーマにLT登壇させていただきました。 また、今回のイベントのテーマがIT×社会貢献ということで、自分が担当しているサービス「kakari」についても紹介しています。 kakari.medpeer.jp

さて、本題のスキーマ駆動開発ですが、

・API定義のドキュメントをメンテしていくのしんどすぎ
・WebAPI開発の効率を上げていきたい

と思ったことが一度でもある方には、ぴったりの内容となっていますのでぜひ目を通してみてください。

speakerdeck.com

今回のLTでは盛り込めませんでしたが、スキーマ駆動開発を目指してみて「困ったこと、大変だったこと」についても機会があったらいつかお話できればと思っています。

イベントの様子

クラフトビールを用意させて頂き、イベント開始と同時にプシュ!っとして、懇親会も盛り上がりました!

f:id:degwinthegreat:20190902200528j:plainf:id:degwinthegreat:20190901120030j:plainf:id:degwinthegreat:20190901120004j:plain

おわりに

引き続きメドピアでは、開発で得た知見を公開するイベントを予定しております。

メドピアの開発に興味のある方、お酒の飲みたい方は、ぜひ参加して下さい!!


(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

松本の地でSass/SCSSの邪悪なアンパサンドを撲滅するために立ち上がった

メドピアCTO室フロントエンドエンジニアの小宮山です、よろしくおねがいします。
趣味はボルダリングとヨガとピラティスです、よろしくおねがいします。

6月某日、長野県松本市の地にて開催されたメドピア開発合宿で取り組んだことについて紹介していきたいと思います。

アンパサンドへのウラミツラミ

タイトルにも挙げたとおり、今回立ち向かったのはSass/SCSSのアンパサンド(&)です。 メドピアのリポジトリはほぼSCSSで統一されているので、この記事ではSCSSの記法ベースでコード例を載せていきます。

アンパサンド記法は、BEMライクなセレクタを書くときによく利用されるのではと思います。
例えばこのようなものです。

.header {
  &__foo {
    color: green;
  }
  &--bar {
    color: blue;
  }
}

アンパサンドを使い、冗長な記述を限りなく減らしたスマートな書き方ですね。書いてるときはDRYなライブ感と共にスタイルを書き進めることができます。

しかしこの書き方には、セレクタの検索可能性が損なわれるという非常に大きな欠点があります。

検索妨害

上述のようなスタイル指定がされたコードに手を入れる場面を想定してみます。変更を加えようとしているファイルはhtmlとjs側です。

html

<div class="header__foo">
  foo
</div>

htmlにはheader__fooというclassを持ったタグがあり、jQueryを使ってセレクタからそのタグを取得して何かの操作をしているとします。

js

$('.header__foo').hide()

何の変哲もない古き良きjQueryコードです。

続いてこの処理をVueで置き換えてみます。細かいところは省略しますが、こんな感じになるのではないでしょうか。

<template>

<div class="header__foo" v-show="show">
  foo
</div>

<script>

Vue.extend({
  data() {
    return { show: false }
  }
})

さくっと置き換えることができ、JavaScript側はもうタグに付けておいたclassを必要としなくなりました。
不要なものは消せるときに消してしまうのが正義です。早速header__fooというclassをタグから消してすっきりさせてしまいましょう。

<div v-show="show">
  foo
</div>

と迂闊に消してしまう人はさすがにいないと信じたいです。JavaScriptから参照していなくても、CSSからclassを参照している可能性は大いに残っています。

このclassに対するCSSを書いた本人なら、このclassがまだ必要であることを覚えているかもしれません。しかし人の記憶は頼りになりませんし、このclassを消していいか迷っているのは何も知らない別人かもしれません。消したいclassがまだどこかで使われているかの判断に迷ったときは、grepするに限ります

どんな結末が待っているかはもう想像がついたかと思います。アンパサンドによる文字列結合で指定されているこのセレクタは、タグに付いているclass名でgrepしても拾われません。

.header {
  &__foo {
    color: green;
  }
}

そして使われていないことを確信し、コード改善と思ってclassを消した開発者には、スタイル崩れという悲劇が待ち受けています。

このようにアンパサンドによってセレクタが文字列結合されていると、タグのidやclassがどこで使われているのか見つける手段が大幅に制限されてしまいます。 地道に探せば見つけられるかもしれませんが、その労力と、不要なセレクタを減らす地道なコード改善が釣り合うのかは微妙なところです。
そして結局は触らぬ神に祟りなし、必要なのかも分からないidやclassは消えることなく未来に向かって増え続けていきます。

purgecss

検索妨害と仕組みは同じですが、アンパサンドによるセレクタ結合はpurgecssによる未使用スタイル削除の妨げとなる可能性があります。

purgecssは非常に強力なものの、セレクタを使っているかの判定は静的解析によって為されています。どこまで正確に判定しているのかまでは精査できていませんが、不要なリスクを回避する意味でも、アンパサンドやスクリプトを用いた文字列結合によるセレクタ生成は避けたほうが無難です。

合宿の地、松本にて

grepを阻害する邪悪なアンパサンドによるセレクタ文字列結合は、すでにメドピアのリポジトリ群に多く入り込んでいました。この現状に立ち向かうべく、アンパサンドを使わない素のセレクタ文字列に変換してしまおうと決意を固めます。

そしてメドピアでは年に数回、2泊3日かけて自由なテーマに取り組むことができる開発合宿を開催しています。 ボリューム的にちょうど良さそうだったこともあり、アンパサンド変換用のCLIツール開発をテーマに決めて取り組むことにしました。

採用言語

CLIツールの開発言語にはRustを選びました。フロントエンド関連なのだからJavaScriptやTypeScriptでいいじゃないかという葛藤はもちろんあったものの、舞台はせっかくの開発合宿です。今の最善でないとしても、興味のある技術にフォーカスしてこそ開発合宿です。普段とは違う言語に四苦八苦したくなるときだってあるんです。

成果物

リポジトリはこちら。 github.com

CLI

まずは当初目的通りCLIツールです。clapというCLIツール作成用便利パッケージを使うと引数解析やヘルプ表示などを簡単に作れます。

USAGE:
    sassruist [FLAGS] <path>

FLAGS:
    -f, --fix        fix original file(s)
    -h, --help       Prints help information
    -V, --version    Prints version information

ARGS:
    <path>    target file or directory path

ファイルを書き換えたりといったCLI特有の機能以外はWebAssembly版と同じ動作なので、デモなどはそちらに引き継ぎます。

WebAssembly

フロントエンド、Rustというキーワードが来たら当然続くのはWebAssemblyでしょう。 冒頭で長々と書いた当初の目的を果たすにはCLIツールさえあれば十分ですが、舞台は開発合宿です。隙あらば気になる技術に全力投球していきます。

成果物

netlifyにデモサイトを用意したのでお試しください。 determined-wescoff-282115.netlify.com

妥協点

成果物発表直後ですが早速妥協点紹介です。というのも正直なところ今回の成果物・・・作った本人から見ても実用レベルには至りませんでした、無念。
以下その理由です。

変換結果

「アンパサンドを置換するだけだし実装なんて簡単だろう」と気楽に作り始めてしまったのが運の尽き、Sassアンパサンド(というよりSassそのもの)は想像以上に手強い相手でした。

まずはこの変換例を御覧ください。

f:id:robokomy:20190716135423p:plain
変換例

みにくい!!!

そうなんです。アンパサンドを解決するにはセレクタのネスト構造を解決する必要があり、そのために記述が複雑になることを避けられませんでした。

ちなみに余談ですが、当初は勢いで走りすぎてこういう変換をしていました。間違い探しとしてお収めください。

f:id:robokomy:20190716135917p:plain
間違い探し

制約

100歩譲ってみにくさは我慢するとしても(diffが死ぬので譲りにくいですが)、仕様的に無視しにくい大きな制約もいくつか残ってしまいました。

特に厳しいのは複数行のセレクタに対応できていないことです。

f:id:robokomy:20190716140516p:plain
🤔🤔🤔

見るも無残な姿になってしまいました。「1行ごとに処理すればいいだろう」という謎の自信に満ちた実装方針により、複数行セレクタへの対応が必要と気付いたときには既に軌道修正が間に合いませんでした。

あと地味にSassを諦めてSCSS専用になっています。妥協点満載です。

CLIとWebAssembly両対応ビルド

気を取り直して、今回一番苦戦した点の紹介です。実は実装を差し置いて、CLIとWebAssemblyの両方にRustコードをビルドすることにかなり苦戦しました。
RustやCargo.toml自体への理解度もあまり深くなく、試行錯誤の末になんとか形になった方法をここで紹介したいと思います。

シンプルなWebAssemblyビルド

RustをWebAssembly化し、ウェブページとして公開する1連の流れ自体はこちらの記事を大いに参考とさせていただきました。

webbibouroku.com

bindgen、wasm-pack、create-wasm-appなど、便利なツールの導くままにWebAssemblyなウェブページを作ることができてしまいます。

RustをWebAssemblyにビルドするだけなら解説通りにwasm-packを使うだけです。しかし今回はもう1つのターゲット(こちらが本命のはずなんですが)としてbinaryにもビルドする必要があります。

異なる依存パッケージ

CLIツールとしてはディレクトリ内のファイルを一気に処理させたかったので、dependenciesにwalkdirというファイル探索用パッケージを追加していました。
ウェブページて使うWebAssemblyにファイル操作は不要なのですが、Cargo.tomlのdependenciesに書かれたパッケージがWebAssemblyビルドのときにも認識され、wasm-pack buildコマンドを実行すると以下のようなエラーを吐かれてしまいます。

error[E0433]: failed to resolve: use of undeclared type or module `imp`
   --> /~~~/.cargo/registry/src/github.com-1ecc6299db9ec823/same-file-1.0.4/src/lib.rs:261:9
    |
261 |         imp::Handle::stdout().map(Handle)
    |         ^^^ use of undeclared type or module `imp`

原因は推測ですが、WebAssemblyにビルドできない何か(walkdirなのでおそらくファイル操作系)が混ざっていると判定されているのだと思います。
その証拠に、Cargo.tomlのdependenciesからwalkdirをコメントアウトすると無事にビルドが成功します。

つまり、WebAssemblyにビルドするときだけ手動でその行をコメントアウトすれば解決します!!!

許されません、これは許されません。ビルドコマンドをただ実行するだけなのに、こんなにも露骨な運用でカバープロセスを挟み込むなんて許されるわけがありません。README.mdにこんなビルド方法の解説を書くこともとてもできません。

環境ごとの依存パッケージ変更

本体のコーディング以上に血眼になって探し、ついにここで答えを見つけました。
修正後のCargo.tomlの書き方はこちらです。

gist.github.com

ポイントはclapwalkdirパッケージに付けたoptional = trueというオプションで、featuresとしてbinを指定したとき(cargo build --feautres bin)だけビルド時の依存関係に含めてくれるようになります。

f:id:robokomy:20190705185245p:plain

大まかな流れは上図のようになっています。WebAssemblyとしてビルドするときは不要なパッケージをwasm-pack buildから除外することに成功しました。そしてCLIとしてmain.rscargo buildするときは、--feautres binを引数に加えることで依存パッケージをしっかりと認識してくれます。
引数追加程度ならREADME.mdに書くこともきっと許されることでしょう。

本当はCLIとしてビルドするときにWebAssemblyでしか使わないパッケージも除外したかったのですが、こちらはwasm-packをさらに解析する必要がありそうで今回は泣く泣く断念しました。こちらの問題にも対応した完全版両対応ビルド設定の模索は今後の課題です。

f:id:robokomy:20190705185931p:plain

成果物やデモを公開するには、やはりウェブブラウザで見れるというお手軽さは魅力的です。本命のCLIツールを作り込みつつ、さくっと触れるデモをウェブで公開したいという贅沢な欲求をWebAssemblyは見事に叶えてくれました。

まとめ

Sass/SCSSの邪悪なアンパサンドへのウラミツラミからスタートし、それっぽく動くけど妥協点満載な解決ツールを作り、WebAssemblyの知見を紹介して締めるという全くまとまりのない記事となってしまいましたが、メドピア開発合宿3日間が見事に集約されています。自分で言うのもあれですが見事に集約されています。

実は人生初の開発合宿というイベントでした。好きなテーマに取り組んでいいという我らがCTO福村の言葉をそのまま受け取り、このように本当に好きなことに取り組むことができた濃密な3日間となりました。これで当初目的だったアンパサンド撲滅も達成できていたら最高だったのですが、残念ながら力及ばず。

しかしなんと、同じく合宿に参加していたフロントエンドエンジニア村上(@pipopotamasu)がstylelint-scssのruleでセレクタのアンパサンド結合を禁止するというスマートすぎるPRをさくっと作って本家のルールに加えてくれました。

github.com

邪悪なアンパサンド勢力の拡大はこうして食い止められたのです。松本の地にて立ち上がっていたのは村上です。僕は何もしていません。やはり持つべきものは仲間です。メドピアではフロントエンドエンジニアの仲間を絶賛募集中です。


(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

開発合宿に行ってきました!@松本

メドピアのサーバーサイドサウナーの川井田です。

メドピアでは、年2~3回のペースで日常業務から離れて、業務改善や、技術研鑽のための開発合宿を開催しており、恒例となっています。

前回の様子はこちら。 tech.medpeer.co.jp

6月26日から28日まで3日間、エンジニア13名で行ってきました!

初めて参加してきたのでレポートしたいと思います。

今回の合宿地は2020年rubykaigiの開催地松本です!

f:id:degwinthegreat:20190703203832j:plain

※我々は遊びに来たのではなく、業務改善や技術研鑽のために合宿に来ております。


開発の様子

ホテルの部屋で集まって開発したり、

f:id:degwinthegreat:20190702082223j:plainf:id:degwinthegreat:20190702082242j:plain

会議室を借りてモブプロしたり、

f:id:degwinthegreat:20190702084618j:plainf:id:degwinthegreat:20190702085548j:plain

早いWi-Fiを求めてカフェに移動したり、

f:id:degwinthegreat:20190702084812j:plain

業務より疲れるという声が聞こえてくるくらいモクモク作業していました。

f:id:degwinthegreat:20190703235502j:plain

※我々は遊びに来たのではなく、業務改善や技術研鑽のために合宿に来ております。


グルメメモリー

ずっとモクモクしていたら心が邪悪になります。

しっかり食事を取ることでピュアな心を取り戻します。

f:id:degwinthegreat:20190702072734j:plainf:id:degwinthegreat:20190703235822j:plainf:id:degwinthegreat:20190704000115j:plain
f:id:degwinthegreat:20190703204341j:plainf:id:degwinthegreat:20190703204944j:plainf:id:degwinthegreat:20190703204351j:plain
f:id:degwinthegreat:20190702072423j:plainf:id:degwinthegreat:20190704000238j:plainf:id:degwinthegreat:20190702072450j:plain

f:id:degwinthegreat:20190704150654j:plain

※我々は遊びに来たのではなく、業務改善や技術研鑽のために合宿に来ております。


20人は収容出来る超巨大ストーブサウナのある銭湯に一人で行くもの f:id:degwinthegreat:20190702082802p:plain

【公式】湯の華銭湯 瑞祥松本|長野県松本インターに近い日帰り温泉

ホテルの宿泊者限定の送迎付き温泉旅館に行くもの

f:id:degwinthegreat:20190704001249j:plainf:id:degwinthegreat:20190704001327j:plain

f:id:degwinthegreat:20190704001337j:plain

www.hotel-shoho.jp

我々...


成果発表

時間も忘れてモクモク作業してきた成果を社内で発表します。 ここに全てをかけています! 外部に公開できそうな成果をまとめます。


開発環境全部構築するまで帰れま10

CTO福村自らみんなの開発環境をカイゼンしていきたい! との思いで取り組んだそうです。

10個のプロジェクトの開発環境を構築し、

立ち上げたコンテナの数はちょうど100!

hostsファイルには48個の*.testが追加されたそうです。

カイゼンPRお待ちしてます。

社員紹介アプリ

急激に社員が増え、顔と名前が一致しないのをカイゼンするために、3人チームで社員紹介アプリを開発していました。

コードは外部に公開されているので、ぜひPRを送ってみて下さい!

github.com

Firebaseを活用した猫監視システム

Firebaseへの好奇心の裏に隠れる愛猫を思う気持ちがとても印象的でした。

reireias-slides.firebaseapp.com

qiita.com

SASS/SCSSのclass名を結合する'&'を撲滅したい

後日詳しいブログを公開予定ですが、SASS/SCSSの'&'を撲滅するためにフロントエンドエンジニア2名が それぞれ違うアプローチで取り組んでいました。

一方はRustでフォーマッターを作ってWebSocketを使ったサーバーレスなリアルタイム変換アプリを公開し、

github.com https://determined-wescoff-282115.netlify.com/

一方は、stylelint-scssにルール追加のPRを作っていました!

※こちらのPRはマージされ、先日リリースされました! https://github.com/kristerkari/stylelint-scss/pull/338

|Added: selector-no-union-class-name rule. github.com


まとめ

私は、普段触らない技術に挑戦し続け、脳内メモリを加速度的に使い切り、 不完全燃焼に終わってしまい、3日間で成果を上げるためには、事前準備が大切であることを学びました。

我々メドピアでは、合宿を始め、輪読会、振り返り会などの、技術研鑽のための取り組みを 日々、行っています。

これらの取り組みを通して、さらに成長して、来年のRubykaigiでまたこの地松本に戻ってきたいと思います!

f:id:degwinthegreat:20190703200647j:plain f:id:degwinthegreat:20190701211105j:plain

(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

Rails × ECS 運用してみたわかった起動タイプ EC2, Fargate の使い所

メドピアマッスル部上腕二頭筋担当、CTO室 kenzo0107 です。
今回はメドピアの直近のプロジェクトで採用している Rails × ECS Fargate についてです。

直近プロジェクト

直近プロジェクトでは AWS ECS を採用しています。

2018年10月にリリースした スギサポ deli は、メドピアで Fargate 初採用となったプロジェクトです。

スギサポ deli とは?

sugisapo.ws

病気で食事制限が必要な方やシニアの方々、より健康な食生活を目指す方など、誰もが美味しく召し上がれるお食事をお届けするサービスです。

「食事制限」 と聞くと、簡素な食事をイメージされる方もいらっしゃると思いますが
一度見て頂くとお分かりの通り、かなりバラエティに富んだ内容となっており、目にも美味しい品々が並んでおります。

是非一度お試しいただければ幸いです♪

今回お話ししたいこと

以前、 Rails x ECS でオートスケーリング&検証環境の自動構築を執筆しました。

tech.medpeer.co.jp

今回は ECS を運用してきてわかった起動タイプ EC2, Fargate の使い所、また、運用時に有用だったことについて話したいと思います。

ECS についておさらい

まずは ECS の起動タイプ Fargate と EC2 について、軽くおさらいです。

起動タイプ Fargate

インフラレイヤーが抽象化されており、EC2 の管理が不要です。

以下の運用コストがなくなることが何よりも有難いです。

  • EC2 インスタンスの定期メンテ
  • ECS エージェントのバージョン管理
  • EC2 オートスケーリング管理
起動タイプ EC2

EC2 を起動し、その上でコンテナを起動しています。

これらの特性により起動タイプ Fargate, EC2 の使い所を検討しました。

検討事項

  • コンテナへのアクセスはどうやってするの?
  • AutoScaling 速いのどっち?
  • お値段はどう?

コンテナへのアクセスはどうやってするの?

f:id:kenzo0107:20190210235843p:plain

起動タイプ EC2

EC2 へ ssh さえできれば、docker ps でコンテナの起動状態を確認したり、 docker exec でコンテナにアクセスし、 rails console を実行することも可能です。

起動タイプ Fargate

インフラレイヤーが抽象化されている為、サーバへ ssh ログインできません。*1

docker exec でコンテナにアクセスする様なことはできません。

AutoScaling 速いのどっち?

起動タイプ EC2

ECS のバックエンドとして AutoScaling Group で EC2 を起動させ、ECS に紐づけた ALB に EC2 を追加する運用をしています。

その為、 EC2 をスケールアウトさせた後に、タスクをスケールアウトする様にしないと、タスクに偏りが生じる等、正しくタスク配置されない時がありました。

起動タイプ Fargate

タスクのみ考慮すればよいです。

f:id:kenzo0107:20190621221227p:plain

特に、EC2 のスケール分を考慮する必要がないとしても、Fargate のスケールアウトが安定的で速いです。

お値段はどう?

f:id:kenzo0107:20190211002727p:plain
Price

Fargate の価格はタスク数, CPU, Memory に正比例します。Fargate pricing

2019年1月に Fargate の価格が下がったとは言え、タスク数が増えることを考えると、まだ Fargate の方が割高?と思います。

本番・ステージング環境での Fargate, EC2 の使い分け

これまでの起動タイプ Fargate, EC2 の性質を加味して、プロジェクトの性質にも依りますが、以下の様な構成を採用しているケースが多いです。

f:id:kenzo0107:20190619234356p:plain

※ 以下の前提です。

  • 一般ユーザがアクセスする方を App、弊社からのみアクセスする管理画面を Admin
  • App, Admin 共に同じ Rails プロジェクトがデプロイされている

Point

  1. 本番環境 App のみ ECS 起動タイプ Fargate

    • ややコストは上がるものの、スケーラビリティに柔軟性がある
  2. その他は ECS 起動タイプ EC2

    • docker exec でコンテナに入りデバッグ可能にする
    • 前回の記事 の様に qa/* ブランチ毎のタスクが複数起動している為、タスク数が増えてもコストに影響しない
    • 本番環境 Admin は、高トラフィックとなる様なことは弊社ではない為、スケーリングを考慮する必要性がない。
    • 本番でも docker exec しコンテナに入りデバッグしたいという要望があり、Admin を 起動タイプ EC2 にすることで担保

開発時に有用だったこと

デプロイ

以下ブランチにマージすることで自動的に試験→デプロイする様にしています。

  • master
  • develop
  • qa/*

f:id:kenzo0107:20190619232701p:plain

CircleCI で試験をパスすると、 aws codepipeline start-pipeline-execution を実行し 指定の CodePipeline を開始する様にしています。

CodePipeline

こちらが実質 ECS へのデプロイをしている箇所です。

f:id:kenzo0107:20190528171331p:plain

デプロイ関連の処理は Capistrano でラップしています。

検証環境自動構築

f:id:kenzo0107:20190619235429p:plain

前回記事 検証環境の自動構築 をご参照ください。

ブランチ qa/* push により以下処理が実行され、検証環境が構築されます。

Rails master.key は?

AWS パラメータストアに登録しており、Rails イメージビルド時に aws ssm get-parameters で取得しています。

その他、イメージタグ付け( :tag_image )、ECR へ登録処理( :push_image_to_ecr )も併記しておきます。

namespace :rails do
  task :build_image do
    run_locally do
      within fetch(:deploy_work_path) do
        execute 'aws', 'ssm', '--profile', fetch(:profile).to_s,
                'get-parameters',
                '--with-decryption',
                '--region', 'ap-northeast-1',
                '--name', "/#{fetch(:application)}/rails/master_key",
                '--query', '"Parameters[0].Value"',
                '--output', 'text', '>', 'config/master.key'
        execute 'docker', 'build', '--no-cache=true',
                '-t', "#{fetch(:ecr_host)}/#{fetch(:env)}-#{fetch(:application)}-rails:#{fetch(:rails_tag)}",
                '--build-arg', "RAILS_ENV=#{fetch(:rails_env)}",
                '-f', 'docker/deploy/rails/Dockerfile', '.'
      end
    end
  end

  task :tag_image do
    run_locally do
      within fetch(:deploy_work_path) do
        execute 'docker', 'tag',
                "#{fetch(:ecr_host)}/#{fetch(:env)}-#{fetch(:application)}-rails:#{fetch(:rails_tag)}",
                "#{fetch(:ecr_host)}/#{fetch(:env)}-#{fetch(:application)}-rails:latest"
      end
    end
  end

  task :push_image_to_ecr do
    run_locally do
      within fetch(:deploy_work_path) do
        push_image_to_ecr("#{fetch(:ecr_host)}/#{fetch(:env)}-#{fetch(:application)}-rails:#{fetch(:rails_tag)}")
        push_image_to_ecr("#{fetch(:ecr_host)}/#{fetch(:env)}-#{fetch(:application)}-rails:latest")
      end
    end
  end
end

def push_image_to_ecr(image)
  execute 'ecs-cli', 'push', "#{image}",
          '--aws-profile', fetch(:profile).to_s,
          '--region', fetch(:region).to_s
end
イメージビルド処理短縮

以前は Rails イメージビルド時に asset_sync を利用し、 assets を S3 に同期していましたが、この同期処理に非常に時間が掛かっていました。

ですが、弊社フロントエンドエンジニア 村上 ( @pipopotamasu )medpacker により、 Sprockets によるアセットのビルド処理をしないようにした為、デプロイ時間が大幅に短縮されました。*2

是非以下ご一読ください。 tech.medpeer.co.jp

Rails メトリクスを Datadog へ送信

デプロイ後に Rails の以下メトリクスを Datadog に送信する様にしました。*3

  • Rails Load Time
  • Rails CodeStats
  • Gem Dependency Count

post rails metrics to datadog · GitHub

f:id:kenzo0107:20190625144500p:plain

こちらは以前 2018年9月12日 に開催された 『MedBeer -Rails開発での技術的負債との付き合い方-』にて、クックパッド社の 小室 直さん (@hogelog) の発表を参考にさせていただきました。

ありがとうございます!

tech.medpeer.co.jp

技術的負債となる指標をプロジェクト初期から意識することで、返済への意識も育まれると思います。(願い)

ロギング

f:id:kenzo0107:20190619235524p:plain

Lambda で LogGroup を S3 に日時バックアップする処理はこちらの Serverless Framework プロジェクトで構築しています。

github.com

ログ閲覧

CloudWatch Logs Insight で非常にログの閲覧がスムーズになりました。

以下の様なクエリで、logStream を rails をプリフィックスとしフィルターをかけると、Rails コンテナのログを抽出できます。

fields @timestamp, @message
| sort @timestamp desc
| limit 20
| filter @logStream like /^rails/

f:id:kenzo0107:20190530222814p:plain

時系列で複数コンテナログを閲覧したい場合は、以下の様にすれば簡単に取得できます。

fields @timestamp, @message, @logStream
| sort @timestamp desc
| limit 20
| filter @logStream like /^rails|^nginx/

以上からインサイトで検索しやすい様、ECS の Service 単位でコンテナのロググループは統一しています。

まとめ

  • 負荷の多い箇所は Fargate がオススメ
    • その他は EC2 がコスト的に良い
  • デプロイ自動化
    • テストは CircleCI、デプロイは CodePipeline と役割分け大事
  • Rails config/master.key は AWS パラメータストアで管理
  • medpacker で脱 webpacker & デプロイ時間短縮
  • Rails メトリクスを Datadog に Post で定点観測
  • ログは CloudWatch Logs に一時保存
    • 日次で S3 保存し長期保存
    • CloudWatch Logs Insight でログ閲覧がスムーズ
    • ECS Service 毎にロググループを統一しとくとコンテナ毎の時系列ログが確認しやすい

上記に加えて、開発時に最も有用な、ECS を利用した検証環境自動構築については、またの執筆の機会にと思います。

以上、参考になれば幸いです。


(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

*1:ssh コンテナを起動させ、アクセスさせることは可能です。

*2:大凡15→8分程度に短縮

*3:Gem の最新度については、現在、定期的な bundle update 当番により解消している為、送信していないです。