Quantcast
Channel: メドピア開発者ブログ
Viewing all 210 articles
Browse latest View live

Rails初学者からみたRailsdm2019の景色

$
0
0

こんにちは、メドピアフットサル部の櫻井です。
Rails歴3ヶ月の自分が贅沢にも「Rails Developers Meetup 2019」に参加してきましたので、そのレポートをお届けしたいと思います。 ちなみに、メドピアでは今回のようなスポンサードイベントへの参加費は会社が全額サポートしてくれちゃいます。

railsdm.github.io

第一線で活躍するRails開発者の技術的知見を広げることに主眼を置く本イベントは、DHH氏の基調講演に代表されるように本当に魅力的なセッションだらけでした。
ただ、それぞれの具体的な内容はRails Developers MeetupのYouTubeチャンネル*1や各登壇者さんのスライドから追うことができるため、本記事ではイベントの様子とそこで駆け出しエンジニアである自分が感じたことをお伝え致します。

f:id:shibadog39:20190326130753p:plainf:id:shibadog39:20190326095106j:plain

ドリンクスポンサーやってました!

メドピアはドリンクスポンサーとして、1日目は特製のPeerWaterを、2日目はメドベアの可愛すぎるカップでコーヒーを提供させていただきました。 特にメドベアコーヒーは好評をいただき、スポンサー枠の350杯が昼過ぎにはなくなってしまうほど大盛況でした。ありがとうございました!

f:id:shibadog39:20190326130923p:plainf:id:shibadog39:20190326131030p:plainf:id:shibadog39:20190326095652j:plain

f:id:shibadog39:20190326095640j:plain
大人気だったメドベアコーヒー

スポンサー企業によるノベルティグッズはどれも個性的でついつい手にとってしまいました。ノベルティはこういったイベントの醍醐味の一つですよね。 また、2日間とも最後には懇親会が開催されました。お寿司からスイーツまで、さらには日本酒からワインまで美味しいものを口にしながらワイワイできる最高の時間でした。

f:id:shibadog39:20190326131510j:plainf:id:shibadog39:20190326100659j:plain

f:id:shibadog39:20190326125419j:plain
大盛り上がりの懇親会

メドピアからの登壇

今回メドピアからは@pipopotamasuさんと@purunkaoruさんがそれぞれセッションとPRセッションで登壇させていただきました!

f:id:shibadog39:20190326100901j:plainf:id:shibadog39:20190326100912j:plain

お二方の登壇の様子

Railsフロントエンドボイラープレート「medpacker」の紹介 by @pipopotamasu

本ブログの記事でも投稿させていただいているmedpackerを紹介させていただきました。
どのような背景からmedpackerを作成する必要が生じたのか、medpackerを使えば何ができるようになるのかわかりやすく説明されています。webpackerからの脱却を考えている場合は、ぜひスライド及び記事の方を参考にmedpackerを使ってみてください!本当に3分かからずにセットアップできます。

MedPeerの取り組みの「失敗」の話をしよう by @purunkaoru

メドピア社内における様々な取り組みとそれにともなう失敗事例を紹介しました。
具体的には

  • 技術的負債をなくしていく整地部
  • Railsのバージョンアップ
  • 懸垂棒

に関するお話を紹介しています。我々と同じ失敗をしないためにも参考にしてください! また、自慢の取り組み・失敗話がありましたら共有していただけると嬉しいです。

初学者から見たRailsdm

冒頭にも書きましたが自分はRails歴3ヶ月で、もちろん今回のRailsdmが初参加でした。
正直に言うと今の自分が参加してなにか得るものがあるのか戦々恐々としていましたが、今となっては参加してよかったと心から思っています。

具体的には以下の3つがその理由です。

手軽に挫折することができる

セッションで前提とされている事柄・技術自体を知らないということが頻発します。比較することもおこがましい方々が登壇者ということを理解しながらも、Railsdmの2日間の間「このままじゃいけない」と何度考えたかわかりません。

手軽に視座を引き上げることができる、世界を広げることができる

第一線のエンジニア・事業責任者の方の問題解決の軌跡や課題意識を直接聞く機会はなかなかないと思います。また、日々の業務では扱う機会がなかったRailsをとりまく周辺技術であったりコミュニティに関する話題に触れることは、自分の興味関心の領域を広げることになると思います。

手軽に自分が何に「おもしろい」と感じるのか観測できる

セッションの内容は本当に多様で、技術にとことん向き合ったものからプロダクトの設計の話、Rubyコミュニティの話までありました。どういう話を聞きたいと思ったか、聞いてどう感じたか改めて振り返ることで自分の志向を再確認できるのではないかと感じました。

まとめると

魔法のようなRailsの技術自体も人の手によって作られているという当たり前のことを認識できたことが最も大きな収穫であったように思います。挫折感もあったのは確かですが、イベント後にはコードを書きたいという前向きな気持ちになっていました。

駆け出しエンジニアこそ、こういったイベントに積極的に参加するべきなのかもしれません。自分はこのRailsdmをきっかけに、各種勉強会に物怖じせずに参加してみようと思うことができました。 嬉しいことにメドピアでは今回のようなイベント・勉強会への参加を推奨する制度があるのでバリバリ活用していきたいと思います。

最後に

Railsdm参加することができてとても良かったです。運営のみなさま本当にありがとうございました!

突然ですが、人間は忘れる生き物です。 ja.wikipedia.org今回自分が感じた「勉強しなきゃ」とか「あの技術調べてみよう」という気持ちもあっという間にどこかにいってしまうことでしょう。そんなの嫌だということで、メドピアではランチの時間で集まってRailsDM再放送上映会を実施することにしました。
こういった取り組みが自発的に起こるのが弊社の魅力なのかもしれません!楽しみです。

f:id:shibadog39:20190326105654j:plain
Railsdm2019最高でした!

以上、Rails Developers Meetup 2019のレポートをお届けいたしました!

※画像左下にクレジットが入っているものはラブグラフさん*2の撮影によるもので、それ以外は個人で撮影したものになります。


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


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

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

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

■開発環境はこちら

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


メドベアと一緒にお待ちしています at RubyKaigi 2019

$
0
0

こんにちは! エンジニアの宮原です。
明日から、RubyKaigi 2019が開催されますね。

rubykaigi.org

メドピアとRuby

メドピアは、現役医師が経営するヘルステックカンパニーとして、国内医師の3人に1人が参加する国内有数のドクタープラットフォーム「MedPeer」や、遠隔医療サービスなどを運営・開発しています。2016年頃から、Ruby on Railsへ移行を行っており、新規プロダクトには積極的にRubyを採用しています。
Rubyのコミュニティを盛り上げるべく、今年はプラチナスポンサーとして協賛しています。

ブースへ遊びに来てください

技術研鑽のためのイベントや勉強会への参加は会社が積極的にサポートしており、RubyKaigiは参加希望の全エンジニアに参加費(交通/宿泊費含む)を支給しています。
今年は、ベテラン・若手含め総勢16名、Rubyエンジニアの80%が参加します。   (留守を守ってくれるエンジニアのみんなに大変感謝です...!) f:id:nyagato_00_miya:20190417073021j:plain

メドピアブースでは、開発中プロダクトのGemfileやRails Statsを公開します。

f:id:nyagato_00_miya:20190415175442p:plain
会場ではモザイク無しの実物を公開します

当日は、ヘルスケアカンパニーとして皆さんの健康をサポートできるよう、400名限定でアルコールパッチテストを実施します。 アルコール体質を「ぜんぜん飲めない族」、「ほんとは飲めない族」、「危ない族」の3種類に判定し飲酒のリスクを知るパッチテスト。 ご参加頂いた方全員へメドベアデザインのチロルチョコをプレゼントします。 さらに、限定20名(抽選)でメドベアTシャツも当たりますよ!

f:id:nyagato_00_miya:20190416203504j:plainf:id:nyagato_00_miya:20190416204112p:plain

f:id:nyagato_00_miya:20190415183424p:plain
3Fメインホールホワイエ メドピアブースでお待ちしております♪

「これは!」と感じたセッションについて、ベテランエンジニアと若手エンジニアが対談形式に考察する内容など公開予定です。

それでは、当日会場で皆さんにお会いできることを楽しみにしています。
是非、一緒にRubyKaigiを盛り上げましょう!
ブースでメドベアと一緒にお待ちしています。


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


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

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

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

■開発環境はこちら

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

RubyKaigi2019にプラチナスポンサーとして参加しました!👉🍻

$
0
0

f:id:chmv:20190424190137j:plain

サーバーサイドエンジニアのhirapi(@chmv71)です。
普段は、スギ薬局さんと共同で提供している、食事制限が必要な方に向けた栄養管理食宅配サービス「スギサポdeli」の開発を担当しています🍱
4/18〜4/20の3日間、福岡は博多で開催されたRubyKaigi2019に参加してきましたので、今回はそのレポートをお送りします🎶

アルコールパッチテストスポンサー🍻

もといプラチナスポンサー💎ということで、3階メインホール前にブースを展開しておりました。

f:id:chmv:20190424214318p:plain
こんなブースでした!

メドピアグループが医療業界やセルフケア領域でサービスを提供していることにちなんで、お越しくださった方々にアルコールパッチテスト*1を試していただきました。

健康管理はまず自分の体質を知るところから。推測するな、計測せよ👀

f:id:chmv:20190424191625p:plain
赤くなるのは弱い体質です

特殊なジェルのついたシールを貼ったまま20分待つと皮膚の色が変わります。
写真のように赤くなると耐性低いので注意、変わらない方は平気な気持ちでつい飲み過ぎてしまうので注意です。

こちらが予想外の大好評を頂き、ご用意していた400名分がなんと完売!🎊
Twitterでも話題にしていだいて嬉しい限りです。
(投稿してくださった方から抽選で20名様にTシャツをプレゼントいたします。お楽しみに😉)
これを機に医療の世界と人々の健康を支援するメドピアという会社の存在を知っていただけたらいいなと思います。
パッチテストをお試しくださった皆様、ご自分の体質に合わせたペースでお酒を楽しんでください!🍻

他にも、我が社の誇るかわいいくまのお医者さん「メドベアちゃん」のチロルチョコをお配りしていました。
いくつかバージョンがある中で、一番人気はしれっと官房長官ごっこをしているこちらでした😍

f:id:chmv:20190424183130j:plain
初春の令月にして 気淑く風和ぎ……

私含めスポンサーとして初めてブースに立つエンジニアメンバーも多くおりましたが、足を運んでくださった皆様とお話しできてとても刺激になりました。
Rubyコミュニティの一員として、Rubyでものづくりをするエンジニアとして、これからも皆様と関わっていけたらなと思います。
お越しくださった皆様、改めてありがとうございました!✨

会議 in RubyKaigi

タイムテーブルを改めて見てみると、3日間で計66ものセッションが行われていたことがわかります。
貴重な知見を惜しげもなく発表してくださった登壇者の皆様、トラブルひとつ無く運営してくださったスタッフの皆様、本当にありがとうございました。おつかれさまでした。

個人的に印象的なのは、やはり最終日の開発者会議です。バージョンアップの度にわくわくさせてくれるRubyの新仕様はこのような議論を経て作られているんだ、と胸が熱くなりました。
大盛り上がりだった👉絵文字演算子👉*2も、壇上・会場・タイムラインがエンジニアらしい遊び心で一体になっていてとても素敵でした。Ruby 3.0の目玉になりそうですね!(笑)

メドピアチームとして

さて、前回の記事にもあった通り、今回メドピアからは16名のエンジニアがRubyKaigiに参加しました。
サービス全体の開発を引っ張るテックリードもいれば、つい最近Rubyを書き始めたジュニア層もいます。興味のある分野も様々です🌈
せっかくこの多様なメンバーで参加したので、今回のRubyKaigiで得た学びを皆で深めつつ、チームの力にしていきたいと考えています。

もちろん、その学びの過程や成果は当ブログで広く発信していく所存です💪
メドピアチームとしてRubyコミュニティ・Rubyist仲間の皆様に貢献できるよう準備を進めておりますので、お楽しみに!

また、Rubyコミュニティをさらに盛り上げていくため、イベントもどんどん開催していく予定です🍺
Rubyistの皆様におかれましてはRubyKaigi2020 in 松本に向けてアップを始めているところかと思いますが、こちらも併せてチェックしていただけると嬉しいです!

5/23(木)イベントやります!

そして早速、5月に新規Railsプロジェクト開発のノウハウについてのイベントを開催します💡 詳細はこちら💨
弊社事例を存分にご紹介いたしますので、東京近郊にお住まいの方ぜひお越しください! 軽食をご用意してお待ちしております😋

---

最後に、参加エンジニア全員で撮った1枚を。また皆様にお会いできることを一同楽しみにしております!

f:id:chmv:20190424181111j:plain
また来年、松本でお会いしましょうー!


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


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

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

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

■開発環境はこちら

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

*1:※ 体質的なアルコール耐性を判定する簡易検査キットのことです。

*2:弊社エンジニア・ぱんのTwitterより↓↓

Railsで処理を別クラスに切り出す方法について

$
0
0

こんにちは。メドピアのRuby(Rails)化をお手伝いしている@willnetです。最近はエンジニアが増えた影響か、Railsの質問に答えていることが多いです。

以前、Railsの太ったモデルをダイエットさせる方法についてというタイトルでPOROを使っていこうという話を書きました。その際にコード例などもなるべく多く載せるようにしたのですが、このエントリだけを読んだ状態では、いざ「POROを使ってみよう!」としたときにまだ悩む余地がありそうです。

POROはその名の通り普通のRubyオブジェクトなので、いろんな書き方ができてしまいます。それなりに経験がある人でないと、どのように書いたらいいんだろう…と悩んで時間を使ってしまいそうですね。さらに、複数人で開発しているチームだと書き方のバラツキも気になるところです。きっと、POROを書くときのお作法が決まっている方が開発しやすいはず。

そこで、お作法を決める手助けをするために例を出してみます。

コード例

slackのようなサービスを作っていると想像してみてください。Messageモデルをsaveしたときに、それがhereメンションのときはチャンネル内のアクティブなメンバーのみ、channelメンションのときはチャンネル内のすべてのメンバーに対してMentionを作るという処理をMessageモデルに定義しています。

classMessage< ApplicationRecord
  has_many :mentions
  belongs_to :creator, class_name: 'User'
  belongs_to :channel

  after_create :create_here_mention, if: :here?
  after_create :create_channel_mention, if: :channel?defhere?; end# 省略defchannel?; end#省略defcreate_here_mention
    members = channel.members.active - [creator]
    create_mentions(members)
  enddefcreate_channel_mention
    members = channel.members - [creator]
    create_mentions(members)
  endprivatedefcreate_mentions(members)
    members.each do |member|
      mentions.create!(to: member, chennel: channel)
    endendend

そもそもコールバック使うのどうなの?など議論の余地があるコードですが、そこまで考え出すとこのエントリで取り上げる範囲が広がりすぎてしまうためそのあたりは無視してください*1。このコードからcreate_here_mentioncreate_channel_mentioncreate_mentionsを別クラスに切り出してみるとします。さてどう切り出すのが良いでしょうか。

切り出し方にいろいろな選択肢が存在します。

  • クラスやメソッドの名前はどのような観点で決めると良いでしょうか?
  • メソッドはクラスメソッドにすべきでしょうか。インスタンスメソッドにしたほうが良いでしょうか?
  • クラスは一つでいいでしょうか?切り出すメソッドごとにクラスを作ったほうがよいでしょうか?

これらの点について、僕は自分なりの意見を持っています。それが正しいかはさておき、他の人がどういう観点で判断をしているのかを知ることで、みなさんがPOROを書くときに迷うことが減るのではないかと思います。

POROに切り出した後のコード

説明の前に、切り出した後のコードを載せます。このコードを参考にしつつ、どういう観点で切り出しているのか書いていきます。

classMessage< ApplicationRecord
  has_many :mentions
  belongs_to :creator, class_name: 'User'
  belongs_to :channel

  after_create :create_here_mention, if: :here?
  after_create :create_channel_mention, if: :channel?defhere?; end# 省略defchannel?; end#省略defcreate_here_mentionHereMentionCreator.call(message: self)
  enddefcreate_channel_mentionChannelMentionCreator.call(message: self)
  endend
classHereMentionCreator
  delegate :channel, :creator, to: :messagedefself.call(message:)
    new(message: message).call
  enddefinitialize(message:)
    @message = message
  enddefcall
    members.each do |member|
      message.mentions.create!(to: member, chennel: channel)
    endendprivateattr_reader:messagedefmembers@members ||= channel.members.active - [creator]
  endend
classChannelMentionCreator
  delegate :channel, :creator, to: :messagedefself.call(message:)
    new(message: message).call
  enddefinitialize(message:)
    @message = message
  enddefcall
    members.each do |member|
      message.mentions.create!(to: member, chennel: channel)
    endendprivateattr_reader:messagedefmembers@members ||= channel.members - [creator]
  endend

メソッド名は統一する

POROに切り出したとき、publicなインターフェースはcallもしくはnew(つまりinitialize)で統一するようにしています。基本的にはcallで、インスタンス化したオブジェクトを返すだけでよいときのみnewという使い分けをしています。

まずメソッド名を考え、それから属するクラスを決めるものだ、という言説があるのは知っていて(要出典)以前はそのように実装していました。しかしHereMentionCreatorのようなクラス名をつけることで、callメソッドがhereメンションを作るのだな、と十分推測可能です。またメソッド名が統一されていると「このクラスのメソッド名ってなんだっけ?」とならずに便利なので最近は統一するようにしています。

処理の実態はインスタンスメソッドに書く

今回やろうとしていることは、「hereメンションを作る」と「channelメンションを作る」という手続きを切り出すことです。なので次のようにクラスメソッドで実装する人も時々見かけます。

classHereMentionCreatordefself.call(message:)  
    channel = message.channel
    members = channel.members.active - [message.creator]
    
    members.each do |member|
      message.mentions.create!(to: member, chennel: channel)
    endendend

サンプルコードが簡単なので、なんだかこれでも問題なさそうに見えますね。しかし手続きがもっと多くなるとどうでしょうか。

チャンネルのミュートの概念を追加し、さらにプッシュ通知もするように機能追加したコードを書いてみます。

classHereMentionCreatorPUSH_NOTIFICATION_LIMIT = 100defself.call(message:)
    channel = message.channel
    members = channel.members.includes(:mute_channels).active - [message.creator]

    members.each do |member|
      message.mentions.create!(to: member, chennel: channel, mute: member.mute?(channel))
    end

    not_mute_members = members.reject { |member| member.mute?(channel) }
    not_mute_members.map(&:id).each_slice(PUSH_NOTIFICATION_LIMIT).with_index do |ids, index|
      PushNotificationWorker.perform_in(index.minutes, message.id, uids)
    endendend

これでも読める人は問題なく読めると思いますが、さっきよりも概要を掴みづらくなったのは間違いないはず。

インスタンスメソッドで実装すると次のように書くことができます。

classHereMentionCreatorPUSH_NOTIFICATION_LIMIT = 100

  delegate :channel, :creator, to: :messagedefself.call(message:)
    new(message: message).call
  enddefinitialize(message:)
    @message = message
  enddefcall
    create_notifications
    create_push_notifications
  endprivateattr_reader:messagedefcreate_notifications
    members.each do |member|
      message.mentions.create!(to: member, chennel: channel, mute: member.mute?(channel))
    endenddefcreate_push_notifications
    not_mute_members.map(&:id).each_slice(PUSH_NOTIFICATION_LIMIT).with_index do |ids, index|
      PushNotificationWorker.perform_in(index.minutes, message.id, uids)
    endenddefmembers@members ||= channel.members.includes(:mute_channels).active - [message.creator]
  enddefnot_mute_members@not_mute_members ||= members.reject { |member| member.mute?(channel) }
  endend

callメソッドがcreate_notificationsメソッドとcreate_push_notificationsメソッドを呼ぶだけになり、処理の概要がつかみやすくなりました。また、membersnot_mute_membersもローカル変数からインスタンスメソッドに切り出されたことで、それぞれのメソッドの行数が減り、処理の内容を把握しやすくなっています。

このように、メソッド分割することで抽象化がしやすくなるのがインスタンスメソッドを利用する主な理由です。

こう書くとクラスメソッドでもメソッド分割できるのでは?という意見がでてきそうですが、クラスメソッドで同様のことをやろうとするとクラスインスタンス変数を更新するコードになり、結果としてスレッドセーフではないコードになってしまいます。

一つのクラスには一つの公開インターフェース

今回はHereMentionCreatorとChannelMentionCreatorのように2つのクラスに切り出しましたが、次のように単一のクラスにhereメンションをするメソッドとchannelメンションをするメソッドを定義する人もいるのではないでしょうか。

classMentionCreator
  delegate :channel, :creator, to: :messagedefself.here(message:)
    new(message: message, type: :here).call
  enddefself.channel(message:)
    new(message: message, type: :channel).call
  enddefinitialize(message:, type:)
    @message = message
    @type = type
  enddefcall
    members.each do |member|
      message.mentions.create!(to: member, chennel: channel)
    endendprivateattr_reader:message, :typedefmembers@members ||= if type == :here
      channel.members.active - [creator]
    else
      channel.members - [creator]
    endendend

一見これでも問題なさそうに見えます。しかし仕様が変更されるについてメンテナンスが難しくなってきます。例えば新しくeveryoneメンションもMentionCreatorで扱うようにするとどうなるでしょうか。everyoneメンションは基本的にchannelメンションと同じですが、すべての人が参加しているチャンネル(generalチャンネル)以外ではメンションとして扱われないという仕様です。

素直にMentionCreatorクラスを拡張してみます。

classMentionCreator
  delegate :channel, :creator, to: :messagedefself.here(message:)
    new(message: message, type: :here).call
  enddefself.channel(message:)
    new(message: message, type: :channel).call
  end# 追加defself.everyone(message:)
    new(message: message, type: :everyone).call
  enddefinitialize(message:, type:)
    @message = message
    @type = type
  enddefcallreturnif type == :everyone&& !channel.general? # 追加

    members.each do |member|
      message.mentions.create!(to: member, chennel: channel)
    endendprivateattr_reader:message, :typedefmembers@members ||= if type == :here
      channel.members.active - [creator]
    else
      channel.members - [creator]
    endendend

結果として、callメソッドに分岐が一つ増えることになりました。このように分岐が増えていくと、1つのユースケース(例えばhereメンションのとき)だけについて考えたい状況でも別のユースケース(channelやeveryoneメンションのとき)のコードについて理解しなければいけなくなり、そのぶん可読性が落ちます。また、コードを修正したときに想定していない箇所でバグを仕込んでしまう、というケースも次第に増えていくことでしょう。

最初の例のように1つの処理ごとにクラスを作り、できるかぎり分岐を避けることでメンテナンスしやすくなります。

まとめ

僕がPOROを書くときの書き方について、それぞれ根拠を添えて説明しました。他にも良いやり方はあると思うので「俺はもっと良い書きかたを採用している!」という人がいたらどのように書いているのか教えていただけると嬉しいです(\( ⁰⊖⁰)/)


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


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

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

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

■開発環境はこちら

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

*1:コールバックについてはまた別のエントリでとりあげるかもしれません

Ruby2.7の(実験的)新機能「パターンマッチ」で遊ぶ

$
0
0

はじめまして、メドピアのサーバサイドエンジニアの草分です。

RubyKaigi2019 1日目のセッションにてRubyのexperimental(実験的)な新機能「パターンマッチ」(Pattern Matching)が発表されましたね。

speakerdeck.com

この記事では発表で紹介されたパターンマッチの各種機能を確認し、遊んでみます。

概要

Rubyのパターンマッチとは?

Rubyist向けの説明として以下のような説明がなされていました。

  • データ構造の評価によるcase/when条件分岐の提供
  • 変数への多重代入

実際に例文を見てみましょう。

例文

case [0, [1, 2, 3]]
in [a, [b, *c]]
  p a #=> 0
  p b #=> 1
  p c #=> [2, 3]end

case/whenではなくcase/inが追加され、caseで指定したオブジェクトとinに記載したデータ構造(パターン)を比較し、マッチした場合処理が行われます。

また、マッチした場合はパターンに記載した各変数に値がバインドされ、内部の処理で利用することが可能となります。


通常の多重代入とは異なり、オブジェクトのデータ構造とパターンが一致しない場合は処理されません。

case [0, [1, 2, 3]]
in [a]
  :unreachable# 配列構造が異なるため処理されないin [0, [a, 2, b]]
  p a #=> 1
  p b #=> 3end

その他、Hashもサポートされています。

case {a: 0, b: 1}
in {a: 0, x: 1}
  :unreachable# Hashの構造が異なるため処理されないin {a: 0, b: var}
  p var #=> 1end

それではこの機能が実装されているRubyを導入し、実際に試してみましょう。

準備

最新のRubyを導入する

  • まずは最新のRubyのソースコードをcloneし、trunkをチェックアウトします。
$ git clone https://github.com/ruby/ruby.git
$ cd ruby
$ git checkout origin/trunk
$ autoconf
$ ./configure
$ make
$ make install
  • しばらく待てば最新Rubyの完成です。

  • rbenvをお使いの場合、2019年5月時点では2.7.0-devバージョンを用いるとパターンマッチが利用可能なrubyをインストールすることができます。
$ rbenv install 2.7.0-dev

解説

基本文法/仕様

試す前に、パターンマッチの基本文法を確認してみましょう。

case expr
in pattern [if|unless condition]
 ...
in pattern [if|unless condition]
 ...
else
 ...
end

case 句に記載したものを検査対象として、以下のように動作します。

  • 記載したパターンを上から順番に評価し、最初にマッチしたものが処理される。
  • 1つもマッチしない場合はelse句の内容が処理される。
  • 1つもマッチせず、else句も存在しなかった場合は NoMatchingPatternError例外が発生する。

また、if/unlessによるguardも可能となっています。

case [1, 1]
in [a, b] unless a == b
  :unreachable# ここは処理されないend

パターンマッチの基本文法は以上ですが、実行するとwarning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!という警告が表示されます。
警告の通り、パターンマッチは実験的な機能であり将来振る舞いが変更される可能性があります。ご注意ください。

次は、現時点で利用可能なパターンの記法を1つずつ確認してみましょう。

Value pattern

case0in0in-1..1inIntegerend
  • ===で比較して一致した対象にマッチします。
  • そのため、上記3パターンはいずれもマッチさせることができます。
    • ※処理が実行されるのは最初にマッチしたパターンのみです。

Variable pattern

case0in a
  puts a #=> 0end
  • 任意の値にマッチし、マッチした値はローカル変数としてバインドされます。
case [0, 1]
in [_, _]
  :reachableend
  • また、変数として利用しない場合は_を使うことができます。

Alternative pattern

case0in0 | 1 | 2:reachableend
  • |を用いることで複数のパターンにマッチさせることができます。

As pattern

case0inInteger => a
  a #=> 0end
  • =>を用いることでマッチした値を任意の変数にバインドすることができます。
  • rescueの使い方と似た記法ですね。
    • rescue StandardError => e

Array pattern

ここから少し複雑になってきます。

Array patternでは以下のいずれかの記法がパターンとして利用できます。

pat: Constant(pat, ..., *var, pat, ...)
   | Constant[pat, ..., *var, pat, ...]
   | [pat, ..., *var, pat, ...] # BasicObject(...)のシンタックスシュガー

そして以下の条件を満たすとマッチします。

  • Constant(何らかのclassを指定) === 検査対象objectがtrueを返す
  • 検査対象objectの#deconstructメソッドが配列を返す
  • そこで返した配列と指定したパターンがマッチする

この#deconstructメソッドの使い方が鍵になります。
まずはArrayを対象としたArray patternのパターンマッチから見ていきましょう。

classArraydefdeconstructselfendendcase [0, 1, 2] # ↓4種のいずれの記法でもマッチ可能in Array(0, *a, 2)
inObject[0, *a, 2]
in [0, *a, 2] 
in0, *a, 2# []は省略可能end

次はStructを対象としたArray patternのパターンマッチを見てみましょう。

classStructaliasdeconstructto_aendColor = Struct.new(:r, :g, :b)
color = Color[255, 0, 0]

case color
in [0, 0, 0]
  puts "Black"in [255, 0, 0]
  puts "Red"in [r, g ,b]
  puts "#{r}, #{g}, #{b}"end

このように、独自に#deconstructを定義することで任意のobjectに対してArray patternによるパターンマッチを行うことができるようになります。
これがRubyのパターンマッチの大きな特徴の1つとなっています。

Hash pattern

Hash patternでは以下のいずれかの記法がパターンとして利用できます。

pat: Constant(id: pat, id:, ..., **var)
   | Constant[id: pat, id:, ..., **var]
   | {id:, id: pat, **var} # BasicObject(...)のシンタックスシュガー

そして以下の条件を満たすとマッチします。

  • Constant(何らかのclassを指定) === 検査対象objectがtrueを返す
  • 検査対象objectの#deconstruct_keysメソッドがHashを返す
  • そこで返したHashと指定したパターンがマッチする

前述のArray patternと似た形式で、Hashのようにkey名を指定してパターンマッチを行うことができます。

classHashdefdeconstruct_keys(keys)
    selfendendcase {a: 0, b: 1} # ↓4種のいずれの記法でもマッチ可能in Hash(a: a, b: 1)
inObject[a: a]
in {a: a}
in {a: a, **rest}
  p rest #=> {b: 1}end

こちらも独自に#deconstruct_keysを定義することで任意のobjectに対してパターンマッチを行うことができるようになるため、Rubyのパターンマッチの大きな特徴の1つとなっています。

また、#deconstruct_keysの引数keysには処理の効率化のためのヒントとして、パターンの処理に必要なkey名の配列が渡されます。 その配列に含まれないkeyについては返却せず無視してよく、処理コストの削減を行うことができます。

遊ぶ

クイックソートしてみる

「クイックソートはパターンマッチを用いると書きやすい」という情報を社内で耳にしたため試してみます。

処理対象は数値の配列であることを前提として、ソート処理を書いてみました。

defqsort(ary) 
  case ary
  in [piv, *xs] # 要素が2個以上
    lt, rt = xs.partition{|x| x < piv}
    qsort(lt) + [piv] + qsort(rt)
  else# 要素が0個または1個
    ary
  endend

ピボットの位置は先頭で決め打ちですが、かなりシンプルに書けますね!

逆にパターンマッチを使用せずに書くとすればこんな感じでしょうか。

defqsort2(ary)
  return ary if ary.length <= 1
  piv = ary[0]

  lt, rt = ary[1..].partition{|x| x < piv}
  qsort2(lt) + [piv] + qsort2(rt)
end

比べてみると、配列長の確認や配列の分割が冗長に見えてきますね。 オブジェクト構造による条件分岐と変数へのバインドが同時に行えるパターンマッチは、かなり強力な文法なのではないでしょうか。

独自拡張クラスを作ってみる

Hash patternをHash以外で使ってみたかったので、Timeクラスを独自拡張し、令和表記で年号を返すメソッドを作ってみました。
As patternと組み合わせて利用しています。

classTimedefdeconstruct_keys(keys)
    { year: year, month: month, day: day }
  enddefto_reiwacaseselfinyear: 2019, month: (5..)
      "令和元年"inyear: (2020..) => year
      "令和#{year - 2018}"else"not令和"endendend

実行してみます。ちゃんと動きましたね。

p Time.local(2019, 5, 1).to_reiwa  # => 令和元年
p Time.local(2019, 8, 1).to_reiwa  # => 令和元年
p Time.local(2020, 1, 1).to_reiwa  # => 令和2年
p Time.local(2019, 4, 30).to_reiwa # => not令和

引数の型指定をしてみる

当日の発表では「引数に対してパターンマッチを実行可能とするのは技術的には可能だが、型の指定に使われることが想定されるので実装しなかった。」という内容の発言がありました。

ということで言いつけを破ってTryしてみましょう。

defarg(num, str)
  case [num, str]
  in [Integer, String]
    :okendend

このメソッドを呼び出してみると…

p arg(3, "3")     # => :ok
p arg("2", 2)     # => NoMatchingPatternError
p arg(:a, "a")    # => NoMatchingPatternError
p arg(1, :a.to_s) # => :ok

引数の型を間違えると例外をraiseするメソッドになりました! 全くRubyらしくありませんね。

まとめ

RubyKaigi2019で紹介されたパターンマッチの機能を一通り触ってみました。まだ実験的な機能であるとのことで仕様変更の可能性はありますが、正式リリースが楽しみな機能です。

この他にもRuby3で実装が予定されている静的解析や、逆に搭載が見送られた仕様など、将来のRubyに関する具体的な情報が次々発表されたRubyKaigi2019でした。今後のRubyの動向から目が離せませんね!


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


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

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

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

■開発環境はこちら

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

メドピアエンジニアに聞いてみたアンケート

$
0
0

こんにちは。 メドピアでエンジニアのゆるふわマネジメントをしている@arihhです。

事業拡大とともにエンジニアの数が増えている弊社ですが、 今回はそんな弊社エンジニアにどんな人がいるのか社内外向けに知ってもらうべく、 エンジニアのみなさんにアンケートをとってみました。

Q1.エディタは何を使っていますか?

みんな大好きエディタ論争です。 f:id:arihh:20190516141551p:plain

社内のemacs・vim戦争の現状をお見せしようと思ってのアンケートだったのですが、 VSCodeが2/3を占めるという一強状態の結果となりました。

Q2.shellは何を使っていますか?

エディタは何を使っているのかのアンケートは見たことがあるのですが、 開発のお供のshellはあまりみたことがないので聞いてみました。

f:id:arihh:20190516152555p:plain

こちらはfish・zsh・bashの3すくみとなりました。

Q3.はじめてHello Worldした言語は?

普段業務ではRubyを書いているエンジニアが多い弊社ですが、初めてのプログラミングを学んだときの言語を聞いてみました。

f:id:arihh:20190516184926p:plain

PHP・Rubyという人は0でした!!

初めての言語はRuby以外の言語の人が多く、またBASICからプログラムをやっている人も多いようです。 その他ですが、VB、MATLAB、Delphiなどの声がありました。

Q4.マウスやキーボードは何を使っていますか?

メドピアでは入社したエンジニアにMacBookPro(JIS/US選択可)を支給していますが、 それ以外にもキーボードを持参してきている方も多いので聞いてみました。

こちらはキーボードは標準のままが半数、HHKBが2割、Mac純正品が1割強といった結果でした。 また、キーボード・マウスの組み合わせでは以下のような回答がありました。

  • Majestouch MINILA / Logicool MX ERGO
  • ARCHISS ProgresTouch TKL US配列(Cherry MX 静音赤軸) / Kensington ExpertMouse ワイヤレストラックボール K72359JP
  • HHKB Pro type-S / SlimBlade Trackball
  • MiSTEL BAROCCO MD600(分離キーボード) / Apple Magic Trackpad 2
  • 素手

Q5.メドピアのいいところは?

ちょっと弊社のアピール的な内容になりますが、メドピアのいいところを聞いてみました。 挙がった声をまとめるとこんな感じです。

  • 働き方
    • 裁量労働制のため勤務時間がゆるい
    • 出社時間が自由
  • 制度や環境
    • テックサポート制度
    • 各種補助制度
    • エンジニアの成長補助が手厚く、Rubyコミュニティへの貢献もしている。社内勉強会も活発。
    • オフィスが静かで良い、集中出来る。色々な場所で仕事もOK
  • 風土・文化
    • 個々に裁量があり、決定する際にも議論がある所
    • 手を挙げれば挑戦させてくれるところ
    • 自分から手を伸ばせばなんでもできる、やらせてもらえるところ
    • コードをリファクタリングしていく文化
    • エンジニア文化が根付いている!医療に関わる事業なので社会貢献に直結している!
    • スキルアップ・新技術選定を柔軟に検討できるところ!
  • メンバー
    • 自分にできないことをできる人が近くにいて、安心して挑戦できるところ
    • 落ち着いたエンジニアが多いと感じるところ。
  • CTO
    • CTOの雰囲気
    • なんか文句あってもCTOにいえば解決してくれる
    • CTOにメンションすると本を買ってもらえるところ

裁量労働制が本当に裁量労働制として勤務時間に裁量をもって業務ができたり、 テックサポート制度やRubyKaigiの参加などを会社が支援してくれたりするのはいいと思います! また、リファクタリングをしていく文化やCTOをイジる文化があるのもいいところかなと思います。

Q6.影響を受けた本は?

こちらの結果は思った以上にバラバラになってしまったので、並べて公開します (Amazonさんへのリンクはないです💦)

  • 達人プログラマー
  • エクストリームプログラミング
  • SCRUM BOOT CAMP THE BOOK
  • カイゼン・ジャーニー
  • ティール組織
  • [24時間365日]サーバ/インフラを支える技術 ……スケーラビリティ,ハイパフォーマンス,省力運用
  • カッコウはコンピュータに卵を産む
  • 実践ドメイン駆動設計
  • エンタープライズアプリケーションアーキテクチャパターン
  • オブジェクト指向のこころ
  • イノベーションのジレンマ
  • プロを目指す人のためのRuby入門
  • メタプログラミングRuby
  • 達人に学ぶ SQL徹底指南書
  • UNIXという考え方―その設計思想と哲学
  • 初めてのPerl
  • リーダブルコード
  • シリコンバレー流世界最先端の働き方
  • Exceptional C++
  • ブッダの教え一日一話
  • 自分の小さな「箱」から脱出する方法
  • すべてがFになる
  • ぼくらの七日間戦争

Q7.好きなコマンド

最後はネタ項目です!

peco
fzf
git checkout branchname origin/branchname
# shutdown -r now
git diff --cached
tail -f
vmstat(だが、medpeerのサーバに入ってない)
tap
hub
grep
z - https://github.com/rupa/z
whois
yes "sl" | sh
history | grep "hoge"
man
grap..less..?
open

最後に

アンケートに協力していただいたエンジニアの皆さんに、この場を借りて協力ありがとうございました。

今回のアンケートで想像以上に個性豊かなメンバーが集まっていることが再確認できました。 今回の記事で雰囲気など掴んでいただければうれしいです。


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


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

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

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

■開発環境はこちら

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

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

$
0
0

メドピアマッスル部上腕二頭筋担当、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 を利用した検証環境自動構築については、こちらのイベントで登壇させていただくのでお話出来ればと思います。

connpass.com

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


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


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

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

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

■開発環境はこちら

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

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

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

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

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

$
0
0

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

メドピアでは、年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.comhttps://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:plainf:id:degwinthegreat:20190701211105j:plain

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


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

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

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

■開発環境はこちら

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


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

$
0
0

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

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

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

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

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

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

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

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

検索妨害

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

html

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

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

js

$('.header__foo').hide()

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

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

<template>

<divclass="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

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

$
0
0

こんにちは。サーバーサイドサウナーの川井田(@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

v-onから辿るVueの細道

$
0
0

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

v-onの不思議

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

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

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

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

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

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

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

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

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

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

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

methods埋込み型

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

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

methods実行型

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

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

③ 関数埋め込み型

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

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

④ 式埋め込み型

<inputtype="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イベント用っぽいですが、非ネイティブなコンポーネントについても同じイベントハンドラの文字列パースがされているので共通で使うことができます。
イベントをそのまま親コンポーネントに渡したいときのショートハンドとして使えなくもないかもしれません。

<inputtype="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

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

$
0
0

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

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

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

これまでの設定例

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

Capybara.register_driver :headless_chromedo |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
endCapybara.javascript_driver = :headless_chrome

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

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

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

解決策

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

Capybara.register_driver :headless_chromedo |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
  )
endCapybara.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

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

$
0
0

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

医師専用コミュニティサイト「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

半年間の開発環境の改善を振り返る

$
0
0

こんにちは、メドピアCTO室 SREの侘美(たくみ)です。
普段はRails/Vue.js/terraform/Lambdaなどを書いています。
趣味は飼い猫と遊ぶことで、生傷が絶えません。

入社してから約半年間、Railsのプロジェクトで実装をしつつ、合間に開発環境の改善をいろいろとやってきました。けっこうな分量となったので、紹介したいと思います。

なお、本記事で扱う開発環境とは下記2つを指すこととします。

  • ソースコードの修正/テストの実行/静的解析の実行環境
  • サービスを起動し、ブラウザでデバッグする環境

特徴

主な改善対象である、「MedPeer」サービスの特徴をご紹介します。

  • Ruby on Rails製
  • 社内では最も巨大なRailsプロジェクト
  • モデル数693
  • 認証サービス、旧サービス(PHP製)と連携している
  • 開発環境はDocker for Macを利用
    • コンテナ数は旧システム、認証システムを入れて32個
  • ソースコードはdocker-syncを使ってローカルとコンテナ内で同期
  • RSpecやrubocopの実行もコンテナ内で実行

システム構成としては、ざっくり下図のようになります。

f:id:satoshitakumi:20191028124949p:plain
サービス構成

課題

私が入社した当時、MedPeerサービスの開発環境には下記のような課題がありました。

  • 電池消費が激しい
  • 起動に時間がかかる
  • メモリ消費が激しい
  • 起動に失敗するときがある
  • docker-syncが不安定
    • ファイルが同期されない
    • CPUが高負荷になる
  • 依存システムのコンテナが多い
  • Linux対応していないため、一部テスト環境のメンテナンスが放置され気味

改善内容の紹介

それでは、改善内容を順に紹介していきます。

電池消費

開発環境を立ち上げていると電池をモリモリ消費する状態でした。
開発環境のコンテナ群を立ち上げっぱなしで、ミーティングに出ると、だいたい、2時間程度で電池が切れてしまうくらいでした。

docker statsでCPU使用率が高いコンテナを絞り込み、topコマンドでプロセスのCPU使用率を確認したところ、SpringのプロセスがCPUを常に利用していることを特定できました。

SpringはRails 4.1から標準で付属するようになったapplication loaderです。 常にバックグラウンドで実行しつづけることで、rails consoleやRSpecの実行等、ロードを伴うコマンドの実行を高速にしてくれます。 しかし、MedPeerサービスではロード対象となるファイル数が多いためか、常時それなりのCPUを消費していました。

Springには、DISABLE_SPRINGという環境変数を指定することで、無効にする機能があります。 この環境変数を設定して一律で無効にすることもできるのですが、電池消費よりもスピードを優先するエンジニアも当然いますので、任意に設定できるように対応しました。 medpeer.jpの開発環境にはdirenvが導入されているので、これを利用し、各エンジニアのローカルで指定した環境変数に応じて、コンテナ内にDISABLE_SPRINGを引き渡すように設定しています。

起動が不安定対策

メモリ消費が多いこともあり、開発環境を落とすことが多いのですが、起動しようとすると、Railsの起動や依存コンテナの起動に失敗することが多々ありました。

原因は様々あったのですが、大半は下記のように起動順序によるものでした。

  • gem、npmの依存ライブラリのインストール量で起動順序が変わっている
  • DBやElasticsearch、fluentd等依存コンテナの起動より先にこれらに接続するコンテナが立ち上がり、エラーとなる

もちろん、docker-composeによるlinksdepends_onを指定し、コンテナの起動順序は担保しているのですが、これらではコンテナ内のプロセスがreadyになったことまでは担保してくれません。

そこで、コンテナ内のプロセスが応答するまでwaitしてくれるufoscout/docker-compose-waitを利用することにしました。 似たツールはいくつもあるのですが、複数のコンテナ/ポートへの疎通チェックができる点で、docker-compose-waitを採用しました。

READMEに書いてあるように取得したスクリプトを /waitにマウントし、環境変数と起動時のcommandを設定することで、指定したコンテナの指定したポートに疎通することをチェックしてから、任意のコマンドを実行することが可能です。

サンプルのdocker-compose.ymlは以下のようになります。

---version:'3'services:web:image: ${ECR_NAME}/app:1.0
    environment: # チェックする対象を環境変数に定義するWAIT_HOSTS: fluentd:24224,elasticsearch:9200
    # /waitが終了したら、実行したいコマンドを実行するcommand: /bin/sh -lc "/wait && ./bin/setup"volumes:- ./bin/wait:/wait
      # その他ソースコードのマウント設定fluentd: # fluentdコンテナの設定elasticsearch: # elasticsearchコンテナの設定

この設定により、依存モジュールのインストール状況等で、各コンテナの起動スピードが多少変化しても、コンテナの起動順序を担保し、起動に失敗し辛い設定することができました。

脱docker-sync

docker-syncが不安定であることもエンジニア内で問題視されていました。

  • 異常にCPUを消費するときがある(暴走状態!)
  • ホスト-コンテナ間の同期が遅いときがあり、ソースコードの変更が反映されていないときがある(突然の死!)

(上記の問題はdocker-syncが利用しているunison起因であることまでは確認しています)

docker-syncはネイティブなDocker環境ではないMac等の環境において、ホスト-コンテナ間のファイルの参照を高速化されるために開発されたツールです。

そもそも、Docker for MacはVM上でLinuxを起動し、そのLinux上のDockerを利用する形なので、ホスト-コンテナ間のファイルの参照(同期)が遅いものとして有名です。

ということで、全員Linuxで開発すれば解決です!!

docker volumeのcacheオプションを利用することで、docker-syncをやめて安定/高速なソースコード同期を実現しようとチャレンジしてみました。

dockerでは、volumeマウントのdelegatedオプションを利用することで、ホスト-コンテナ間でのファイルの参照の遅延を許容し、高速な参照を実現することが可能です。

# beforevolumes: # sync-volumeへの./appのマウントはdocker-sync.ymlファイルで定義されている- sync-volume:/app
# aftervolumes:- ./app:/app:delegated

結論から言うと、脱docker-syncはできませんでした。

開発環境を立ち上げ、複数のページの表示速度を計測した結果、いずれも約2.3倍程度表示スピードが劣化し、ページの表示に2~3秒かかるようになってしまいました。 この数値はしばしば見かける「docker-syncで約2倍程度高速になる」という記述とも合致しており、確からしい結果となりました。

ある程度巨大でファイル数のあるプロジェクトにおいて、Docker for Macで開発を行う場合、docker-syncが最も高速であるという知見を得ました。 一方で規模の小さいアプリケーションであれば、docker volumeのcacheオプションだけでも開発環境のページ描画はそこまで遅くならないので、安定性を重視してdocker-syncの利用は不要だと思います。

また、docker-syncを利用していないプロジェクトにdelegatedオプションを導入したところ、jestの実行が数倍高速になるといった副次効果を得ることもできました。

依存モジュールをコンテナ内に閉じ込める

こちらは、docker-syncを導入していないプロジェクトに導入した設定です。

前述したように、Docker for Macのvolumeマウントはとても遅いので、ホスト側のファイルを参照する量が増えるほど、コンテナ内でのRailsの挙動は遅くなります。

上記の課題を解決するため、下記のような対策を採りました。
現状、vendor/bundle配下のファイルがホスト側で必要になるシーンが特にないため、vendor/bundle以下のファイルはコンテナ内のみに存在するように構成を変更します。 また、コンテナを破棄/再作成した際に、vendor/bundle以下のファイルが削除されてしまうと、再度bundle installするのに時間がかかってしまうため、docker volumeを使い、コンテナのライフサイクルとは別に永続化しています。

具体的なdocker-compose.ymlファイルは下記のようになります。

# beforeservices:web:volumes:- ./app:/app

# afterservices:web:volumes:- ./app:/app
      - bundle:/app/vendor/bundle
      - node_modules:/app/node_modules
volumes:bundle:{}node_modules:{}

図で示すと、下図のようになります。

f:id:satoshitakumi:20191028132546p:plain
before

f:id:satoshitakumi:20191028132622p:plain
after

この構成により、コンテナ内から参照するホスト側のファイル数を減らし、Railsの動作を高速化することができます。

Linux対応

MedPeerサービスの一部のテスト環境は、Ubuntu上に構築した開発環境で動作しています。

過去にdocker-sync導入後、Ubuntu上の環境は別ブランチで構築する構成となっていたため、メンテナンスされず放置される傾向となっていました。 この問題を解決するため、同じコードでdocker-syncを利用したMac OSでも、docker-syncを導入していないLinux OSでも開発環境が構築できるように修正しました。

具体的な方法としては、docker-composeのoverride機能を利用します。 Mac OS上では、docker-compose.ymlを読み込み、docker-sync startdocker-compose upコマンドで環境を立ち上げます。 Linux OS上では、環境変数にCOMPOSE_FILE=docker-compose.yml:linux.ymlを設定し、docker-compose.ymlに加え、linux.ymlを読み込み、設定を一部上書きします。その設定でdocker-compose upコマンドで環境を立ち上げます。

Linux OSの場合にdocker-compose.ymlを上書きするlinux.ymlには、docker-syncに関するvolumeの設定を上書きし、通常のdockerによるvolume mountの仕組みでソースコードをマウントするように設定します。

---# docker-compose.ymlversion:'3'services:web:image: ruby:alpine
    volumes:- sync-volume:/app
volumes:sync-volume:external:true
---# linux.ymlversion:'3'services:web:volumes:- ./app:/app

さらに、Makefile中でOSに応じてCOMPOSE_FILEの設定を変更するように設定してあるため、同じmakeコマンドを実行することで、Mac OSでも、Linux OSでも環境が立ち上がるように構築されています。

認証機能の有無の切り替え

われわれのプロジェクトの中には既存の認証サービスと連携するものがいくつかあります。
開発環境でも本番環境と同等の認証の仕組みを動かそうとすると、認証に関係するコンテナだけで、13個ものコンテナを追加で起動する必要があります。

これでは明らかにローカルマシンのリソース消費が増えてしまうため、開発環境では認証機能をダミーに変更し、これら13個のコンテナを削除したい気持ちになります。 しかし、一方で認証機能の検証を行いたいシーンもあります。

そこで、新しいいくつかのプロジェクトでは開発環境で認証機能の有無を切り替えられるようにし、認証機能無しで開発環境を立ち上げた場合は、余計なコンテナが起動しないように設定しています。

こちらに関しても、docker-composeのoverride機能で実現しています。 認証機能を利用しない構成でdocker-compose.ymlファイルを用意し、これに認証サービスを追加するためのauth.ymlファイルを用意します。

これらのdocker-composeで利用するファイルは、USE_AUTH環境変数をdirenvを利用して設定し、Makefile中でCOMPOSE_FILE変数に設定することで制御しています。 また、Rails内部の実装でも認証をダミーの実装に切り替える必要があるため、USE_AUTH環境変数をRailsのコンテナに渡し、実装が切り替わるようにしています。

認証機能を利用しない構成の場合は、ベースとなるdocker-compose.ymlのみで構築されます。

---# docker-compose.ymlversion:'3'services:rails: # rails用コンテナの設定mysql: # mysql用コンテナの設定

f:id:satoshitakumi:20191028134355p:plain
認証機能を利用しない構成

認証機能を利用する場合は、Makefileにて環境変数COMPOSE_FILE=docker-compose.yml:auth.ymlを設定することで、認証機能を追加します。 COMPOSE_FILEに指定したことで、下記のauth.ymldocker-compose.ymlがoverrideされ、下の図のような構成でコンテナが立ち上がります。

---# auth.ymlversion:'3'services:rails:environment:USE_AUTH:auth: # 認証サービスのコンテナの設定 # その他認証系のコンテナが合計13個

f:id:satoshitakumi:20191028134420p:plain
認証機能を利用する構成

このような切り替え機構を導入することで、ほとんどの開発シーンでは認証機能を省略し、省リソースでの開発環境を実現することができました。

不要なコンテナの停止

「MedPeer」サービスの開発環境には、一部の機能の動作を検証すためのコンテナや、モバイル版含めてすべてのサービスが動作するように下記のコンテナも含まれています。
つまり、デフォルトはmaximumな構成となっています。

起動しているコンテナの中には、下記のようなものも含まれています。

  • メールの内容を確認するためのmailcatcherコンテナ
  • S3へのファイルアップロードを検証するためのminioコンテナ
  • モバイル版等、一部の機能で利用するAPI用コンテナ

こういったコンテナはすべての開発者の環境で必要となるわけではないので、不要なコンテナを停止させることで、開発環境のリソース消費量を削減しておきたいです。

一から構築する場合は、何度か紹介したdocker-composeのoverrideを利用し、デフォルトをminimumな構成とし、特定の開発時に必要なコンテナは別ファイルで定義し、環境変数で切り替えるのが良いでしょう。

しかし、今回はすでにデフォルトがmaximumな構成となっているため、docker-composeのoverride機能を利用し、不要なコンテナは起動後即終了するように設定することで、コンテナの起動数を減らす方向としました。 こちらも最小構成で十分な開発者は、direnvを利用し環境変数にCOMPOSE_FILEを設定することで、構成の切り替えをできるようにしています。

---# docker-compose.ymlversion:'3'services:rails: # rails用コンテナmailcatcher: # メールをwebUIで確認できるmailcatcherコンテナminio: # AWS S3互換のminioコンテナ
---# docker-compose.minimum.ymlversion:'3'services:mailcatcher:entrypoint:['echo', 'Service disabled']minio:entrypoint:['echo', 'Service disabled']

個人ごとのカスタマイズを可能に

上記を応用することで、自分のみのコンテナを追加したり、逆に特定のコンテナを起動しないようにするといったカスタマイズも可能になっています。

docker-compose.custom.ymlのようなファイルを作成し、任意の設定を追加します。 また、.git/info/excludeに設定しコミットから除外します。 環境変数のCOMPOSE_FILEを設定し、作成したdocker-compose.custom.ymlを読み込むようにすることで切り替えを実現できます。

まとめ

今までの半年間で行ってきた開発環境の改善を振り返ってみました。
単純なものからちょっとテクニカルなものまで、いろいろやったなあ、という感想です。

今後も改善を続けてイケてるモダンな開発環境を実現していきたいと思います!


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


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

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

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

■開発環境はこちら

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

f:id:satoshitakumi:20191029102404p:plain

Rubyバージョンアップで見つけたバグとハマりどころ

$
0
0

こんにちは、最近ruby-vipsに惚れ込み始めたエンジニアの宮原です🐕

先日、医師専用コミュニティサイト「MedPeer」で使用されているRubyをVersion 2.6.5にアップデートしました🎊

f:id:nyagato_00_miya:20191024163222p:plain

今回は、Rubyアップデートを行った際にハマった箇所について紹介と解説をしてみたいと思います。 また、類似の内容で発表もさせていただいておりますので、合わせてご一読いただければと思います。

今回紹介するハマりどころは、ActiveSupport::DurationのバグとSidekiqの安全な再起動についてです。

🐛Durationのバグ

最新のdevelopを取り込み、Rubyアップデートのブランチで作業しているとReceived 'killed' signalというエラーが発生し、CIのRSpecが途中で終了してしまう事象に遭遇しました。

f:id:nyagato_00_miya:20191024182229p:plain
3並列目でメモリを食いつぶし移行のテストが失敗している

調査を進めると、あるテストで異常な処理時間がかかっていることがわかりました。これによりCircleCIのメモリが枯渇し、残りのテストも軒並み失敗していることが判明しました。 メモリを枯渇させるほどの異常な処理の原因も調査を進めていくと、Range#stepActiveSupport::Durationを渡す箇所でパフォーマンスが著しく低下しており、こちらが原因であることを突き止めました。

どの程度、処理速度に差があるのかを確認するため、以下のコードで検証します。

require'active_support'require'active_support/core_ext'

start = Time.now
(0..300).step(15.seconds).to_a
pp "time: #{Time.now - start}"

処理速度は下記の通りで、Ruby 2.6系を使うと圧倒的に処理速度が悪化していることがわかります。

Ruby 2.5.3 Ruby 2.6.3
time: 2.1e-05 time: 11.550944

こちらの事象が発生する組み合わせは、下記の通りです。

ソフトウェア バージョン
Ruby 2.6.3, 2.6.4, 2.6.5
Rails 5.2.3
ActiveSupport 5.2.3

github.com※issueも上がってました。

🔍なぜ計算量が指数関数的に増加してしまったのか

Range#stepActiveSupport::Durationの挙動が怪しそうなので、検証していきます。 下記のようなベンチマークコードを利用して、処理速度を計測していきましょう。

require'active_support'require'active_support/core_ext'require'benchmark'

current = 0
step = 15.seconds

loopdo
  puts current

  time = Benchmark.realtime do
    current = step.coerce(current).sum
  ene

  p "processing time:#{time}"end

計測結果は下記の通りです。ご覧の通り、Ruby 2.6系ではステップ数が増加するごとに、著しく処理時間が増加していることがわかります。

f:id:nyagato_00_miya:20191031105425p:plain
Ruby 2.5とRuby 2.6による処理速度の比較

Integer#stepRange#stepは、Ruby 2.6からEnumerator::ArithmeticSequenceを返すように修正されました。 (Ruby 2.5までは、Enumeratorを返してました。)

Ruby 2.6の変更では、Enumerator::ArithmeticSequenceすなわち等差数列が返り値となることを期待しています。 しかし、期待するデータ構造が仮に(duration 15)としていたところに、(duration (duration (duration (duration (duration 15))))のようなネストが深いデータ構造で渡ってしまう場合、再帰処理も相まって処理時間が指数関数的に増加していました。

🔧修正するには

Ruby 2.6系とRails 5.2系の組み合わせで、当該事象を回避するにはIntegerとして値を渡せば大丈夫です。

(0..300).step(15.seconds.to_i).to_a

🚦Sidekiq Jobの安全な再起動

RubyやRailsのアップデートを実施する場合は、各サーバーをHard Restartする必要が生じます。 しかし、Jobサーバーなどを思い切ってHard Restartしてしまうと、実行中のJobが正しく完了しない場合や2重に実行されてしまうなどの問題が起きる可能性があります。 このため、Jobサーバーを安全に再起動する必要が生じました。

🔧安全な再起動手順について

「MedPeer」ではSidekiq Enterpriseを利用していますので、こちらの安全な再起動について解説します。

以下の手順でSidekiqを安全に再起動させていきます。

  1. Jobを新規実行しないようにする
  2. 実行中のJobが終わるまで待つ
  3. Sidekiqのプロセスを停止
  4. einhornのプロセスからSidekiqのプロセスを起動

まず、現在実行中のJob数を確認します。

takashi-miyahara@stg-hogehoge-batch-a01:~$ ps -ef | grep sidekiq
medpeer  10921     1  0 10月09 ?      00:00:00 einhorn: bundle exec sidekiq --config config/sidekiq.yml --environment staging
medpeer  12864 10921  2 15:38 ?        00:00:18 sidekiq 5.2.7 medpeer [3 of 10 busy] leader
#     ↑こちらがSidekiqのプロセス
takashi+ 13506 13421  0 15:50 pts/0    00:00:00 grep --color=auto sidekiq

どうやら3つのJobが実行中のようですね。

次に、Sidekiqのプロセスへ-TSTP(v5.0.0 未満は USR1)シグナルを送りkillします。

takashi-miyahara@stg-hogehoge-batch-a01:~$sudo kill -TSTP 12864

Sidekiqのプロセスをkill後に、プロセスを確認するとstoppingとなり徐々に実行中のJobが減っていきます。

takashi-miyahara@stg-hogehoge-batch-a01:~$ ps -ef | grep sidekiq
medpeer  10921     1  0 10月09 ?      00:00:00 einhorn: bundle exec sidekiq --config config/sidekiq.yml --environment staging
medpeer  12864 10921  2 15:38 ?        00:00:18 sidekiq 5.2.7 medpeer [1 of 10 busy] stopping
takashi+ 13506 13421  0 15:50 pts/0    00:00:00 grep --color=auto sidekiq

その後、すべてのJobが実行済みになることを確認します。

takashi-miyahara@stg-hogehoge-batch-a01:~$ ps -ef | grep sidekiq
medpeer  10921     1  0 10月09 ?      00:00:00 einhorn: bundle exec sidekiq --config config/sidekiq.yml --environment staging
medpeer  12864 10921  2 15:38 ?        00:00:18 sidekiq 5.2.7 medpeer [0 of 10 busy] stopping leader
takashi+ 13506 13421  0 15:50 pts/0    00:00:00 grep --color=auto sidekiq

これで、後続のJobが実行されない状態になりました。

次に、TERMシグナルを送りプロセスをkillします。

takashi-miyahara@stg-hogehoge-batch-a01:~$ sudo kill -TERM 12864

しばらくするとeinhornのプロセスが、自動的にSidekiqのプロセスを起動してくれます。 これは、einhornのPIDを親プロセスに持つ子プロセス(Sidekiqのプロセス)がkillされると、einhornが新しいプロセスを立ち上げます。

では、新しく起動したプロセスの様子を確認してみましょう。

takashi-miyahara@stg-hogehoge-batch-a01:~$ ps -ef | grep sidekiq
medpeer  10921     1  0 10月09 ?      00:00:00 einhorn: bundle exec sidekiq --config config/sidekiq.yml --environment staging
medpeer  13535 10921 92 15:55 ?        00:00:02 /var/www/medpeer/shared/bundle/ruby/2.5.0/bin/sidekiq --config config/sidekiq.yml --environment staging
takashi+ 13537 13421  0 15:55 pts/0    00:00:00 grep --color=auto sidekiq

まだ、Ruby 2.5が使われているようですね。どうやらeinhornの自動再起動では新しいRubyを読み込んでくれないようです。 こんな時は、Sidekiqを明示的に再起動してあげましょう。

takashi-miyahara@stg-hogehoge-batch-a01:~$ sudo service sidekiq restart
takashi-miyahara@stg-hogehoge-batch-a01:~$ 
takashi-miyahara@stg-hogehoge-batch-a01:~$ ps -ef | grep sidekiq
medpeer  10921     1  0 10月09 ?      00:00:00 einhorn: bundle exec sidekiq --config config/sidekiq.yml --environment staging
medpeer  13535 10921 92 15:55 ?        00:00:02 /var/www/medpeer/shared/bundle/ruby/2.6.0/bin/sidekiq --config config/sidekiq.yml --environment staging
takashi+ 13537 13421  0 15:55 pts/0    00:00:00 grep --color=auto sidekiq
takashi-miyahara@stg-hogehoge-batch-a01:~$ 
takashi-miyahara@stg-hogehoge-batch-a01:~$ sudo service sidekiq status
● sidekiq.service - sidekiq
   Loaded: loaded (/lib/systemd/system/sidekiq.service; enabled; vendor preset: enabled)
   Active: active (running) since 水 2019-10-09 22:00:22 JST; 17h ago
  Process: 12782 ExecReload=/usr/local/rbenv/shims/bundle exec einhornsh --execute upgrade (code=exited, status=0/SUCCESS)
 Main PID: 10921 (bundle)
    Tasks: 22
   Memory: 242.6M
      CPU: 31min 34.652s
   CGroup: /system.slice/sidekiq.service
           ├─10921 einhorn: bundle exec sidekiq --config config/sidekiq.yml --environment staging
           └─13535 sidekiq 5.2.7 medpeer [0 of 10 busy] leader

無事、Ruby 2.6を使って起動していることがわかります。 これで、長時間Jobの実行を止めることなくSidekiqを安全に再起動することができました。

当該手順を実行する前に、Jobのスケジュールを確認し再起動できそうな時間帯などを確認しておくと良いでしょう。

これから

前々回のRubyアップデートから約9ヶ月を経て、最新のRubyで「MedPeer」が動作するようになりました。 「MedPeer」の開発メンバーも増えてきましたので、もう少し早い周期でRubyのアップデートができそうですね。

今後も、RubyやRailsのアップデートに関する内容を発信していければと思います!


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


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

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

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

■開発環境はこちら

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


メドピア開発合宿でVue.jsテストライブラリ「vue-function-tester」を作った話

$
0
0

涙の数だけ強くなるフロントエンドエンジニア村上(@pipopotamasu)です。

先週の水木金とメドピア恒例の開発合宿 in 熱海に行ってきたので、そこで作ったVue.jsのテストライブラリ「vue-function-tester」を紹介したいと思います。

github.com

f:id:ec0156hx39:20191121182751j:plain
atami

vue-function-testerとは

vue-function-testerは、Vue.jsの「メソッドの単体テストライブラリ」として作りました。
もっと具体的に言うと、Vue.jsのmethods, lifecycle hooks, computed, watchをテストするためのライブラリです。
※ちなみに「vue-method-tester」にしなかったのはmethodsプロパティと被ってややこしくなるのでやめました

Get started

長々と解説するよりは実際のコードをみて行きましょう。
テスト対象のコンポーネントとして、簡単な検索ボックスのサンプルコードを用意しました。

// SearchBox.vueexportdefault Vue.extend({
  data () {return{
      query: '',
      error: ''}},
  methods: {
    search () {this.error = ''if (this.query === '') {this.error = "Please input."}else{this.$emit('search', this.query)
        this.query = ''}}}})

上記のsearchメソッドに対して、vue-function-testerでテストを書くと以下のようになります
(※ jest依存ライブラリなのでjestのテストコードになります。余談ですがメドピアのフロントエンドはjestでバシバシテストコードが書かれています。)

# SearchBox.spec.js
import SearchBox from "@/components/SearchBox.vue";
import{ methods } from "vue-function-tester";

describe("search()", () => {const{ search } = methods(SearchBox);

  describe("no query", () => {
    it("show error", () => {
      expect(search().run({ query: "", error: ""}).error).toBe("Please input.");
    });
  });

  describe("with query", () => {
    it("emits query", () => {const result = search().run({ query: "test", error: ""});
      expect(result.$emit).toBeCalledWith("search","test");
      expect(result.query).toBe("");
    });
  });
});

上記のようにsearchメソッドに対するテストを書くことができます。 lifecycle hooksやcomputedのテストに関しては以下をご覧ください。

https://github.com/pipopotamasu/vue-function-tester#lifecycle-hookshttps://github.com/pipopotamasu/vue-function-tester#computed

vue-function-testerを作った背景

ただなんとなく作りたかっただけです。深い理由はありません。

(上記だとブログの長さ的に寂しくなりすぎるので、理由を無理やり捻り出しました。)

vue-function-testerを使わなくてもVueコンポーネントのメソッドのテストを書くことは可能です。 例えば...

  • vue-test-utilsを使用する
  • メソッド内の処理を別の関数として切り出してそちらをテストする
  • VueオブジェクトもしくはVueコンストラクタからメソッドの参照を取り出しテストする

ざっと考えついただけ3つの方法がありますが、どれも良い面もあればそうでない面もあり、個人的にときめくものではありませんでした。

vue-test-utils

皆さんご存知の通り、既存のVue.jsのテストライブラリとして「vue-test-utils」という有名なライブラリがあります。
とても便利なライブラリで私ももちろん愛用させてもらっています。

しかしこれは「Vueコンポーネント」のテストにフォーカスしたライブラリです。 常日頃開発している身としては、「Vueコンポーネントのメソッド」にフォーカスしてテストを書きたい時にちょくちょく出くわします。
その場合、vue-test-utilsだとテストコードが冗長になってしまうことがあります。

  • テストの度に毎回mountが必要
  • mount時に初期設定が必要な場合がある
    • テストしたいメソッドとは関係のないpropsを用意する
    • created hooks用の設定
    • etc...

等々、Vueコンポーネントのメソッドテストをときめく感じに書きたいという欲を満たすことができませんでした。

VueオブジェクトもしくはVueコンストラクタからメソッドの参照を取り出しテストする

上記のサンプルコードを例にすると以下のような感じになります。

# SearchBox.spec.js
import SearchBox from "@/components/SearchBox.vue";

describe("search()", () => {const{ search } = (SearchBox as any).options.methods;

  describe("no query", () => {
    it("show error", () => {const context = {
        query: "",
        error: ""}
      search.call(context);
      expect(context.error).toBe("Please input.");
    });
  });

  describe("with query", () => {
    it("emits query", () => {const context = {
        query: "test",
        error: "",
        $emit: jest.fn()
      }
      search.call(context);
      expect(context.$emit).toBeCalledWith("search", "test");
      expect(context.query).toBe("");
    });
  });
});

テスト自体は普通に書けるのですが、若干冗長さを感じてしまいます。え、私だけ...?特に上記のコードで冗長と感じるのは以下の2点です。

  • thisのcontextを別変数で宣言->メソッド実行->context変数の検証と、3ステップ踏まないといけない。
  • $emitのモック作成。methodsやcomputedのsetterのテストを書いているとそこそこの頻度で$emitが出てくるのに毎回モックするのが面倒。

Vueコンポーネントのメソッドにフォーカスするという部分では問題ないのですが、上記の点でときめかない点が残りました。

メソッド内の処理を別の関数として切り出してそちらをテストする

そもそも別関数として切り出すというやり方です。
関数のインターフェースをcontextベース(this)ではなく、引数・返り値ベースでinput/outputを実装すればとてもテストしやすい関数になります。
(※ contextベースだと結局、contextを別変数で宣言->メソッド実行->context変数の検証と、3ステップ踏まないといけない)

exportfunction sum(lfs, rhs) {return lfs + rhs;
}exportdefault Vue.extend({
  data() {return{
      lfs: 1,
      rhs: 2,
      result: 0
    };
  },
  methods: {
    sum() {this.result = sum(this.lfs, this.rhs);
    }}});

簡単すぎる例ですが、上記のsum関数なら容易にテストできます。
特にロジックが長くなってくると、Vueコンポーネントのメソッドとして定義しておくとコンポーネント全体の視認性が悪化するので上記のような分割はとても良い手段です。

ただテストのために全てのロジックを外部の関数に切り出すのはどうでしょうか? そもそも切り出すのがめんどくさいですし、切り出した関数のテストが正でも対象のVueコンポーネントのメソッドが正常に動くとは限りません。

こちらの手段も若干ときめかない部分が残りました。

vue-function-testerのときめくポイント

ではどうすればときめくのか、それはコードを短く書くことではないかと自問自答し、以下のコードを短くするときめきポイントを実装しました。

ときめきポイントその1: メソッドチェーン

以下のようにメソッドチェーンで繋げることで、ワンライナーで検証できるようにしました。

import{ methods } from "vue-function-tester";
const{ search } = methods(SearchBox);

expect(search().run({ query: "", error: ""}).error).toBe("Please input.");

ときめきポイントその2: alias

さらにaliasを貼り文字数を削減。

import{ methods } from "vue-function-tester";
const{ search } = methods(SearchBox);

// 通常
expect(search().run({ query: "", error: ""}).error).toBe("Please input.");
// aliasその1
expect(search().r({ query: "", error: ""}).error).toBe("Please input.");
// aliasその2
expect(search.run({ query: "", error: ""}).error).toBe("Please input.");
// aliasその3
expect(search.r({ query: "", error: ""}).error).toBe("Please input.");

※メソッドに引数を与えなければならない時はaliasその2とその3は使えません

ときめきポイントその3: $emit

使用頻度の高い$emitを事前にモック化

import{ methods } from "vue-function-tester";
const{ search } = methods(SearchBox);

expect(search.r({ query: "test", error: ""}).$emit).toBeCalledWith("search","test");

終わりに

このようにメドピアの開発合宿では自作のライブラリを作ったり、他のOSSにPRを送ったり、使ったことのない技術を使って何かしらのサービスを作ってみたり等々、参加者それぞれが自由に課題を設定しコードを書きます(年2回、水木金の2泊3日)。

f:id:ec0156hx39:20191122141942j:plain

今回で14回目の開発合宿となりましたが、まだまだ今後も続けていくので開発合宿をしたくなったら是非メドピアへ!


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


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

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

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

■開発環境はこちら

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

メドピア16期目初の開発合宿@熱海を開催しました!

$
0
0

みなさんこんにちは!

10月からメドピアにジョインしました、サーバサイドエンジニアの福本です。

メドピアでは日常業務から離れ、業務改善や技術研鑽のための開発合宿を定期的に開催しております(ちなみに、前回の開発合宿の様子はこちら↓)。

tech.medpeer.co.jp

私は今回が初めてのメドピア開発合宿だったのですが、すごく楽しくて有意義な時間だったので、合宿の様子をみなさんにもお届けできればと思います。

開発合宿のプログラム

今回の開発合宿は、11月20日(水)~22日(金)の2泊3日で行われました!

開発するテーマは自由なのですが、当日に内容に悩んで時間を無駄にしないように、事前に内容をエンジニアのメンバーに事前に決めてきてもらいます(前もって準備をしてきてもOK)。

また、せっかく時間を掛けて取り組んだ内容なので、開発合宿でのアウトプットを各自が発表する場を設けているのですが、合宿中に発表まで行ってしまうと開発に使う時間が短くなったり、資料に掛けられる時間が少なくなってしまいます。

そのため、成果発表は合宿が終わった次の週にオフィスで開催することにし、合宿中は思う存分開発に集中できるようにしました。

発表の様子もこちらの記事でお伝えしますので、ぜひ最後までお付き合い頂けると幸いです!

1日目

f:id:ec0156hx39:20191121182751j:plain

待ちに待った開発合宿は、お昼過ぎからの開始!熱海駅に1名を除きエンジニアメンバー全員で集合し、会場まで向かいました。

ちなみに、今回予約した宿泊所はこちらです↓

www.airbnb.jp

Airbnbで予約したのですが、地下1階から5階まで広く自由に使えますし、屋上で足湯に入りながら開発できたり、地下にはゲームやカラオケがあるなど、みんなで楽しみながら(後述)開発できるような環境になっています。館内のWi-Fiも速く、チーム開発も問題なく進められるのでオススメです。

f:id:terryyy:20191124180829p:plain

まずは会場で全員集合して、各メンバーが取り組む開発テーマを改めて発表し、その後は各自で自由に開発を行います。足湯に入ってリラックスながら開発をするメンバー、机と椅子でバッチリ集中して開発をするメンバーと、色々な開発スタイルが見られて面白かったですね。

f:id:terryyy:20191124182909j:plain f:id:terryyy:20191124183007j:plain

夜は熱海...ということもあり、休憩がてら近くのスパや温泉を楽しみに行くメンバーも。ちなみに、夜ご飯は参加メンバー全員で近くのお店に食べに行きました。周辺に飲食店が充実しているというのも、熱海のいいところ。

f:id:terryyy:20191124183848j:plain f:id:terryyy:20191124183936j:plain

部屋に戻ってからは、各自で開発に戻りました。

2日目

2日目は移動がないので、朝起きてから夜遅くまで、ひたすら開発に集中できる貴重な一日でした(そのためブログに書くことも少ない....)!

とはいえ気分転換も大事。朝食がてら朝日を拝みに行ったり、

f:id:terryyy:20191124184939j:plain

お昼ごはんは、みんなで海鮮を食べに行ったりしました。

f:id:terryyy:20191124185530j:plain

そんな2日目ですが、なんとRailsに送ったPRをマージされたメンバーがおりました...(すごい)。合宿で取り組んでいるメインのテーマではなかったようですが、目に見える形でわかりやすく成果が出るのは非常に嬉しいですね!

f:id:terryyy:20191124185901p:plain

画像にもURLありますが、マージされたPRは以下。よろしければご覧ください。

github.com

3日目

楽しい開発合宿も最後ですが、合宿所の予約日時の都合上、3日目は場所を近くのコワーキングスペースに移して開発を進めました。場所は同じく熱海にありますnaedocoさん。

naedoco.jp

naedocoさんは2016年にできたばかりのコワーキングスペースで、今までの開発場所とはまた違った雰囲気の中、リフレッシュして開発をすすめることができました。

f:id:terryyy:20191124190852j:plain

プログラム上は15時に解散、その後は各自自由に帰宅・開発という流れだったのですが、集中するあまりコワーキングスペースの開場時間ギリギリまで開発をするメンバーも居ました。

そんなこんなで、3日間の開発合宿は終了。各メンバーがどこまで進捗したのかはお互いあまり知らない状態だったので、成果発表がすごく楽しみです。

成果発表LT

厳密には合宿...ではないのですが、土日のお休みを挟み、開発合宿の成果発表会を社内LTのような形で実施いたしました!

f:id:terryyy:20191125235251j:plain

合宿に参加できなかったエンジニアはもちろんですが、参加したエンジニアも合宿中には自分たちの開発内容に没頭していたこともあり、開発合宿の進捗や得た学びを改めて確認する場になりました。

開発テーマの内容は本当に様々で、例えば人工知能系のチャットボットについてがっつり調査を進めてくれたメンバーも居れば...

f:id:terryyy:20191125235715j:plain

心拍数を測り、PCのカメラから撮影した動画の顔に計測結果を写す」という、日常のWeb開発の業務ではなかなか触れることのない技術を使いこなしてアウトプットをしたメンバーも。

f:id:terryyy:20191126000114j:plain

参加したエンジニア全員が発表したところで、開発合宿は完全に終了!

改めて振り返ってみると、単純に楽しかったというのはもちろんですが、参加したエンジニア全員がしっかりと開発テーマを進捗させて、アウトプットできる形まで持っていっている、という部分が印象に残る合宿でした。

初めて触れる技術をテーマにしていたエンジニアも多い中で、適度に楽しみながらも、集中すべきところはしっかりと集中してやり切る姿に、メドピアのエンジニアの底力を見たように思います。

さいごに

f:id:terryyy:20191124193411j:plain

最後までお付き合い頂き、ありがとうございました!

メドピアという会社が、事業も開発チームも大きくなっていく中で、多くのエンジニアメンバーが3日間も集中できる時間を頂けるということは決して当たり前のことではなく、会社全体の理解と協力があってのことで、非常にありがたいことだと改めて感じました。

今後も開発合宿という文化を継続して開催していけるよう、事業に技術でしっかり貢献していきます。

また、今回の合宿で培われた技術によってメドピアのサービスが改善されたり、あるいは新しい事業やサービスを産みだされることで、より多くの医師を支援し患者を救うことに繋がればと考えております。10月から16期目を迎えたメドピアを、みなさまどうぞ応援よろしくお願いいたします!

 


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


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

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

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

 

■開発環境はこちら

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

Tailwind CSSという風と共に走るフロントエンド開発

$
0
0

10km40分切りが2020年の目標、メドピア長距離部の小宮山です。

みなさんTailwind CSSはご存知でしょうか。tailwindとは「追い風」を意味します。最高に気持ちよく走れるコンディションですね。

目次

サービス概要

まずは今回新たに立ち上げたサービスの紹介です。 「MedPeerスポット×リクルートメディカルキャリア」という医師向けスポット求人マッチングサービス(以下、本サービス)が11月にリリースされました。

medpeer.co.jp

ログインや応募などサービスのコアな部分はMedPeer医師会員限定になってしまいますが、サイトの雰囲気自体は非会員でも十分に味わえますので是非ともサイトを開いてみてください。医師向けスポット求人という普段なかなか見ることができない世界を覗くこともできます。

技術概要

本サービスの技術的な構成をざっと紹介していきたいと思います。

まずはMedPeerといえば(?)なRuby on Rails(以下、Rails)です。最新ほやほやの6.0です。自分はフロントエンド畑な人間でそれほどRailsに精通しているわけでもなくそのすごさをちゃんとは理解していませんが、きっとすごいことなんだと思います。

以前のMedPeerなら、「Railsを使っています、以上です。」で終わってしまっていたところですが、なんと今回はもうひとつ目玉技術があります。Nuxt.js(以下、Nuxt)です。

ja.nuxtjs.org

実はNuxtの利用自体はMedPeerでは初めてではありませんでした。静的サイトのジェネレータとしてシンプルなLPを作成した実績はすでにあります。

かかりつけ薬局化支援サービス「kakari」のLPがまさにそれにあたります。

kakari.medpeer.jp

ではなぜ再度Nuxtを強調し直すのか。なんと今回は、Nuxtを本番運用してSSR(サーバーサイドレンダリング)するというMedPeer初の挑戦だったのです。さらにバックエンド(Rails)とフロントエンド(Nuxt)をリポジトリもAWSリソースも分離してしまうというMedPeer初だらけの野心的な技術構成となっていました。

そして非常に申し訳ないのですが、今回伝えたいのはNuxtのことでもフロントエンド分離のことでもありません。開発プロセスやVue.jsの話題ですらありません。それ以上にTailwind CSSのことを伝えたいモチベーションが高すぎました。新大陸の大地そのものよりも、そこに吹いていた追い風にこの心を掴まれてしまったのです。

Tailwind CSSとは何か

tailwindとはずばり「追い風」を意味します。今日の走りは絶好調だと思ったら折返しで現実に引き戻されるやつですね。

公式サイトの文言をここに引用します。

A utility-first CSS framework for rapidly building custom designs.

Tailwind CSS is a highly customizable, low-level CSS framework that gives you all of the building blocks you need to build bespoke designs without any annoying opinionated styles you have to fight to override.

一応CSSフレームワークという分類になりそうですが、BootstrapVuetifyといった所謂一般的なCSSフレームワークとはやや毛色が異なります。

一般的なCSSフレームワークの多くが「コンポーネント」を提供してくれるのに対して、Tailwind CSSが提供してくれるのはスタイルを便利に指定するためのツール類だけです。

例えばダイアログを作りたいと思ったとき、Vuetifyならコンポーネントとして既に用意されていますが、Tailwind CSSでは用意されたツールを組み合わせてダイアログというコンポーネントを自力で作らなければいけません。Tailwind CSSが提供してくれるのは自動車本体ではなく、その名前が意味する通り、自力で走る者たちへの追い風です。

HTMLとCSS

Webページを構成する3要素といえば、HTML・CSS・JavaScriptです。HTMLはWebページの構造を、CSSはレイアウトや装飾を、JavaScriptは何かをしてくれます。
(今回はJavaScriptについてはあまり掘り下げません。)

ja.wikipedia.org

文書の構造と体裁を分離させるという理念を実現する為に提唱されたスタイルシートの、具体的な仕様の一つ。

令和を生きる私はCSSが登場したころの状況を実際には知らないのですが、どうやらCSSはHTMLからスタイルを分離させることを目的に生み出されたもののようです。そして実際に、文章構造とスタイルやHTMLとCSSそれぞれに切り出して役割分担させるのがWebの作法だという認識が広く広まっていたと思います。

HTML

<ulclass="content-list"><liclass="content-item">item 1</li><liclass="content-item">item 2</li></ul>

CSS

.content-list{display: flex; }.content-item{padding: 1rem; }

なんの変哲もない例を出すとこんな感じでしょうか。HTML側ではタグにclassを付け、CSS側ではそのclassをセレクタとしてスタイルを付与しています。

一方でこんなHTMLはどうでしょうか。

HTML

<ulstyle="display: flex;"><listyle="padding: 1rem;">item 1</li><listyle="padding: 1rem;">item 2</li></ul>

スタイルをCSSに分離することなく、タグにそのまま書き込んでいます。所謂インラインスタイルです。基本的に、インラインスタイルが歓迎されることはほとんどないと思われます。実際に私もこのコードだけを見たらレビューコメントを入れる手を止めることはできないでしょう。

しかしなぜ歓迎されないのでしょうか?実のところ「そういうもの」で終わらせてしまうことが多く、あまり深く考えたことはありませんでした。実際にCSSは誕生目的からして「そういうもの」なのですが、改めてその問題点をざっと考えてみました。

  • HTMLが冗長になる
    class指定が不要とはいえ、styleで書き込むので文字数の絶対量は増えてしまいます。
  • スタイルの共通化ができない
    タグ毎にstyleを書き込むしか無いので、リストアイテムなどには大量に同じstyle属性を書き込まないといけません。HTML冗長化をさらに後押しします。
  • CSS関連のエコシステム(Sass、PostCSS、Stylelintなど)が使えない
    最近のフロントエンド開発ではこれが相当のデメリットではないでしょうか。
    (もしかしたらインラインスタイルにもいい感じに変換やlintを当ててくれる仕組みがあるかもしれません。)
  • レスポンシブ対応が難しい
    若干思いつきベースですが、インラインスタイルでレスポンシブなスタイルって作れるんでしょうか?やるなら下記のようになりそうですが、試そうと思ったこともなさすぎて効くのかどうかは未検証です。
<ulstyle="@media (min-width:480px) { display: flex; }"> ... </ul>

HTMLとCSSとコンポーネント

時代は変わりつつあります。既に変わってしまっているのかもしれません。その変化の主役となる存在がコンポーネントです。HTMLとCSSは文章構造とスタイルという役割で分離するのが当たり前だったのに、コンポーネントという新たな区切がより重視されるようになってきています。

従来のWebページ構成イメージ
従来のWebページ構成

コンポーネント指向なWebページ構成その1のイメージ
コンポーネント指向なWebページ構成その1

コンポーネント指向がもたらす大きなメリットは、コンポーネントという明確な区切りが生まれることです。今までのHTML・CSS・JavaScriptという3層では、互いに分離されてはいるものの、その層内はほぼグローバル空間と化していました。良く言えば自由で手軽に作れてすぐに変更可能、悪く言えば分かりにくく壊れやすくて変化に弱い。

コンポーネントという区切りは、この3層のより上位の区切りとしてWebページのグローバル空間を分割します。そしてさらにコンポーネント内でHTML・CSS・JavaScriptの3層が存在するようなイメージです。

双方ともにメリットとデメリットは様々にありますが、今のフロントエンド界隈はコンポーネント指向が勢い付いていると見て間違いないでしょう。

シンプルに考えると、Webページがコンポーネントによってまず区切られ、コンポーネント内部では従来の3層区切りがそのまま再現されているだけです。確かにコンポーネントが注目されだした当初はそうだったかもしれません。

しかし変化はそこで留まってはくれませんでした。コンポーネントという閉じた安全な世界が作られたことで、その内部ではかつて常識と考えられていた壁の存在感が薄れだしました。

コンポーネント指向なWebページ構成その2のイメージ
コンポーネント指向なWebページ構成その2

コンポーネントとして完結していて、外部との連携で求められるインタフェースが満たされてさえいれば、コンポーネント内部の実装が多少暴れん坊になっていても大した問題にはなりません。いや問題ではあるんですが、コンポーネント内部はいわばprivateな部分なので大した問題ではないことがほとんどです。コンポーネントという区切りが明確に存在しているので、どうしようもなくなったら丸ごと取り替えてしまうことだって可能です。

かくしてコンポーネントの躍進により、HTMLとCSSの境界は薄れだしました。もちろん完全になくなったわけではありませんが、CSSが生まれた目的そのものであったHTMLとスタイルの分離は、コンポーネントというより大きな関心事にかき消されつつあります。

コンポーネントと雑なセレクタ

コンポーネントという概念によってHTMLとCSSの間にあった従来の区切りは曖昧になり、両者の距離はぐっと近づいています。区分けされた両者を繋ぐために必須だったclassを軽視する、こんな手抜きスタイル指定も横行しているのではないでしょうか。

HTML

<ulclass="content-list"><li>item 1</li><li>item 2</li></ul>

CSS

.content-list{display: flex; }.content-list>li{padding: 1rem; }

階層を指定しているとはいえ、セレクタとしてliを直で指定しています。従来のHTML・CSS分離の方針に従うなら真っ先にレビュー指摘が入りそうなコードです。実際に私もこんなコードを素の状況で見つけたらおそらく指摘を入れます。ただしこのコードがコンポーネントに関するものであれば話は別で、CSSにHTMLの構造が漏れ出していてもそれほど気にしません。

理由は2つあります。1つはスタイルがコンポーネント内に閉じているからです。liという豪快なセレクタ指定がされていようと、そのセレクタがコンポーネント内のHTMLに限定されているのなら、コンポーネント外の予期せぬ箇所でスタイルが崩れることはありません。
(セレクタのパフォーマンス観点については今回は立ち入りません。)

もう1つは、classを丁寧に付けるほうがむしろコストが高くなるかもしれないからです。classを増やせば当然HTMLの文字数は増えて汚れます。適切なclass名を文書構造と翻訳サイトを往復しながら考えなくてはなりません。HTMLとCSS間の視点移動だって必要です。つまり面倒くさいのです。単発では小さな手間かもしれませんが、それを数百数千回と繰り返すのはとても面倒くさいのです。人生は短いんです。早く家に帰って走りたいんです。

コンポーネントとインラインスタイル

コンポーネントという箱庭の存在によって、雑なセレクタが許されるようになってきたとしましょう。エンジニアというのはどこまでも怠惰を求める生き物らしいです。当然、一度雑になったセレクタ指定はどこまでも雑になっていきます。雑になればなるほどHTMLとCSSは近づいていきます。そして行き着く先に何があるかというと、インラインスタイルの再登場です。

HTML

<ulstyle="display: flex;"><listyle="padding: 1rem;">item 1</li><listyle="padding: 1rem;">item 2</li></ul>

コンポーネント指向という前提に立ち、改めて先に挙げたインラインスタイルのデメリットを再評価してみます。

  • HTMLが冗長になる
    基本的には従来と同様だと思います。
  • スタイルの共通化ができない
    コンポーネント単位での共通化という大きな道が開けます。利用するフレームワーク依存となりますが、v-forjsxなどで効率的に記述する手段も多くあります。
  • CSS関連のエコシステム(Sass、PostCSS、Stylelintなど)が使えない
    基本的には従来と同様だと思います。
  • レスポンシブ対応が難しい
    コンポーネントごと表示を切り替えるなどの手段が生まれてきそうですが、ほぼ従来と同様と思われます。

ただしHTMLの冗長さやCSSエコシステムについては、コンポーネント指向な開発の現場でよく使われるstyled-componentsやCSS in JSなどのJavaScript側からのアプローチによって解決は可能かもしれません。
(普段Vue.jsのscoped CSSばかり使っているのでこのあたりの動向には疎いです。)

場面によってはインラインスタイルで済ませてしまうことも十分に可能ですが、大勢をそれだけでなんとかするのはやはり厳しい印象です。特にCSS系のエコシステムとの相性と、レスポンシブ対応のやりにくさは致命的です。インラインでベンダープレフィックスを付けていく作業以上の苦痛はなかなか見つからないと思います。人生は短いし早く帰って走りたいんです。

Tailwind CSSがもたらしたもの

やっとTailwind CSSの話題にたどり着きました。先に挙げたインラインスタイルの例をTailwind CSSで書くとこうなります。

HTML

<ulclass="flex"><liclass="p-4">item 1</li><liclass="p-4">item 2</li></ul>

ご理解いただけたでしょうか。ここまで長々と紹介してきたコンポーネントとスタイル指定に関する内容そのものが、Tailwind CSSの良さへと至る道筋です。コンポーネント化の波がもたらしたスタイル指定の怠惰の極みにあるインラインスタイル、良さもあるが辛さも多量に含んだその地点に到達する一歩手前に我々を留めてくれる存在、それこそがTailwind CSSです。

見ての通り、Tailwind CSSはスタイルとほぼ1対1に対応したclassを提供してくれます(一部複合的なスタイル付きclassがあったり、独自で拡張することも可能です)。つまり実質class方式でスタイルを指定しているだけなので、autoprefixerpurgecssなどとの連携が容易です。

ブレークポイント用のプレフィックス(sm:, md:, lg:)が用意されているので、レスポンシブ対応に苦慮する必要もありません。

例えばPCサイズ以上でだけ横並びにしたい場合はこう書けます。インラインスタイルの良さを残しつつ悪さを緩和する、絶妙な具合ではないでしょうか。

HTML

<ulclass="flex flex-col md:flex-row"><liclass="p-4">item 1</li><liclass="p-4">item 2</li></ul>

HTMLが冗長になるという点に関しては、インラインスタイルよりは改善されているものの、class形式でスタイルを当てていくときほどには軽量にならないです。スタイルとclassがほぼ1対1になっているので、この点は受け入れるしかないというのが現時点の評価です。

まとめ

Tailwind CSSは決して強力なCSSフレームワークではありません。フレームワークと呼んで良いのか怪しいくらいに具体的なものは何も提供してくれません。公式サイトに掲げられている通り、utility-firstなツールを提供してくれるだけです。ツールをどう使うか、何を作るかは完全に使い手に委ねられています。

そこに心地よさを感じかどうか個々人で評価が分かれるかもしれません。しかしながら本サービスの立ち上げからリリースまでTailwind CSSを使い続けた私の感想としては、「次もまた使いたい」し「発展に貢献したい」です。

その理由を端的に表現するのはとても難しいです。なぜなら具体的にこれが良いと言い切れるようなものではなく、この記事で述べてきたように、Webフロントエンドの潮流の中で味わう様々な要因が組み合わさって到達した良さだからです。しかしその良さを実感しているのは事実です。

今回はTailwind CSSの使い方自体はほとんど紹介できませんでしたが、最近は利用事例も増えて情報も多く出ているので探すのに困ることはないと思います。需要があればMedPeerでのTailwind CSS利用事例として改めて記事化するかもしれません。

Tailwind CSSの巻き起こす追い風、みなさんも是非味わってみてください。


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


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

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

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

■開発環境はこちら

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

CircleCIのYAMLを短く書けるRails Orbを作りました

$
0
0

11月に入社したCTO室SREの@sinsokuです。

主にやっていることはRailsアプリのレビューや開発環境の改善です。*1

  • 社内のRailsアプリを横断して浅くレビューする(8つくらい)
  • MedPeerの開発環境の改善
    • docker-compose upで30個のコンテナが起動するのを減らす
  • SwitchPointからActiveRecord v6への移行
  • CircleCIの実行時間の短縮、稀に落ちるテストの修正
  • その他の細かい改善

このうち、CircleCIについて知見が溜まったので技術ブログで紹介します。

CircleCIで気をつける点

CircleCIの実行時間を短くするにはいくつかコツがあります。

  • gemとnpmをできるだけキャッシュする
  • RSpecを並列で実行する前に assets:precompileを実行しておく
  • 各ジョブで必要なgem(もしくはnpm)だけをキャッシュから復元する
    • 例: JavaScriptのテストはnpmのキャッシュだけ復元する

ワークフローを図にするとこんな感じです。

f:id:sinsoku:20200207111113p:plain
CircleCIのジョブのワークフロー

キャッシュの仕組みについて

詳しく知りたい人はCircleCIのページを読んで頂くとして、ここでは一番重要な 部分キャッシュについて説明します。

CircleCIのキャッシュキーには複数のキーを指定することができます。

- restore_cache:keys: # 「OSとCPUの種類」「ブランチ名」「Gemfile.lock の checksum」でキャッシュを探す- gem-cache-v1-{{ arch }}-{{ .Branch }}-{{ checksum "Gemfile.lock" }}
    # 上のキーで見つからない場合、「OSとCPUの種類」「ブランチ名」でキャッシュを探す- gem-cache-v1-{{ arch }}-{{ .Branch }}
    # それでも見つからない場合、 `gem-cache-v1` で始まる最新のキャッシュを探す- gem-cache-v1

キャッシュキーを複数指定すると上から順番にキャッシュを探します。
これによりGemfile.lockが変わっても、 bundle-installで全てのgemをインストールしないで済むようになります。

キャッシュの肥大化

部分キャッシュを使っているとgemが増え続け、キャッシュのリストアに時間がかかる問題が起きます。
bundle install --cleanすれば良いのですが、少しずつ遅くなるため気づき辛い問題です。

参考: bundle install には --clean を指定する (特に Circle CI では) | Born Too Late

ちなみにYarnは自動的に不要なnpmを消してくれます。🐈

Rails Orb

上記の点を気にしながら、社内のRailsアプリで横断的に対応するのは大変なので、誰でも良い感じに設定できるOrbを作りました。

github.com

実際に社内のいくつかのRailsプロジェクトに導入しています。

使い方

Orbの提供するジョブやコマンドの詳細はOrb registryを参考にしてください。

また、Rails Orbを使う前にCircleCIの設定画面で uncertified orbsを許可する必要があります。

f:id:sinsoku:20200212184310p:plain

gemやnpmのキャッシュについて

Gitリポジトリのデフォルトブランチをキャッシュキーに含めることでキャッシュヒット率を上げています。

- restore_cache:keys:- << parameters.key >>-{{ arch }}-{{ .Branch }}-{{ checksum "Gemfile.lock" }}
    - << parameters.key >>-{{ arch }}-{{ .Branch }}
    - << parameters.key >>-{{ arch }}-{{ checksum ".git/refs/remotes/origin/HEAD" }}
    - << parameters.key >>-{{ arch }}

一般的なジョブを提供

CircleCIの設定を簡単にできるように、以下4つのジョブを提供しています。

  • rb-deps: bundle installを実行する
  • js-deps: yarn installを実行する
  • assets: assets:precompileを実行する
  • rspec: RSpecでテストを並列に実行する

新規案件で rails newした直後なら以下の設定で良い感じにCircleCIが動きます。

version:2.1orbs:rails: medpeer/rails@x.y.z

executors:ruby:docker:- image:&docker_ruby circleci/ruby:2.7.0-node-browsers
  ruby_with_db:docker:- image:*docker_ruby- image: circleci/postgres:12.1-alpine-ram
    environment:DATABASE_URL:'postgres://postgres:postgres@127.0.0.1:5432'workflows:rspec:jobs:- rails/rb-deps:executor: ruby
      - rails/js-deps:executor: ruby
      - rails/assets:executor: ruby
          requires:- rails/rb-deps
            - rails/js-deps
      - rails/rspec:executor: ruby_with_db
          db-port:'5432'parallelism:4requires:- rails/assets

ジョブとコマンドを組み合わせる

Orbの提供するrb-depsジョブとbundle-installコマンドを組み合わせると、RuboCopを実行するジョブなども短く書けます。

version:2.1orbs:rails: medpeer/rails@x.y.z

executors:ruby:docker:- image: circleci/ruby:2.7.0-node-browsers

jobs:rubocop:executor: ruby
    steps:- checkout

      # Restore gems from cache- rails/bundle-install:restore_only:true- run: bundle exec rubocop --parallel

workflows:rspec:jobs:- rails/rb-deps:executor: ruby
      - rubocop:requires:- rails/rb-deps

まとめ

Rails Orbを使うと .circleci/config.ymlの記述をかなり減らせるかなと思います。

各社でCircleCIのYAML職人をしている方、ぜひRails Orbを試してみてください。


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


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

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

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

■開発環境はこちら

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

*1:SRE所属だけど、あまりSREっぽい仕事をしていない

Nuxt利用プロダクトでIE11と仲良くするためのE2E

$
0
0

フロントエンドなエンジニアの皆さま、ご機嫌いかがでしょうか。

唐突な質問ですが、Internet Explorer 11というブラウザはお好きでしょうか。勿論大好きであられるかと存じ上げます。Webの歴史をまさにその身をもって築き上げてきた由緒正しきブラウザであります。唯一無二の王道です。昨今は様々なブラウザが溢れてあそばせております。しかし所詮それは一時的なこと。やがて全人類は母なるInternet Explorer 11の元へと還っていくことでしょう。

我々が目指したこと

Internet Explorer 11(以下、IE11)を目にすること、操作すること、その他あらゆる接点を限りなく減らしつつ、プロダクトがIE11でも動作可能なことの検証と保証を行いたい。

これを成し遂げるエンジニアリング的な手段、つまりIE11環境でのE2Eテストを自動化することを目指します。

環境

自動化を行うにあたり、何をするにもまずは環境を用意しなければなりません。IE11が動く環境です。

そして早速ですがこの環境こそがIE11環境自動テスト一番の問題と言っても過言ではないでしょう。なぜならIE11が動くということは、当たり前ですがOSとしてWindowsが動いていなければなりません。Linuxディストリビューションではだめなのです。

1年前ならここで既に諦めざるを得なかったかもしれません。しかし今は既に2020年です。Windows環境に対応したCI環境は十分手に届く範囲に登場してきています。

メドピア開発部が最もお世話になっているテスト環境はCircleCIです。そしてなんと、CircleCIでもWindows環境に対応を始めているではありませんか。

circleci.com

既にWindows対応のテスト環境は目の前にあったわけです。しかし不運なことに、この事実はこの記事を執筆中に発見しました。執筆中ということは、当然ながら当初の目的は既に果たしているわけです。今ネタバレをしましたが、つまり今回採用したのは慣れ親しんだCircleCIではありません。

IE11の全てを委ねるべく採用した環境は、Github Actionsです。
なぜGithub Actionsか、とてもいい質問です。「Githubで完結する」、「GithubがMicrosoft傘下になっていた」、「社内ではまだ導入事例も少なく目新しさがあった」、そこには色々な選ぶべき理由があります。

そしてこういう理由は後付けです。最初に目に入ってしまったからです。IE11でサービスが動いていなかった障害に傷心しながら「IE11 E2E」と適当なワードで検索したら偉大な先駆者達の功績が目に入ってしまったわけです。

qiita.com

moneyforward.com

今まで不可能だと諦め続けていたことが、「できる・・できるんだ・・っ!」という確信に変わった瞬間です。あとはもう勢いでやり切らざるを得ません。そして勢いのままに開発フローに導入し、勢いの落とし前としてこうして外部発信をして既成事実としていくわけです。

プロダクト概要

昨年11月にリリースした、「MedPeerスポット×リクルートメディカルキャリア」という医師向けスポット求人マッチングサービスです。 spot-rmc.medpeer.jp

プレスリリースはこちら。 medpeer.co.jp

求人マッチングサービスという特性上、求人情報の検索、応募フォームの入力、応募リクエストの送信あたりが動作してくれないと非常に困るクリティカルな機能となってきます。

プロダクト構成

今回ターゲットとしたプロダクトは、APIを提供するRuby on Railsによるバックエンドと、フロントエンドをSSR対応で配信するNuxt.jsという2枚看板な構成です(以下、Rails、Nuxt)。

Tailwind CSSの話題に終始した前回記事と同じプロダクトなので、お時間あればこちらもご覧ください。 tech.medpeer.co.jp

フロントエンドとバックエンドの橋渡し役となるAPIは、OpenAPIに乗っ取ってスキーマ定義を行い、openapi-generatortypescript-axiosを利用して型付きのクライアントSDK化しています。

f:id:robokomy:20200221144059p:plain
3者連携

このSDKは独立したリポジトリになっていて、スキーマ更新をpushするとSDKも最新化してくれるようCIを組んでいます。
あとはフロントエンドリポジトリ側でSDKのインストールやアップデートをしていけば、型で(ある程度は)守られたSDKを通してAPIとの連携が可能です。

問題

Railsが強めなメドピア開発環境からすると、フロントエンドが完全にRailsから分離された今回のプロジェクトはなかなかに攻めた構成でした。フロントエンド分離主義者の方々もきっと満足してくださるでしょう。

かくいう私も満足していた1人だったのですが、E2Eテストという観点からするとこの構成は大きな問題を抱えていました。
Rails主導な構成であればcapybaraを利用したFeature specが大体いい感じにしてくれるみたいです。実際メドピアではこれが大活躍していて、入社当初はFeature specによるテストの充実具合に驚いた思い出があります。

しかし困ったことに、今回のフロントエンド環境はRailsの支配下にありません。こういう状況で、E2Eテストをどう行うべきかという知見が社内に全くない状態でした。

このようなマイクロサービス気味構成におけるE2Eテストのベストプラクティスを一緒に考え、議論し、実現していってくれるエンジニアをメドピア開発部では絶賛募集中です。

今、私にできること

全てがうまくいくトータルオールインワンストップE2Eソリューションの実現は残念ながら成りませんでしたが、ある程度の妥協を許せばこんな私にもできることは残されています。

今回の目的に立ち戻ります。それは、「IE11での動作検証を自動化する」ことです。IE11固有の動作検証をしたいわけです。例えばIE11固有の仕様により、ajax通信用APIを提供しているだけのRails側機能が動作しないということはあるのでしょうか?

落ち着いてください、お気持ちは分かります。確かに無いとは言い切れないかもしれません。IE11の全てを疑ってかかりたい人生を送られてきた皆様のお気持ちは十分に分かりますが、それでもときには信じることだって大事なんです。

はい、信じました。これでまずはRailsが動作検証の対象から外れました。

f:id:robokomy:20200221153357p:plain
関心範囲

Railsが関心範囲から外れてしまえばあとはもうフロントエンド原理主義者が好きにやるだけです。先ほどAPIとの連携部分は型で守られたSDK化していると紹介しました。つまりは型さえ守っていればモックに差し替えることは容易いわけです。

f:id:robokomy:20200221153739p:plain
最小の関心範囲

当初は登場人物が3人も居て途方に暮れていましたが、IE11で動作検証することを目的とするならば、Nuxtだけを検証対象にすればよいという状況に漕ぎ着けました。

Github Actions

Github Actions用ymlファイルの紹介です。
既に先人達が詳細に紹介してくれていて、特に大きな差分もないです。異なるのは、NuxtによるSSR環境を再現するために、テストランナー用のスクリプトを別途用意している点です。

gist.github.com

続いてそのテストランナー用のスクリプトの中身です。
テスト対象となるNuxtサーバーの起動と、テスト本体を実行してくれるTestcafeの起動を担当しています。
Nuxtを裏で起動したままTestcafe(後述)を起動するという制御が必要だったので、非同期な処理が書きやすいnodeスクリプトを利用しています。

Github Actions職人であればNuxtの裏起動をもっとスマートな方法で実現できるのかもしれません。メドピアではGithub Actions職人もきっと募集しています。

gist.github.com

なにやら怪しげなコメントがいくつか挿入されています。いずれも遭遇したエラーとそれを乗り越えた歴史です。

windows環境でspawnが使えない

spawnというのは非同期で外部コマンド実行してくれる関数です。
IE11を動かすGithub Actionsは当然ながらwindows環境です。普段windows環境でnodeスクリプトを実行する習慣がないので油断していました。spawncmd経由でコマンドを実行する必要があるようです。

stackoverflow.com

@nuxtjs/pwaモジュールが吐き出すエラーでジョブが落ちる

ERROR  (node:5136) DeprecationWarning: Tapable.plugin is deprecated. Use new API on .hooks instead

こういうやつです。その他環境ではエラーを吐いても特に問題なくNuxtのビルドは成功判定となってくれるのですが、なぜかGithub Actionsのジョブとして実行すると容赦なくエラー扱いで落ちてしまいました。

@nuxtjs/pwaモジュールを最新化すればエラーは消えるらしいものの、まだβバージョンということで躊躇し、nodeスクリプト内でGithub Actionsさんに気づかれないよう実行することで回避しました。

クロスブラウザでテストする場合だと毎回ビルドし直しになって効率悪いですが、今回の対象はIE11だけなのでここも割り切っていきます。

windows環境でnpmスクリプトに環境変数を渡せない

もう1件windows由来問題です。どうやらwindowsではNODE_ENV=production yarn buildのような環境変数の指定はできないらしいです。やはり普段触らない環境は知らないことだらけですね。
cross-envというパッケージを使うとさくっと指定できるようなのでnpxでさくっと使わせていただきます。

stackoverflow.com

Testcafe

環境が出揃ったところで今更ですがテストランナーの紹介です。今回採用したのはtestcafeです。

採用理由は、IE11でさくっと動いたからです。第一目的はIE11における動作検証であって、プロダクトの仕様や機能検証といった大層なものではありません。さくっと仕組みを作って多大な労力を削減する。できそうなことが分かったのですからそのまま勢いで組み上げます。

テストコード抜粋はこんな感じです。できる限りE2E専用のidclassは入れないほうがメンテは楽な気がします。文言変更で壊れたらそのときは諦めて都度直しましょう。

gist.github.com

なにやらresizeWindowという怪しげな関数があります。これはTestcafeに限った問題ではなくIE11利用のE2E全般の問題らしいですが、IE11では画面外要素はなぜか存在しない判定をされてしまうらしいです。

全盛期の彼ならその理由を躍起になって深掘っていったかもしれません。そんな彼もIE11に打ちのめされ尽くした今となっては、目の前の現実をただ受け入れるだけのマシーンです。そういうものです、目の前で起きていることは紛れもない現実なのです。

ただし開発環境のfirefoxやchromeなどで実行するときはサイズが大きすぎると怒られるので、申し訳程度に分岐を入れておきます。

成し遂げたこと

f:id:robokomy:20200221165608p:plain
動くことの証明

かくして、IE11で我々のプロダクトが動作することの検証は自動化に成功しました。E2Eのテストカバレッジは相当少なく、おそらく10%にも届いていないです。しかしながら、最も重要なトップ画面から応募完了までのルートをIE11で通過できることが自動テストで保証されている安心感は凄まじいです。

dependabotでnpmパッケージアップデートのPRが飛んできても、それがbabel関連パッケージだったとしても、自動テストが元気よく動いてIE11環境の動作検証を行ってくれています。

結局スタイルチェックで実機IE11チェックは必須なんですがこの記事が皆様のIE11ライフに少しでも貢献できましたら幸いでした。


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


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

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

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

■開発環境はこちら

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

Viewing all 210 articles
Browse latest View live