Medley Developer Blog

株式会社メドレーのエンジニア・デザイナーによるブログです

runitが便利なので、使い方を紹介した話〜メドレーTechLunch〜

メドレー開発本部のnakataniです。

開発本部で定期的に開催している勉強会「TechLunch」で、runitというunixのプロセススーパバイザについてお話しました。 その内容について紹介させていただきます。

runit自体は特に目新しい技術ではなく(Linuxbusyboxに収められていたりする枯れた技術です)、大して難しい話題でもありません。

ただ、個人的には便利に使っている手放せないツールであり、もしスーパバイザというものの存在を知らずに使わずにいる人がいると勿体無いなあという思いから、TechLunchのテーマとして取り上げた次第です。

runitとはなんなのか

プロセスをデーモンとして立ち上げて、プロセスが死んでも再度起動し続けてくれるツール郡です。C言語で開発されています。

Linuxなどのunixではたいてい標準でinit, Upstart, Systemd, launchdなどのスーパバイザが組み込まれています。 runitはそれらと同じような位置づけのものです。

qmailの作者であるdjbが作ったdaemontoolsの後継のプロダクトです。

runitがあると何が便利なのか

■マシンが起動しているかぎり、プロセスを動作させ続けることができる

マシンを立ち上げたあとに、起動コマンドを叩いたり、プロセスが落ちたときに再起動をする必要がありません。 また、フォアグラウンドで動作するプロセスを起動した後に、端末を切り離す操作をする必要もありません。

ただ、これは他のスーパバイザでも同じことが実現できます。

■その場しのぎで作ったスクリプトを、ほぼそのままデーモン化できる

Shell, Ruby, Perl, Haskell どのような言語で作ったスクリプトであっても、 シェルなどで実行可能なファイルがあれば、それをそのままデーモンとして実行することができます

スクリプトの標準出力をそのままログファイルとして扱うことができる

svlogdというプログラムが、デーモンの標準出力をログ化し、ローテーションなどの面倒も見てくれます。 自作のデーモンが思った通りに動かない際のデバッグが容易です。

macOSへの導入方法

macOSに導入するための手順を記載します。詳しくはスライドやrunitのドキュメントなどを理解して使うようにしてください。

XcodeやHomebrewをmacOSに導入していることが前提です。

/serviceをrootのrunitディレクトリとして設定する手順

$ brew install runit # macports feels better.
$ sudo mkdir /service
$ sudo cat <<EOF |sudo tee /Library/LaunchDaemons/runit.plist
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd" >
<plist version='1.0'>
  <dict>
    <key>Label</key><string>runit</string>
    <key>ProgramArguments</key>
    <array>
      <string>sh</string><string>-c</string>
      <string>PATH="/usr/local/sbin:/usr/local/bin:$PATH"
    exec '/usr/local/bin/runsvdir' '/service'
      </string>
      <string>;</string>
      <string>--pid=exec</string>
    </array>
    <key>Debug</key><false/><key>Disabled</key><true/><key>KeepAlive</key><true/>
  </dict>
</plist>
EOF
$ launchctl load -w >/Library/LaunchDaemons/runit.plist
$ launchctl list | grep runit

自作スクリプトをデーモンにする手順

$ cd /service/
$ sudo mkdir -p hello/log # logを同時につくるとrunsvdirがlogの準備もする
$ cd hello
$ sudo cat <<EOF |sudo tee run
#!/usr/bin/env ruby
# 自作スクリプト
while true
  puts("hello ruby #{Time.now.to_i % 100}");
  STDOUT.flush();
  sleep(1);
end
EOF
$ sudo cat <<EOF |sudo tee log/run
#!/bin/sh
exec svlogd -ttt .
EOF
$ sudo chmod 755 run log/run
$ tail -F log/current # ログが見られる。
$ sudo sv st . # daemonの状態を確認する。
run: .: (pid 35517) 46s; run: log: (pid 34727) 456s
$ sudo sv st /service/hello/ # ディレクトリの指定方法は自由。
$ sudo sv t . # TERMシグナルをdaemonに送る
$ sudo sv st .
run: .: (pid 35589) 1s; run: log: (pid 34727) 470s # 起動時間が1sになってる。

まとめ

結局のところ、「使えばわかるし使わないと便利さがよくわからない」というのが正直なところです。 そのため、TechLunchにおいては、使うための手順を時間をかけて解説をするようにしました。

speakerdeck.com

みなさんも興味があれば、ぜひ導入して使ってみてください。

僕自身、開発マシンであるmacbook proにrunitを入れて、開発環境のRubyやMongoDB, Elasticsearch, Nginxなどのサーバ群、 定期的に動かしたいちょっとしたスクリプトなどをrunitで管理しています。

異なる設定のサーバ群を一つのマシンに同居させる場合も、設定ファイルを分けて別ポートで立ち上げたりしています。

以前のプロジェクトでは本番環境をrunitで構築したこともありますし、今のプロジェクトでも、たまったゴミデータを削除し続けるスクリプトをrunitで対応してそのまま放置(放置してもOKなくらいメンテナンスフリー)していたりします。

最近はクラウドやコンテナ技術が活況であり、環境を抽象化しようという流れがあります。しかしながら、そもそもプロセスやUNIX OS自体が環境を抽象化するための技術群です。そういった基本的な技術と仲良くすることで、物事がシンプルになることがあるのではないかと考えたりしながら、日々開発に取り組んでいます。

お知らせ

メドレーでは、医療介護の求人サイト「ジョブメドレー」、医師たちがつくるオンライン医療事典「MEDLEY」、口コミで探せる介護施設の検索サイト「介護のほんね」、オンライン診療アプリ「CLINICS」などのプロダクトを提供しています。これらのサービスの拡大を受けて、その成長を支えるエンジニア・デザイナーを募集しています。

www.medley.jp

メドレーで一緒に医療体験を変えるプロダクト作りに関わりたい方のご連絡お待ちしております。

RubyKaigi 2017にメドレーがRuby Sponsorとして参加しました

こんにちは!開発本部のエンジニア・新居です。
メドレーは9/18〜20に開催された、RubyKaigi 2017Ruby Sponsorを務めさせていただきました。(9/15〜17に開催されたiOSDC JAPAN 2017に引き続きの協賛です!メドレーでは今年からテックカンファレンスへの協賛を積極的に行っています)

f:id:medley_inc:20170928115311j:plain

イベント当日は、弊社からCTOの平山、エンジニアの平木と宍戸、そして新居の4人が参加しました。今回はそのときの様子などをレポートしたいと思います。 

会場の様子

今年は広島県広島国際会議場での開催でした。
広島国際会議場平和記念公園の敷地内にあり、近くには原爆ドーム広島城などもあるのでRubyKaigiのついでに観光しちゃおうなんて人も多かったのではないでしょうか。 f:id:dev-medley:20170922103927j:plain
 
f:id:dev-medley:20170922103952j:plain
 
会場の一角には今回のRubyKaigi参加者がどこから来たかを掲示するボードが設置されていました。 f:id:dev-medley:20170922104014j:plain
日本以外からもたくさんの人が参加しておりRubyKaigiの注目度の高さが伺えます。
 
そして隣にあったスポンサーボードのMEDLEYロゴをしっかり確認して、CTO平山とパシャリ!!! 
f:id:dev-medley:20170922104049j:plain
 

ブースの様子

続いてブースの様子を!
ブースの仕上がりはこんな感じで、メドレーコーポレートカラーの赤が目立って良い感じでした!

f:id:medley_inc:20170928115453j:plain

両サイドにはメドレーの事業概要と、メディアなどからも注目されることが多いオンライン診療アプリ「CLINICS」を紹介するポスターを配置、写真中央のモニターでは、スマートフォンやPCで医師の診療が受けられるというCLINICSのサービス概要がわかる紹介動画を流し、会社全体やプロダクトについて説明できる準備を整えました。
 
f:id:dev-medley:20170922104208j:plain
ノベルティは「MEDLEYステッカー」「MEDLEYうちわ」「MEDLEY絆創膏」の3種類を用意しました。
 
1日目は台風一過で気温が上昇し、暑さをしのぐためMEDLEYうちわが大活躍してくれました!
MEDLEY絆創膏はデザインのかわいさと医療系ならではということもありとても好評でした!参加者の中には靴擦れしている人や小さい擦り傷を負ってる人もいて、MEDLEY絆創膏が参加者のお役に立てたのではないでしょうか。
 
f:id:dev-medley:20170922104242j:plain
ブース前で意気込む3人!
 
ちなみにTシャツはメドレー特製のバリューTシャツ(左)とカレッジロゴTシャツ(中央と右)を着用しています。
写真では見えませんが、バリューTシャツにはメドレーのバリュー(行動規範)のひとつである「中央突破」の文字が入っています。
両Tシャツ共、目を留めてくれた参加者からはかわいいという声もいただけて、こちらも好評でした。

 

f:id:medley_inc:20170928115637j:plain

午後の15:20~15:50のAfternoon Breakでは大勢の参加者がブースゾーンに。
 
実際にブースに足を運んでいただいた参加者とお話していると、メドレーやCLINICSのことを知らない人がほとんどでしたが、メドレーが医療系のサービスを提供している会社であることや、CLINICSというオンライン診療アプリを開発していることをお話すると興味を持っていただけることが多かったように思います。
「今度病院で見かけたら使ってみます!」という声をいただいたり、参加者自身の医療体験を元にCLINICSというサービスについて意見を交わしたりと、メドレーエンジニアと参加者が密にコミュニケーションをとれる良い機会になりました。

f:id:medley_inc:20170928115734j:plain

大盛況です!
 

CTO平山の発表

そして3日目の最後のKeynote前にはRubyスポンサーのPR枠があり、弊社のCTO平山が発表しました。

f:id:medley_inc:20170928120004j:plain

 
発表では、「医療ヘルスケア分野の課題を解決する」というミッションのもとメドレーが4つの事業を行っていること、その中のひとつであるCLINICSのプロダクトについて、「医療 x ITへの挑戦」にむけて医療従事者とエンジニア・デザイナーが協力してプロダクト開発を行う体制などを紹介しました。
f:id:dev-medley:20170922104626j:plain
  
最終日3日目の最後ということもありお疲れの方も多かったと思いますが、会場を笑いで沸かすシーンもあり、メドレーと、そのプロダクトのことを少しでも多くの人に知っていただくとても良い機会になったのではないでしょうか。
  
当日の発表資料はこちらからどうぞ。

speakerdeck.com

 

周辺散策の様子(おまけ)

最後におまけということで周辺散策の様子を!

f:id:dev-medley:20170922120050j:plain

まずは原爆ドーム
 

f:id:medley_inc:20170928120937j:plain

続いて広島城跡へ。御門橋。 

 

f:id:dev-medley:20170922120112j:plain

歩みを進める3人。
 

f:id:medley_inc:20170928120718j:plain

そして天守閣。原爆で倒壊したためコンクリート建築として復元されたようです。
 
f:id:dev-medley:20170922115844j:plain
近くには広島護国神社
 

f:id:dev-medley:20170922115739j:plain

メドレー恒例の?参拝!
 

f:id:dev-medley:20170922115919j:plain

美味しい広島のお好み焼きもいただきました!
  

さいごに

今回はメドレーとして初のRubyKaigi参加でした。
ブース運営などへの不安もありましたが、実際に当日を迎えてみると、参加者のみなさんとお話でき、メドレーのことを少しでも多くの人に認知していただくとても良い機会だったと思います。
 
まだまだメドレーのことをご存知ない方も多かったですが、実際にお会いして話してみると、医療というリアル産業をインターネットの力で変えていくという面白さに興味を持っていただけたことが多かったなという印象を受けました。
良いプロダクトを作ることも大切ですが、医療分野でのプロダクト作りの醍醐味や産み出せる価値などを多くのエンジニア・デザイナーに知ってもらえるよう、こうしたテックカンファレンスの場などで発信し続けていくことも大切だなあと改めて実感しました。
 
弊社で「医療 x ITへの挑戦」に取り組みたいエンジニアのみなさんを心からお待ちしております!興味がある方は、こちらの「話を聞いてみたい」からご連絡ください。
開発本部の雰囲気をもっと知りたい方は、こちらからどうぞ。

iOSDC Japan 2017にメドレーが協賛しました

みなさん、こんにちは。開発本部のエンジニア・平木です。

表題の通りなんですが、メドレーは今年からテックカンファレンスの協賛を本格的に開始しています。その内の1つが9/15〜17に早稲田大学で開かれた iOSDC Japan 2017(以下iOSDC)です。

みなさんご存知かと思いますが、iOSDCは国内のiOSイベントの中ではtry! Swiftと並ぶ最大級のイベントです。

メドレーは今回、ゴールドスポンサーとして協力させていただきました。ブース出展はしていないのですが、CLINICSのiOS版をメインで担当している高井と一緒に参加してきました。

f:id:medley_inc:20170925173341p:plain

会場の雰囲気

f:id:medley_inc:20170927113807p:plain

まず第一印象として人が多かったです。

会場は4つに分かれていたのですが、一番大きいトラックA以外はセッションが始まるころになると満席になっていることが多く、自分が見たセッションでは大体立ち見が出ている感じでした。

企業ブースを出展している企業さんの中では、サイバーエージェントさんのブースがiOS開発に関するアンケートを取っていたようで、盛況でした。

f:id:medley_inc:20170925174226p:plain

スタッフさんの人数も多かったということもあるのか、スケジュール通りにセッションが進んでいるのが印象的でした。機材トラブルなどもほとんど 見受けられなかったので、運営がとてもスムーズでした。

また、朝にオープニングムービーが流れてスポンサーの紹介などをしている映像が流れるのですが、まさかの三石琴乃さんがナレーションでビビりました。 すげえ。

youtu.be

iOSDCではランチが配られる形式なのですが、来場者が多かったはずなのに、食べる場所がない…みたいなこともなく良かったですね。

ランチはサンドイッチで、大変おいしかったです。席が近いからか知らない人とも話しやすい雰囲気を感じました。

f:id:medley_inc:20170927113648p:plain

セッション

一口にiOS関連といってもセッションはかなりバラエティに富んだ内容です。Swiftに関するもの、設計に関するもの、運用に関わるものなどなど…。

f:id:medley_inc:20170925174414p:plain

ちょいちょいとAndroidやKotlinと関係したセッションがあったのは意外でした。が、考えてみると両方をやってるエンジニアの方も多いでしょうし、 SwiftとKotlinも文法なんかは割と似てるということで、親和性があるんでしょうね。

全体としては、やはり、iPhone XやiOS11の話題がちらほらと出ており、さすが専門のカンファレンスだけあってわりとつっこんだ話が聞けたのが良かったです。

f:id:medley_inc:20170927113424p:plain

個人的には言語の話なども面白かったんですが、運用周りの話をしてるセッションがとても面白かったです。

リニューアル周りの話題やCIでの運用の話などは大変に参考になりましたし、特にトレタさんの ロギングの話はぜひ弊社でも実践せねばという話でした。

LTの時間前にビールが配られて、飲みながら参加できるのは大変ポイントが高いですね!

f:id:medley_inc:20170927113925p:plain

最後に

iOSDCは自分は初めての参加だったのですが、大変よいイベントだなと感じました。

f:id:medley_inc:20170925180142p:plain

iOSだと特に大体毎年新しいバージョンや新しい機種が出てきますし、 そうなると新しいSDKAPIもどんどん出てくるという感じです。もちろんネットの情報などで知識をアップデートしたりするのも必要ですが、こういったイベントでより新鮮な知見などを共有できるというのは、やはり素晴しいなあという感想です。

弊社のアプリ開発でもそういった知見などを活かして開発していける仲間を引き続き募集しています! 興味がある方は、こちらの「話を聞いてみたい」からご連絡ください。 www.wantedly.com

開発本部の雰囲気を知りたい方はこちらからどうぞ。 www.medley.jp

PaaSをHerokuからAWS Elastic Beanstalkに移行した話

こんにちは、開発本部の宮内です。

さて、これまで弊社のオンライン診療アプリ「CLINICS」では、ローンチ時よりHerokuを利用しておりました。 Herokuとは、PaaSの一種でWebアプリケーションを簡単にデプロイ、ホスティングできるサービスです。 ある程度の制約はつきますが、(大体の制約は金で解決できるので)使っているかたも多いのではないでしょうか。

今回、事情によりHerokuからAWS Elastic Beanstalk(以下、EB)へ移行することになりましたので、そのあたりでやったことを共有できればと思います。

Private PaaSにしないの?

まずはじめに移行にあたり、Priavte PaaSを構築する方法を模索しました。 ですが、これらのクラスタの構築はできても、専任の(Ops|SRE|インフラ)チームがいないため、日々の管理や運用に手が回らないだろうという思いから、Private PaaSの構築は見送りました。 この辺りは今後チーム人数が増えたら挑戦していきたいです…。

検討時、参考にしたリンク

なぜAWS Elastic Beanstalkにしたの?

EBにはRailsSinatraで作成されたウェブアプリケーションを実行するためのRubyプラットフォームが予め用意されております。 ただし、今回の移行では、アプリケーションへの変更を一切加えずに行いたかったため、Rubyプラットフォームを利用しませんでした。 代わりにDockerコンテナが実行できるプラットフォームがあったため、そちらを使うことにしました。

herokuishでDockerイメージを作成

アプリケーションのDockerイメージ化には、gliderlabs/herokuishを使いました。 これはbuildpackを使いアプリケーションをslug化したり、slugを実行するためのツールです。

Dockerイメージ作成の手順は以下の通りです。 1. herokuish buildpack buildherokuish slug generate でアプリケーションをslugにする 2. herokuish slug importでslugをインポートして完成

それでは、それぞれ簡単に説明していきたいと思います。

アプリケーションをslugにする

docker pull gliderlabs/herokuish
docker run \
  -v "/path/to/app:/tmp/build" \
  -v "/path/to/cache:/tmp/cache" \
  -v "/path/to/slug:/tmp/slug" \
  gliderlabs/herokuish \
  /bin/bash -c 'herokuish buildpack build && herokuish slug generate && herokuish slug export > /tmp/slug/slug.tgz'

herokuishは特定のディレクトリに対して処理を行います。↑ではビルドするアプリケーションまでのディレクトリパスを/tmp/buildマッピングしています。

/tmp/cacheはbuildpackが利用するキャッシュ置き場です。このディレクトリを次回以降のビルドでもマッピングしておくとビルドの高速化が見込めます。

最後の/tmp/slugはビルドしたslugをコンテナからホストへコピーするために指定しています。(これはherokuishで用意されてるものではなくコンテナからホストへファイルをコピーする方法を悩んだ末のアドホックな対応です…)

他にも様々なディレクトリがあります。詳しくはドキュメントをご覧ください。

slugをインポートする

次に作成したslugを使いアプリケーションのDockerイメージをつくります。

cd $(mktemp -d)
mv /path/to/slug/slug.tgz .
echo '
FROM gliderlabs/herokuish

COPY slug.tgz /tmp/slug.tgz
RUN cat /tmp/slug.tgz | herokuish slug import && rm /tmp/slug.tgz

EXPOSE 5000
' > Dockerfile
docker build -f Dockerfile .

docker build時にContextとしてカレントディレクトリ全体が送られるため、一時ディレクトリを作成しその中でdocker buildを行っています。

終わり

CLINICSでは上記のような手段で作成したDockerイメージをAmazon EC2 Container RegistryにアップロードしEB上で実行しています。

本来であれば、アプリケーションをslugにする部分と、slugをインポートする部分を分割しなくても良いと思いますが、CircleCIでDockerイメージを作成する関係上でこのような方法になりました。

先日GAとなったCircleCI 2.0にはまだ対応できていないので、今後の課題としたいと思います。

お知らせ

メドレーでは、CLINICSだけでなく、医療介護の求人サイト「ジョブメドレー」、医師たちがつくるオンライン医療事典「MEDLEY」、口コミで探せる介護施設の検索サイト「介護のほんね」などのプロダクトも提供しています。これらのサービスの拡大を受けて、その成長を支えるエンジニア・デザイナーを募集しています。

www.medley.jp

メドレーで一緒に医療体験を変えるプロダクト作りに関わりたい方のご連絡お待ちしております。

思考とデザインスキル 〜メドレーTechLunch〜

はじめまして!最近みるみる太りだしてはいるものの、まだ機は熟していないとダイエットの時期をぐっと堪えている開発本部イケメン担当のデザイナー・小山です。

メドレーではTechLunchという社内勉強会を実施しているのですが、前田に引き続き私も発表する機会をいただきましたので、その内容を紹介させていただきます。テーマは「思考とデザインスキル」です。発表資料は記事の最後をご覧ください。 

 

テーマに入るまえに… 

みなさん『黄金比』ってご存知ですか? 

f:id:medley_inc:20170913121842p:plain

黄金比は美しいとされる物の形に共通してみられる比率で、古くから絵画や彫刻、建築などに使われています。デザイナーであれば、一度以上は使ったことあるのではないでしょうか?私もデザイナーなので何度も使っています。

ただ使ってはいるものの、なぜ美しくなるのか上手く説明ができません。当たり前のように世の中に広まっていて『困ったときの黄金比!』といった安易な思考で使っています。三十路を軽く超えたのでそれではいけないと思い、すこし調べてみました。

そもそも黄金比はエウクレイデスというユーグリッド幾何学を体系化した数学者が見つけた比率なのですが、そのとき彼は『黄金比=美しい』とは明言していないそうです。自然物や人工物の形には一定の比率で成り立っていると考え、その比率に黄金比と名付けだけとのこと。

その後、その使いやすさから至るところで活用され、その比率に親しみが芽生え巷で受け入れられるようになりました。

認知心理学では、それを『単純接触効果』と呼びます。たくさん触れるうちに親しみが沸く機能が人にはもともと備わっていて、黄金比をつかったものが美しく見えるのもその影響ではないか?と言われています。この説を聞いた時、基礎的なのに原理が曖昧なデザイナーのスキルの輪郭がすこしだけハッキリしてきました。

今回のTechLunchでは、曖昧だったデザインのスキルを人の思考や心理現象という視点から捉え直してみると、新しい発見があるのかも?と考え、『思考とデザインスキル』のテーマを選びました。 

『早い思考』と『遅い思考』

まず最初に人の思考がどういう構造になっているか整理したいと思います。

*思考の捉え方には幅があるため、今回はWikipediaで【思考】の狭義にあたる『情報処理』の内容になります。

 

ここで3つの質問を左から順に答えてください。

f:id:medley_inc:20170913122003p:plain

大抵の人であれば2問目までは瞬時に答えが出たと思います。3問目はどうでしょうか?

3問目は他よりも時間がかかったかと思います。情報を処理するときの思考には1問目と2問目のように瞬時に答えれるのは『早い思考』、3問目のように少し時間は必要な『遅い思考』の2つがあります。

行動経済学では、この早い思考を『システム1』、遅い思考を『システム2』と呼んでいます。 

システム1は、自動的に直感で動く

では次にこちらをご覧ください。

f:id:medley_inc:20170913122043p:plain

どちらの直線が長いでしょうか?答えはどちらも同じです。この問題をご存知の方でも見た瞬間はAが長く見えるのではないでしょうか?

ではこちらではどうでしょうか?

f:id:medley_inc:20170913122101p:plain

こちらの文字をみたときに、海水浴していて溺れている、もしくはそれに近いシーンを思い浮かべてないでしょうか? この二語のあいだには『りんご あまい』というような直接の相関はありません。それにもかかわらず出来れば避けたくなるようなことでも無意識に関連づけられ、シーンが思い浮かんだはずです。

システム1には、さきほどの直線の長さのように間違っていたとしても『見たまま』を認識する機能と2つの文字から『関連づけ』をおこないストーリーを組み立てる機能があります。そのどちらもが自分の意識とは関係なく自動で、しかも強力に働いています。

システム2は、手動で論理的に動く

 最初の二桁の掛け算を思い出してください。日本で算数を学んだ人であれば、二桁の掛け算のとき『考える』段階に移ると思います。これがシステム2のスイッチです。システム1は常時スイッチが入っていてほぼ自動で答えを出しますが、システム2は意識的に『考える』というステップを踏まないと動きません。システム1は自動ですが、システム2は手動です。

手動のため手間かかりますが、システム1にはない用心深さと慎重さがあります。こちらの問題をご覧ください。

バットとボールで110円、バットはボールより100円高い、ボールの値段は?

即答で答えた人は10円と答える方が多いようです。こういう問題にはシステム2がうってつけで、論理的かつ正確に答えを出そうとします。答えは5円です。なかには頭の回転が早く即答できる人がいらっしゃることだと思います。そんな方にはこちらの問題を答えていだだきましょう。

3日前から食べた夕飯の献立を口に出して発表しながら、『26x673』『245x287』『346x4546』の3問を90秒以内に解答してください。

いかがでしょうか?システム2は手動でうごき論理的で正確に答え出そうとしますが、複雑すぎる演算やマルチタスクにめっぽう弱くスタミナもありません。あきらめて電卓を叩いた方は、システム2がギブアップしたということかもしれません。

システム1と2の特徴は以下。どちらも良いところとそうでないところがあります。 

f:id:medley_inc:20170913122233p:plain

デザインを再構築

これらシステム1&2の特性を踏まえた上で簡単なニュース記事のタイポグラフィを整理してみました。 

発表資料の58ページからご覧ください。

speakerdeck.com

こと細かく情報をシステム1&2のフィルターを通すことで、タイトルやリード、本文の行間、文字サイズ、それらがシステム2が情報を理解しやすくするための1つ機能として捉えることができます。当たり前に使っていたスキルや機能を別の視点で捉えることで違った深い意味を見出せるようになり、大きな発見につながりました。

今回はタイポグラフィだったのでシステム2寄りのものでしたが、ビジュアルアイデンティティが強く抽象的なデザインは、意図的にシステム2を封じ込めシステム1の直感性を利用して内容を理解してもらうこともできます。

システム1&2の2つの特性を踏まえて使いこなすことで、スキルの深い理解はもちろんですが、感覚とは違うデザインの伝え方の新しいヒントにもなりそうです。

さいごに

YouTubeInstagram、LINEはどちらかというと直感性を促すシステム1が活躍するアプリです。それだけでなく身の回りにあるサービス全体がその傾向という印象をうけます。システム1は普段からスイッチが入っているため、比較的に簡易なステップで働きかけることができます。

一方でシステム2は複雑な演算には耐えれず、しっかりとケアしながらでないと十分な運用ができません。ただケアをしっかりすると恩恵も大きいといえます。

金融、教育、雇用、そして医療と、20年前とは比較にならないほどインターネットは人生の節目に深く関わるようになったからです。人生に関わる選択をネットで行うとき、直感だけでなくシステム2を働かせて熟考し納得のいく決断を行うことが、よりユーザーの利益につながると私は考えています。

メドレーで提供しているサービスはユーザーの人生に少なからず関わる性質をもっています。すぐに結論づけしてもらうより、ユーザーにとって正しいと思える判断ができるように、システム2をうまく働かせることができる環境をデザイナーとしてつくっていきたいと思います。

おまけ

 今回のテックランチで参考にさせていただいた書籍は以下のものになります。

 ファスト&スロー(上・下)』ダニエル・カーネマン

予想通りに不合理』ダン・アリエリ

錯覚の科学』クリストファー・チャブルス&ダニエル・シモンズ

UIデザインの心理学』ジェフ・ジョンソン

今回のお話は、本当に本当に表面の部分になります。流し読みでも参考になるので手に取って見てください。またこういうことに興味がわいたデザイナーさん・エンジニアさん、是非是非メドレーに遊びに来てください!(絶賛募集中です!)

お知らせ

メドレーでは、ジョブメドレーだけでなく、医師たちがつくるオンライン医療事典「MEDLEY」やオンライン診療アプリ「CLINICS」、口コミで探せる介護施設の検索サイト「介護のほんね」などのプロダクトも提供しています。これらのサービスの拡大を受けて、その成長を支えるエンジニア・デザイナーを募集しています。

www.medley.jp

今後ともメドレーを、よろしくお願いいたします!

クライアント認証とPath Based Routingが必要なサーバをAWSで構築(後編:App層)

今回の内容について

メドレー開発本部の田中です。 先日、Proxy層をElastic Beanstalk上のNginxで、App層をEC2インスタンスで構築する機会がありました。ここだけ見るととても普通に見えますが、制約があることで苦労した点もあり(前編参照)、制約を乗り越えるための工夫も含めてお話できる限り共有させていただきます。 前編ではProxy層の構成として、主にNginxを使用したPath Based Routing周りについてのお話でした。後編ではApp層で使用したEC2、 Systems Managerパラメータストアあたりについて共有いたします。

App層の構成

App層の方針や構築の流れ等をまとめると以下の通りです。

  • ゴールデンイメージとしてOS設定やサーバアプリケーションをインストールしたimage(AMI)を作成しておく
  • 上記のAMIを元に、クライアント毎にEC2インスタンスを作成する
  • EC2インスタンス起動時に、クライアントに応じたTagや環境変数をもとにサーバアプリケーションのセットアップを行う
  • 自身の内部IPとTagに設定したクライアント識別IDを元にRoute53のPrivateDNSに登録する

それでは、それぞれの詳細について説明していきたいと思います。

AMI作成

Packerを使用して各インスタンス共通となるAMIを作成します。provisionersで指定した構築用スクリプトでOS設定や必要ライブラリ、またメインとなるサーバアプリケーションをインストールします。また、cloud-initを使用して初回起動時に動かすスクリプト類もコピーしておきます。

なお、cloud-initから実行するスクリプトはGitやS3などから動的に取得する方法もありますが、さほどスクリプトの内容に変更は発生しない点と、内容的に変更ある場合はimage再作成がどちらにしても必要になりそうだったので割り切ってimage内に含めることにしています。

作成したpacker.jsonprovisioners部分を抜粋するとこのような感じになります(説明コメント部分は実際には記載していません)

  "provisioners": [
    -- type: shellとして、構築用スクリプト指定。ビルド時に実行される
    {
      "type": "shell",
      "scripts": [
        "scripts/provision.sh"
      ]
    },
    
    -- type: fileでインスタンス起動時に実行させるスクリプト群をコピー
    -- これらのスクリプトはcloud-initから実行される(cloud-initの設定は別途インスタンス作成時に行っている)
    {
      "type": "file",
      "source": "./scripts",
      "destination": "/home/hoge"
    },
    
    -- 上記のスクリプトに対して実行権限付与
    {
      "type": "shell",
      "inline": [
        "chmod +x /home/hoge/scripts/*"
      ]
    }
  ]

packer build でビルドしたimageがAWSに今回の共通で使用するAMIとして登録されます

EC2インスタンス作成

作成したAMIを元に、クライアントごとのインスタンスを作成します。なお、インスタンス作成は TerraformCloudFormationなどは使わず、AWS CLIを利用したスクリプトを作成して実行しています。

インスタンス作成スクリプトはこのような流れの処理となります。

インスタンス作成

以下のように、aws ec2 run-instances コマンドを使用し、Tagにクライアント識別IDを指定して作成しています。 ここで指定したクライアント識別IDを元にパラメータストアから自分用の環境変数を登録/取得したり、Private DNS用のドメインに使用します。

aws ec2 run-instances \
  --image-id ${AMI_ID} \
  --key-name ${KEY_NAME} 
  --region ${REGION} \
  --subnet-id ${SUBNET_ID} \
  --security-group-ids ${SECURITY_GROUP} \
  --user-data file://${USER_DATA} \
  --instance-type ${INSTANCE_TYPE} \
  --tag-specifications "ResourceType=instance,Tags=[{Key=ClientId,Value=${CLIENT_ID}}]" \
  --iam-instance-profile "Arn=${SERVICE_ROLE}"

user-data には初回起動時に実行したいスクリプト(Packerでビルド時にコピーしておいたスクリプト)を指定しているだけとなります。

#!/bin/bash
/home/hoge/scripts/bootstrap.sh

パラメータストアに環境変数登録

使用する環境変数は、Keyは共通ですが値がクライアントによって異なります。そのため、HOGEというKeyを使用する場合、<クライアント識別ID>.HOGE という形式でパラメータストアに登録しています。

(注. パラメータストアに階層やタグ付けがサポートされたらしく、このあたりの構成は今後見直す予定です)

登録は aws ssm put-parameter を実行します

aws ssm put-parameter \
  --name ${KEY} \
  --value ${VALUE}  \
  --type ${PARAMETER_TYPE} \  # String、SecureStringなど
  --overwrite

これでクライアントごとのEC2インスタンスが作成、起動されます。次にインスタンス起動時の流れについてです。

EC2インスタンス起動

起動時は、初回起動と毎回起動でそれぞれ以下のような処理を行います。

  • 初回: パラメータストアから自身に関連する環境変数を取得し、サーバアプリケーションのセットアップ
  • 毎回: 自身の内部IPをRoute53のPrivate DNSに登録/更新

内部IPは固定しておらず起動時に割り振られるため、毎回更新するようにしています。

それではそれぞれの内容について見ていきます。

パラメータストアから環境変数取得

登録時の内容で記載しましたが、環境変数<クライアント識別ID>.HOGEという形式で登録しています。そのため、まずは自身のクライアント識別IDを判定した後に必要な環境変数aws ssm get-parametersで取得します。

# 自身のインスタンスIDをメタデータから取得
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)


# クライアント識別IDをインスタンス作成時に指定したTagから取得
# (describe-instancesのfilterに自身のインスタンスIDを指定)
CLIENT_ID_TAG=$(aws ec2 describe-instances \
  --region=${REGION} \
  --filters "Name=instance-id,Values=${INSTANCE_ID}" \
  | jq -r '.Reservations[].Instances[].Tags[] | select(.Key == "ClientId").Value'
)


# 環境変数を取得
# タイプをSecureStringにしている変数もあるため、一律 --with-decryption オプションを指定している
HOGE=$(aws ssm get-parameters \
  --name "${CLIENT_ID_TAG}.HOGE" \
  --with-decryption --region ${REGION} \
  | jq -r ".Parameters[].Value")


export HOGE=${HOGE}  

内部IPをPrivate DNSに登録

最後に、Proxy層からPrivate DNSで名前解決できるように自身のIPをRoute 53に登録してやります。

なお、Route53には事前に対象のHosted Zoneを Private Hosted Zone for Amazon VPCタイプとして登録しておきます。ここでは例としてDomain Nameをlocalとします。

EC2インスタンスから登録されるRecordSetは以下の形式とします。

  • Name: <クライアント識別ID>.local
  • Type: CNAME
  • Value: EC2インスタンスの内部IP

これらを行うスクリプト例は以下となります。

# 内部IPを取得
# (describe-instancesのfilterに自身のインスタンスIDを指定)
PRIVATE_IP=$(aws ec2 describe-instances \
  --region=${REGION} \
  --filters "Name=instance-id,Values=${INSTANCE_ID}" \
  | jq -r '.Reservations[].Instances[].PrivateIpAddress'
)


# Route53の登録先Hosted Zone IDを取得
# SEARCH_KEYは今回の例でいうと 'local.' になります
HOSTED_ZONE_ID=$(aws route53 list-hosted-zones \
  --region=${REGION} \
  | jq -r ".HostedZones[] | select(.Name == \"${SEARCH_KEY}\").Id"
)


# この後の登録コマンドで指定するための定義ファイル
# 毎起動時の登録用(IPが変わるため)に、Actionには 'UPSERT' を指定
RECORDSET_FILE="/tmp/create_recordset.json"
cat <<EOT > ${RECORDSET_FILE}
{
  "Changes": [
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "<クライアント識別ID>.local",
        "Type": "CNAME",
        "TTL": 300,
        "ResourceRecords": [
          {
            "Value": "${PRIVATE_IP}"
          }
        ]
      }
    }
  ]
}
EOT


# 作成した定義ファイルを指定し、Route53に登録
aws route53 change-resource-record-sets  \
  --hosted-zone-id ${HOSTED_ZONE_ID} \
  --change-batch file:///${RECORDSET_FILE}

実行するステップはやや多いですが、このような構成をとることでVPC内ではドメイン指定でのアクセスが可能となるため、IPを意識する必要がなくなるため柔軟な構成になるかと思います。

今回のまとめ

いまさらインスタンス立てるとかめんどくさいなぁ、、、とか思いながら色々調べて構築しましたが、EC2まわりのサービスも増えてるんだなぁ、なんて感じました(特にパラメータストアはとても便利)

パラメータストア以外にもSystems ManagerにはRun CommandやPatch ManagerなどEC2インスタンスを管理する上でとても便利な仕組みが揃っていますのでこのあたりも導入していきたいと思います。

余談ですが、Systems Managerの存在はre:Invent 2016で発表された時から名前だけは知ってましたが、今回の対応するまでずっとオンプレ専用のサービスだと勘違いしてて記憶から消えかけていました。。。

最後に

前編をProxy層(Nginx)、後編をApp層(EC2)について書かせていただきましたがいかかだったでしょうか。 そもそもの要件自体がけっこう特殊だったりもするので、なんでこんな構成に?みたいなとこもあるかも知れませんが、どなたかの参考になれば幸いです。もう少し聞いてみたい、というかたはwantedlyの「話を聞いてみたい」ボタンからどうぞ。

※前編をあらためて読みたい方はこちらからどうぞ developer.medley.jp

お知らせ

メドレーでは、医師たちがつくるオンライン医療事典「MEDLEY」、オンライン診療アプリ「CLINICS」、医療介護の求人サイト「ジョブメドレー」、口コミで探せる介護施設の検索サイト「介護のほんね」などのプロダクトを提供しています。これらのサービスの拡大を受けて、その成長を支えるエンジニア・デザイナーを募集しています。

メドレーで一緒に医療体験を変えるプロダクト作りに関わりたい方のご連絡お待ちしております。

www.medley.jp

クライアント認証とPath Based Routingが必要なサーバをAWSで構築(前編:Proxy層)

今回の内容について

メドレー開発本部の田中です。 先日、Proxy層をElastic Beanstalk上のNginxで、App層をEC2インスタンスで構築する機会がありました。ここだけ見るととても普通に見えますが、後述する制約から苦労した点もあり、制約を乗り越えるための工夫も含めてお話できる限り共有させていただきます(先にお伝えしておくと、特殊な事情がなければ今回のようなケースではALBで対応するECSサービスにPath Based Routingしてやるのが良いと思います)。

技術要素として、Nginx(OpenResty)/ Route53 Private DNS / EC2 / Systems Manager パラメータストア あたりに触れたいと思います。(BeanstalkはMulticontainer Dockerを使用し、それも慣れるまでちょっとクセあったなぁと思ったのですが分量が多くなりそうなのでまた別の機会に共有させて頂きます)

まず前編としてProxy層、主にNginxを使用したDynamic Path Based Routingについてお話して、後編はApp層について、EC2とSystems Manager パラメータストアあたりについて共有させていただければと思います。

設計/構築する上での前提と方針

対象となる案件を進める上での要件・制限内容は諸事情あり、ざっとまとめるとこのような感じです。

  • 環境はAWSを使用する
  • サーバアプリケーション、クライアントアプリケーションはユーザ毎で、サーバアプリケーションは共用できない(ユーザが増える度にクライアント/サーバのセットが増えるイメージ)
  • ただし、クライアントからの接続先となるEndpointは同じだが、Host Based Routingは訳あって利用できない
  • クライアント認証を使用する

上記から、以下の設計方針で進める事にしました。

  • Proxy層でクライアント認証を行い、Path Based Routingで対象となるサーバにリクエストをproxyする。Path部分にクライアント別の識別IDを含め、その値を元にPrivate DNSで名前解決する
    • 例) https://example.com/a-client/api => http://a-client.local/api
    • App層は個別EC2インスタンスとする

設計する上で悩んだ点

主に2点ありますが、まずはProxy層です。出来るだけAWSのマネージド・サービスで済ませたかったので、クライアント認証とPath Based Routingが可能でやりたい事に合うかどうか調べましたが以下の理由で断念し、普通(?)にELB + Nginxを利用することにしました。

  • ALB: クライアント認証に非対応。またSSL終端となるのでNginx側でクライアント認証が出来ない
  • API GW: クライアント認証は対応しておりRouting部分もがんばればいけるかも?、と思ったがProxy先が動的に増えたリするので管理ふくめ難しそうであった

次にApp層の構成をどうするかでした。集積度を高めるためにコンテナ利用も検討したのですが、使用するアプリケーションの必要スペックや要件などからいまいちフィットせず、個別のEC2インスタンスにすることにしました(今でももっと良い方法がないか悩んでたりします)

全体構成

出来上がった全体構成のイメージは以下となります。なお台数は実環境と異なり、今回の内容と関係ない部分などは省略しています。

f:id:medley_inc:20170823180401p:plain

次に、今回の本題となるProxy層の構成について触れたいと思います。

Proxy層の構成

Proxy層の方針等はまとめると以下の通りで、proxy先の動的判定と名前解決する箇所がキモとなります。

  • App層のインスタンスは、起動時に自身の内部IPとTagに設定したクライアント識別IDを元にRoute53のPrivateDNSに登録する
    • クライアント識別IDがa-clientの場合、a-client.localのように登録
  • Proxy層のNginxはクライアント認証を行い、リクエストパスから取り出したクライアント識別IDを元に転送先Endpointを生成し、backendにproxyする
    • App層のインスタンスは動的に増えるため、リクエスト時に名前解決したい(インスタンスが増える度に自動でNginxのconfを編集することも検討したが追加数が読めず、confがふくれあがるのもなぁ、、、という思いがあり止めました)

Nginxはbackendが増えても起動しっぱなしで動的に名前解決して動作させたかったため、lua-nginx-moduleを導入し balancer_by_lua ディレクティブと lua-resty-dns モジュールを使用することとし、構築の手間の関係からOpenRestyを導入することにしました。

lua-nginx-moduleを使用したconfファイル

confファイル全体としては以下となります(関係ない箇所は省いています)。ポイントと記載した部分についての説明は後述します。

http {
    upstream app {
        # ポイント1. 
        # Private DNSで設定したIP(CNAMEに設定)を元に動的Routing
        balancer_by_lua_block {
            local balancer = require "ngx.balancer"


            local host = ngx.ctx.upstream_server.cname
            local port = '8888'


            local ok, err = balancer.set_current_peer(host, port)
            if not ok then
                return ngx.exit(500)
            end
        }
    }


    server {
        listen 443 ssl;


        set $proxy_upstream_host '';
        set $proxy_upstream_domain '.local';


        location ^~ /api/ {
           rewrite_by_lua_block {


               -- pathからクライアント識別IDを取得し、Private DNSに設定したドメインを生成
               -- https://example.com/<id>/api という形式のリクエストから、<id>.local というドメインを生成して
               -- ngx.var.proxy_upstream_host変数に格納
               local ngx_re = require "ngx.re"
               local res, err = ngx_re.split(ngx.var.request_uri, "/", nil, {pos = 0})
               local id = res[3]
               ngx.var.proxy_upstream_host = id..ngx.var.proxy_upstream_domain;


               -- resolver設定
               local resolver = require "resty.dns.resolver"
               local r, err = resolver:new{
                   nameservers = {{"x.x.x.x", 53}}, -- 使用するnameserver
               }
               if not r then
                   ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
               end


               -- ポイント2.
               -- 生成したドメイン名(<id>.local)を元に名前解決し、取得した結果をngx.ctxにセット
               --  (balancer_by_lua_blockで使用する)
               local answers, err = r:query(ngx.var.proxy_upstream_host, { qtype = r.TYPE_CNAME })
               if not answers then
                   ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
               end
               if answers.errcode then
                   ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
               end
               ngx.ctx.upstream_server = answers[1]
           }


           proxy_set_header Host $host;
           proxy_set_header X-Real-IP $remote_addr;
           proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
           proxy_set_header X-Forwarded-Proto $scheme;


           -- http://<id>.local/api に proxy
           rewrite ^/api/(.+)$ /api/ break;
           proxy_pass http://app;
        }
    }
}

ポイント1. 動的Routing

balancer.set_current_peer にてproxy先を動的に設定します。

host 部分にはドメインを直接指定することができないため、ポイント2. で ngx.ctx にセットしたDNSの値からIP(Route53にCNAMEレコードとして設定している)を指定しています。

balancer_by_lua_block {
    local balancer = require "ngx.balancer"


    -- ngx.ctxにセットしていた、Private DNSから取得した内部IPをセット
    local host = ngx.ctx.upstream_server.cname
    local port = '8888'


    -- proxy先セット。hostにドメインは直接指定できない
    local ok, err = balancer.set_current_peer(host, port)
    if not ok then
        return ngx.exit(500)
    end
}

詳細についてはOpenRestyのドキュメントを参照してください

ポイント2. 動的名前解決

r:query にて、生成したドメイン名(.local)を問い合わせます。r 部分は resolver:new でnameserverを指定したresolverとなります。 なお、nameserverに指定するIPは今回はRoute53のPrivate DNSを指定するため、外部nameserverではなくローカルのnameserver(10.0.0.2など)を指定することになります。

問い合わせ結果のanswers部分はLua table形式の配列となります。今回の例でいうと対象は1件となるので、その値をbalancer_by_lua_blockで使用するためにngx.ctxにセットしています。

local answers, err = r:query(ngx.var.proxy_upstream_host, { qtype = r.TYPE_CNAME })
if not answers then
    ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
if answers.errcode then
    ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
ngx.ctx.upstream_server = answers[1]

詳細についてはOpenRestyのドキュメントを参照してください

今回のまとめ

upstream先を動的に判定してproxyするという要件はそうそう無いかもしれませんし、途中までは複雑な構成になりそうだなぁとドキドキしてしましたが、結果としてはそれなりにシンプルになったかなと思います。今更ながらNginx(とlua module)は柔軟で良く出来てるなぁという感想でした。

後編はApp層について、EC2とSystems Manager パラメータストアあたりについて共有させていただければと思います。

developer.medley.jp

参考リンク

構築にあたり、下記記事を参考にさせていただきました。ありがとうございます。

qiita.com

お知らせ

メドレーでは、医師たちがつくるオンライン医療事典「MEDLEY」、オンライン診療アプリ「CLINICS」、医療介護の求人サイト「ジョブメドレー」、口コミで探せる介護施設の検索サイト「介護のほんね」などのプロダクトを提供しています。これらのサービスの拡大を受けて、その成長を支えるエンジニア・デザイナーを募集しています。

メドレーで一緒に医療体験を変えるプロダクト作りに関わりたい方のご連絡お待ちしております。

www.medley.jp