LIVESENSE ENGINEER BLOG

リブセンスエンジニアの活動や注目していることを発信しています

気軽に試せるエンタープライズSDS「ScaleIO」を試してみた

こんにちは、インフラグループの水野です。
みなさんネットワークストレージ大好きですよね?
NFSやFC(Fiber Channel)-SAN、iSCSI、GlusterFS、Ceph、広義の意味ではオブジェクトストレージのAmazon S3、Swift etc… 長年利用されているものから新しいもの、エンタープライズからオープンソースまで様々あると思います。
今回はScaleIOの導入検証する機会がありましたのでご紹介させていただきます。

What’s ScaleIO?

EMC ScaleIO (以下ScaleIO)はEMCが提供しているエンタープライズ向けのSoftware Defined Storageプロダクトです。
同様のものとしてOSSのCephがしばしば挙げられます。

ScaleIOは以下のような特徴を持っています。

  • ソフトウェアで定義された、ブロックストレージを提供
  • 数千ノード規模まで対応しており、台数の増減が容易
  • 一部ノードがダウンしても運用継続可能な耐障害性を備える
  • 高速、高性能(特にCephより高速であると謳っている)
    • オーバーヘッドも少ないため、ハイパーバイザーなどとの同居が可能
  • インストールが容易、管理用のUIが便利

2015年ごろから検証用途であれば機能・容量共に無制限で利用できるようになったということがあり、気軽に検証できるようになりました。

いざ導入

公式で提供されているドキュメントが非常にわかりやすいのでそちらにしたがって進めていきます。

構成

ScaleIOは下記コンポーネントで構成されます。

  • GW(GateWay)
  • IM(Installation Manager)
  • MDM(Meta Data Manager)
  • TB(Tie Breaker)
  • SDS(ScaleIO Data Server)
  • SDC(ScaleIO Data Client)

また、公式ドキュメントではスタンダードな最少3台構成と冗長性を高めた最少5台構成が提案されています。

  • 最少3台構成
    • Master MDM * 1
    • Slave MDM * 1
    • TB * 1
  • 最少5台構成
    • Master MDM * 1
    • Slave MDM * 2
    • TB * 2

今回は最少3台構成+GW / IM用1台の合計4台構成で試してみます。 ScaleIOが扱うボリューム領域を別途用意する必要がありますが、今回はLVMを使って切り出すことにしました。
詳細情報は以下のとおりです。

役割 IPアドレス ディストリビューション ScaleIO用の論理ボリューム名(参照してるデバイス名)
GW / IM 192.168.10.1 Ubuntu 16.04 LTS
Master MDM 192.168.10.2 Ubuntu 16.04 LTS /dev/VolGroup00/lv_scaleio( /dev/dm-2 )
Slave MDM 192.168.10.3 Ubuntu 16.04 LTS /dev/VolGroup00/lv_scaleio( /dev/dm-2 )
TB 192.168.10.4 Ubuntu 16.04 LTS /dev/VolGroup00/lv_scaleio( /dev/dm-2 )

最終的には以下のような全体像になります。 f:id:nashiox:20170622201229p:plain

インストール

パッケージをダウンロード

インストールに必要なパッケージ群をダウンロードします。 必要なものは以下からダウンロードできます。

日本語版のダウンロードページだと現在(2017/06/21) v.1.32.3が、海外版のダウンロードページだとv.2.0.1.2がダウンロードできます。日本語ページのバージョンがちょっと古いですね。
今回は最新のv.2.0.1.2を利用するため海外版のページからScaleIO Linux版をダウンロードリンクから落とします。

ダウンロードしたものは以下のようにして解凍しておきましょう。

$ unzip path/to/ScaleIO_Linux_v2.0.zip
$ ls ScaleIO_2.0.1.2_Complete_Linux_SW_Download
Documentation                                      ScaleIO_2.0.1.2_Gateway_for_Windows_Download       ScaleIO_2.0.1.2_SLES_12.1_Download
ScaleIO_2.0.1.2_GPG-RPM-KEY_Download               ScaleIO_2.0.1.2_RHEL_OEL6_Download                 ScaleIO_2.0.1.2_UBUNTU_14.04_Download
ScaleIO_2.0.1.2_GUI_for_Linux_Download             ScaleIO_2.0.1.2_RHEL_OEL7_Download                 ScaleIO_2.0.1.2_UBUNTU_16.04_Download
ScaleIO_2.0.1.2_GUI_for_Windows_Download           ScaleIO_2.0.1.2_SLES_11.3_Download                 ScaleIO_2.0.1.2_XEN_6.5_Download
ScaleIO_2.0.1.2_Gateway_for_Linux_Download         ScaleIO_2.0.1.2_SLES_12.0_Download                 ScaleIO_v2.0.x_Linux_Windows_Quick_Start_Guide.pdf

ScaleIO Gatewayのインストール

まずはインストーラーをセットアップするため、ScaleIO Gatewayをインストールします。

Gatewayインストール用のrpm/debパッケージがあります。

$ ls ScaleIO_2.0.1.2_Complete_Linux_SW_Download/ScaleIO_2.0.1.2_Gateway_for_Linux_Download
EMC-ScaleIO-gateway-2.0-12000.122.x86_64.rpm ScaleIO_Complete_Config.csv                  emc-scaleio-gateway_2.0-12000.122_amd64.deb
EMC_ScaleIO_Software_Agreement.txt           ScaleIO_Minimal_Config.csv

今回はUbuntuで利用するのでemc-scaleio-gateway_2.0-12000.122_amd64.debをGatewayホストにアップロードしておきます。

インストーラー画面の初期パスワードを決めてインストールを行います。

[root]
### rpmとjavaが必要なので予め入れておきます
$ apt-get install rpm openjdk-8-jre

### 初期パスワードをGATEWAY_AMDIN_PASSWORD環境変数に渡してインストールします
$ GATEWAY_ADMIN_PASSWORD={{ パスワード }} dpkg -i /tmp/emc-scaleio-gateway_2.0-12000.122_amd64.deb
~~~~~ 省略 ~~~~~
The EMC ScaleIO Gateway is running. PID=11422.

https://192.168.10.1(Gatewayをインストールしたアドレス)にアクセスをしてインストーラー画面が表示されればOKです。 初回の接続が重く、タイムアウトする場合があるのでローカルからcurlなどでアクセスしておくと良いです。

デフォルトでは80/443でリッスンしていますが、ポートを変えたい場合は/opt/emc/scaleio/gateway/conf/server.xml内の${http.port}${ssl.port}を任意のポートに変えて、gatewayを再起動します。

$ sudo systemctl restart scaleio-gateway

ScaleIOのインストール

https://192.168.10.1でインストーラーにアクセスし、User name: adminPassword: {{ パスワード }}でログインします。 f:id:nashiox:20170621214628p:plain

ログインができると次のような画面になります。 f:id:nashiox:20170621214710p:plain

ガイドに従って、Get Startedボタンからinstallation packageのアップロードに進みます。 f:id:nashiox:20170621215243p:plain

Blowseボタンを押してインストールに必要なパッケージをアップロードしていきます。 ダウンロードしてきたパッケージ群の中に各ディストリビューションのインストールパッケージが入っています。

$ ls ScaleIO_2.0.1.2_Complete_Linux_SW_Download/ScaleIO_2.0.1.2_UBUNTU_16.04_Download
EMC-ScaleIO-lia-2.0-12000.122.Ubuntu.16.04.x86_64.tar    EMC-ScaleIO-sds1-2.0-12000.122.Ubuntu.16.04.x86_64.tar   EMC-ScaleIO-xcache-2.0-12000.122.Ubuntu.16.04.x86_64.tar
EMC-ScaleIO-mdm-2.0-12000.122.Ubuntu.16.04.x86_64.tar    EMC-ScaleIO-sds2-2.0-12000.122.Ubuntu.16.04.x86_64.tar   EMC_ScaleIO_Software_Agreement.txt
EMC-ScaleIO-sdc-2.0-12000.122.Ubuntu.16.04.x86_64.tar    EMC-ScaleIO-sds3-2.0-12000.122.Ubuntu.16.04.x86_64.tar
EMC-ScaleIO-sds-2.0-12000.122.Ubuntu.16.04.x86_64.tar    EMC-ScaleIO-sds4-2.0-12000.122.Ubuntu.16.04.x86_64.tar

今回はUbuntu16.04なのでそこからtarファイルをすべて選択してUploadボタンを押します。 アップロードが完了すると次の画面のようになると思います。 問題なければProceed to Installボタンで次に進みます。 f:id:nashiox:20170621215705p:plain

次はScaleIOクラスタの構成情報をアップロードします。 クラスタ構成を記述した以下のようなCSVを用意します。

IPs,Password,Operating System,Is MDM/TB,Is SDS,SDS Device List,Is SDC
192.168.10.2,rootパスワード,linux,Master,Yes,/dev/dm-2,Yes
192.168.10.3,rootパスワード,linux,Slave,Yes,/dev/dm-2,Yes
192.168.10.4,rootパスワード,linux,TB,Yes,/dev/dm-2,Yes

上記ファイルをBlowseボタンで選択し、Upload Installation CSVボタンでアップロードします。 f:id:nashiox:20170621220539p:plain

アップロードが完了すると次のような画面になると思います。 f:id:nashiox:20170621220143p:plain

MDM Password、LIA Passwordをそれぞれ入力し、Licenseにチェックを入れます。 あとはTopologyの内容がCSVに設定した項目とあっているかを確認して、次へ進みます。

ここまで来るとあとはほぼ自動でインストールが進みます。 各インストールフェーズが終わるたびにボタンを押して進んでいきます。 f:id:nashiox:20170621220705p:plain

インストールが全て完了すると以下のようになります。 Mark Operation Completedボタンを押して完了しましょう。 f:id:nashiox:20170621220815p:plain

バグフィックス

Ubuntu16.04にインストールをしていくと、SDCがうまくインストールできない事象にぶつかりました(同様にCentOS7でもインストールしてみましたがそちらでは発生しませんでした)。
ぶつかったのは以下の通りですが、一通り解決した方法を記載しておきます。

  • systemdのunitファイルが無い
  • unitファイルが利用する実行ファイルのパスが違う
  • driverを取得しに行くところの設定が無い

systemdのunitファイルがない

以下のパスにありました。

$ sudo cp -a /opt/emc/scaleio/sdc/bin/sdc.service /etc/systemd/system/
$ sudo systemctl daemon-reload

unitファイルが利用する実行ファイルのパスが違う

先ほど配置したファイルを書き換えました。

$ diff -u /opt/emc/scaleio/sdc/bin/sdc.service /etc/systemd/system/sdc.service
--- /opt/emc/scaleio/sdc/bin/sdc.service    2016-12-23 15:13:30.000000000 +0900
+++ /etc/systemd/system/sdc.service 2017-06-22 15:30:39.117453761 +0900
@@ -11,8 +11,8 @@
 After=network.target

 [Service]
-ExecStart=/opt/emc/scaleio/sdc/bin/scini start > /dev/null 2<>/dev/null
-ExecStop=/opt/emc/scaleio/sdc/bin/scini stop > /dev/null 2<>/dev/null
+ExecStart=/etc/init.d/scini start > /dev/null 2<>/dev/null
+ExecStop=/etc/init.d/scini stop > /dev/null 2<>/dev/null
 Restart=always
 RestartSec=0
 RemainAfterExit=true


$ sudo systemctl daemon-reload

driverを取得しに行くところの設定が無い

下記URLを参考に/bin/emc/scaleio/scini_sync/driver_sync.confを書き換えました。

EMC Community Network - DECN: ScaleIO: Set SDC Performance Profile and Set SDC Name Failed

github.com

使ってみる

今回はOpenStackのcinderバックエンドとして組み込んでみます。
/etc/cinder/cinder.confを次のように書き換えます。

$ sudo vim /etc/cinder/cinder.conf
[Default]
enabled_backends = lvm,scaleio  ### scaleioを追記

### 以下を最下部に追記
[scaleio]
volume_driver = cinder.volume.drivers.dell_emc.scaleio.driver.ScaleIODriver   ### 新しめのOpenStackなら同梱されてます
volume_backend_name = scaleio
san_ip = {{ ScaleIO GatewayのIP }}
sio_protection_domain_name = default
sio_storage_pool_name = defaultSP
sio_storage_pools = default:defaultSP
san_login = admin
san_password = {{ MDMのパスワード }}
san_thin_provision = false

$ sudo systemctl restart openstack-cinder-api openstack-cinder-scheduler

### openstack側にvolume設定をします
$ cinder type-create scaleio
$ cinder type-key scaleio set volume_backend_name=scaleio

これでOpenStack側でボリュームを作成する際に、scaleioをストレージの種別で選択すればScaleIO上にボリュームが作られます。
f:id:nashiox:20170622193550p:plain

オーバーヘッドが少ないのでコンピュートノードにScaleIOをインストールすることもできるそうです。

ノードの増減

ノードを増やすときはインストールのときと同様にWebインストーラーから行います。 先ほど用意したCSVを以下のように修正しましょう。

IPs,Password,Operating System,Is MDM/TB,Is SDS,SDS Device List,Is SDC
192.168.10.2,rootパスワード,linux,Master,Yes,/dev/dm-2,Yes
192.168.10.3,rootパスワード,linux,Slave,Yes,/dev/dm-2,Yes
192.168.10.4,rootパスワード,linux,TB,Yes,/dev/dm-2,Yes
192.168.10.5,rootパスワード,linux,,Yes,/dev/dm-2,Yes

192.168.10.5を追加してみました。 あとは先ほど同様CSVをアップロードしてインストールを進めていきます。 この時、下記画面でAdd to existing sys.に変えるのを忘れないようにします。 f:id:nashiox:20170621221614p:plain

これで簡単にノードを増やすことができます。

再起動等でノードが切り離された場合でも自動で復旧しますし、データはミラーリングされているため、一部ノードが切り離されたとしても継続して動作します。 この辺も非常に頭が良くて便利です。

ScaleIO GUIで見てみる

Windowsに同梱のGUIツールをインストールしてみました。 表示はこのような感じになります。 f:id:nashiox:20170622174310p:plain

ストレージ容量やIO速度、管理してる台数、アラートなどなど様々な項目があり非常に見やすいです。

まとめ

GUIでインストール・管理も簡単、ノードの増減による拡張性が高いなどさすがはエンタープライズ製品だなという感じでした。 エンタープライズ製品のストレージを触るためには通常費用が発生するので気軽に試すという機会はなかなか作れませんが、検証用途なら機能無制限で無料で利用できるというのは非常に良かったです。 本番導入にはライセンスが必要になりますので、導入は予算との兼ね合いになると思います。 しかしながら、管理・運用の容易さを考えると検討の価値は十分あるのではないかと思いました。

AWSの構成図をいい感じに出力してくる「Cloudcraft」を使ってみた

こんにちは、リブセンスのインフラグループに所属している竹本です。 主にDOOR賃貸(AWS)や転職ドラフト ITエンジニア版 / デザイナ版(オンプレ)のインフラまわりを担当しています。 インフラグループでは、各プロダクト毎に担当者(窓口役)をつけて定期的にローテーションをしているため、業務引き継ぎの際に構成図が必要な場面が多々あります。 そこで本日はAWSの構成図をいい感じに出力してくれる「Cloudcraft」についてご紹介したいと思います。

背景

DOOR賃貸では、ここ1年でELBをALBにリプレースしたり、一部APをRails化したりとインフラの構成が日々変化しています。 しかしながら、構成図の更新は手間がかかることもあり、後回しにしがちです。 また、作成者によって粒度が違うので解説が必要だったり、手作業なので漏れや間違いが発生する可能性もあります。

今回は、そんな課題を解決してくれる可能性を秘めたツール「Cloudcraft」を試してみました。

Cloudcraft

概要

AWSの構成図を3Dでいい感じに描けるアプリです。 使い方は、こちらからSign upすればすぐに使えます。 Google OAuthにも対応してます。 見た目かなりカッコイイです。

f:id:livesense-made:20170524110347p:plain

有料の「Pro Solo」プラン以上を契約すると、「Live Sync機能」が使えます。 この機能は、IAMにCloudcraft用のロールを作成し、「ReadOnlyAccess」ポリシーを付与することで利用できるようになります。 登録したアカウントのAWS環境をスキャンし、構成図を作成してくれるという優れものです。

料金プラン

詳細はこちら から確認できます。

f:id:livesense-made:20170529124459p:plain

Freeプランでも構成図を作成したり、Exportしたり、各コンポーネント(EC2等)のコストを表示できます。 f:id:livesense-made:20170524110830p:plain

Cloudcraftを使ってみた

一通り使ってみた所感をまとめたいと思います。 ご活用頂ければ幸いです。

初回作成

Live Sync機能を使って最初にできた構成図がこちら。 f:id:livesense-made:20170524112606p:plain なんだこれは(笑) カオス過ぎて記念にスクリーンショット取ってしまいました。 スキャンから構成図の作成まで自動でやってくれると思っていたのですが、 コンポーネントの配置は自分でやらないといけないようです。

最終的に出来上がった構成図

主要な部分のみですが、DOOR賃貸のステージング環境構成図を作ってみました。

f:id:livesense-made:20170529134554p:plain

良かった点

Live Sync機能

すでにご紹介した通り、スキャンすれば当該AWSアカウントで使用しているコンポーネントが自動でリストアップされます。 漏れ無く構成図を書くことができますね。

例) コンポーネントのリスト

f:id:livesense-made:20170529132210p:plain

関係する各コンポーネント間を自動でリンク

Live Syncでリストアップされたコンポーネント(例えばRoute53)をクリックするだけで、 Route53とそれに紐づくELBが画面上に出力されます。 不要なリソースが起動している等、予想と違う構成になっていた場合に気づきやすいので有用ですね。

例) 構成図のサンプル

f:id:livesense-made:20170524113701p:plain

フィルタ機能

フィルタ機能があり、例えば「staging」と入力すれば、インタンス名やtag等にstagingと入っているものだけが表示されます。 タグをうまく使えば、環境や機能ごとに構成図が作れそうです。

例) フィルタ入力画面

f:id:livesense-made:20170525184945p:plain

コンポーネントの自動更新

DOOR賃貸では、EC2を blue/greenでデプロイしています。 AMIに変更があれば、EC2のblue/greenを入れ替えるのですが、なんとその変更にも自動で追従してくれます。

blue/greenデプロイの詳細についてこちらのブログをご参照ください。

構成図を作成するだけで、月額料金が分かる

「BUDGET」タブを選択すれば、構成図上にあるコンポーネントの料金を出力してくれます。 設計の段階でおおよそのコストが分かるのはありがたいですね。

例) バジェット画面のサンプル

f:id:livesense-made:20170529153310p:plain

マネジメントコンソールへのリンク

例えば、構成図上のEC2コンポーネントをクリックすると以下のような画面になります。

例) EC2の詳細画面

f:id:livesense-made:20170529154256p:plain

出力されるインタンスIDをクリックするとマネジメントコンソールの当該EC2ページに遷移します。 ELB等の他コンポーネントも同様です。

あったらいいなと思う機能

サブネットの自動配置

サブネットがあるとかなり見やすくなるのですが、 自動ではサブネットは追加されません。手動でサブネットを追加しようと思うと画力が必要になってきます。 サブネットの自動追加機能が待たれます。

注意点

有料プランの場合、グリッドのサイズは無制限ですが、freeプランの場合は制限があります。 グリッドが足りなくなる可能性がありますのでご注意ください。

例) 有料プランでグリッドを無制限にした場合

f:id:livesense-made:20170526165722p:plain

まとめ

ある程度直感的に操作できるようなってはいますが、それでも操作に慣れるまでは時間がかかります。 また、構成図にサブネットがあるのとないのでは仕上がりがだいぶ違ってきますが、今のところ自動でサブネットは追加されません。 Pro Solo以上のプランを利用する場合、月49ドル以上の費用がかかってしまいますが、環境によっては費用以上の効果を発揮すると思います。 例えば、一度しっかりと作り込んでおけば、インフラ構成に変更があったとしても差分は自動更新してくれる(配置は手動ですが)ので、更新はかなり楽になると思います。 また、追記漏れや作成者によって粒度が違うといった問題も解消されます。 無料でも試せるのでこれを機会に検証してみることをオススメします。

番外編

havaも使ってみた

今回ご紹介したCloudcraft以外にも、同様にAWSの構成図を作れるhavaというサービスがあったのでご紹介します。 こちらはスキャンすると構成図の作成まですべて自動でやってくれます。 仕上がりがこちら。

f:id:livesense-made:20170526170530p:plain

すごく縦長になってしまいました。縦置きのディスプレイが必要な長さです。 こちらは構成図自体の修正はできないようです。 シンプルな構成であれば活用できるかもしれません。 よかったらこちらもお試しください。

転職会議のECSデプロイ事情

こんにちは、転職会議でプログラマをやっている山内です。 皆さんはDockerを使っていますか? 転職会議では、AWSに移行する際に一部のアプリケーションにおいてDockerを採用しました。

AWS上でDockerコンテナを動かすのにECSを利用しています。 今日は転職会議のECSへのDockerデプロイツールであるpnzrについて紹介します。

当初の運用方式

転職会議はAWS移行した際にDockerを導入しました。 AWSが公式で用意しているAWSコンソールには、最低限の機能しか用意されていません。 このためDockerコンテナのデプロイはchat botとlambdaを利用して行っていました。

デプロイに利用するtask definition templateにはデータベースのパスワードなどの秘密情報が含まれるため、githubのリポジトリに含めることができません。 そこで、秘密情報はS3に設置するようにしました。

当初のデプロイ方式をfig1に示します。

f:id:ieee0824x:20170526140742p:plain
fig1: 当初のデプロイ方式

この構成ではいくつか問題が有りました。

まずtask definitionは日々複数の開発者によって更新されるため、なるべくgitによるバージョン管理を行いたいです。 またS3に置いているとはいえ、秘密情報を平文で保存するのはセキュリティ上よくありません。 理想的には以下のような方式でtask definitionの管理とデプロイをできるとよさそうです。

f:id:ieee0824x:20170526140825p:plain
fig2: 理想的なデプロイ方式

何か良いものは無いか検討した

いろいろ下調べをした上でやりたいことはhakoが近かったです。 ですが以下のような点があったので採用しませんでした。

  • 自分たちでhakoを管理しようと思ったらドキュメントが少ない
  • 設定の暗号化をしたい
  • 複数のAWSアカウント(本番環境/検証環境)へのデプロイをやりたい
  • ELBの管理はTerraformでやるので必要ない

作ったもの

前述のようなことを行うため、Go言語製のコマンドラインツール pnzr (“ぱんつぁー”)を作りました。

転職会議ではchat botからpnzrを呼び出すことでデプロイを行っています。

f:id:ieee0824x:20170526140853p:plain
fig3: pnzrのデプロイ方式

このpnzrはECSのServiceとtask definitionの管理だけを行うという方針で設計されており、ELBやクラスターの管理は行いません。 ELBの管理も原理的に不可能ではありませんでしたが、ELBは別途Terraformで管理をする予定だったので含めませんでした。

pnzrには次の5つの機能があります。

  • ECSのデプロイ
  • 設定の分割管理
  • 設定の暗号化
  • 暗号化されたファイルの確認
  • 暗号化されたファイルの編集

基本的な設定項目はECSのtask definitionの項目に依存します。 実装の都合により、jsonのキーはキャメルケースにしておく必要があります。

pnzrで簡単なアプリケーションをデプロイする

例えば test-cluster という名前のクラスターに sample という名前のサービスをデプロイするとします。 その時の設定を sample.json という名前で保存したと仮定します。 その時の設定を例に示すと以下のようになります。

{
    "ECS":{
        "Service":{
            "Cluster":"test-cluster",
            "DeploymentConfiguration":{
                "MaximumPercent":200,
                "MinimumHealthyPercent":50
            },
            "DesiredCount":1,
            "LoadBalancers":[
                {
                    "ContainerName":"sample",
                    "ContainerPort":80,
                    "TargetGroupArn":"taget group の arn"
                }
            ],
            "Role":"ecsServiceRole",
            "ServiceName":"sample",
            "TaskDefinition":"sample-app"
        },
        "TaskDefinition":{
            "ContainerDefinitions":[
                {
                    "Cpu":0,
                    "Essential":true,
                    "Image":"ieee0824/dummy-app:latest",
                    "MemoryReservation":2048,
                    "Name":"sample-app",
                    "PortMappings":[
                        {
                            "HostPort":0,
                            "ContainerPort":8080,
                            "Protocol":"tcp"
                        }
                    ]
                }
            ],
            "Family":"sample",
            "NetworkMode":"bridge",
            "TaskRoleArn":"task role の arn"
        }
    }
}

前述の通りpnzrではロードバランサーの管理は想定していません。 ロードバランサーとtarget groupは予め何らかの方法で用意してください。

ロードバランサー周辺の設定は少しわかりづらいので説明しておきます。 ECSにおいてデプロイされたコンテナがロードバランサー紐づく時target groupを利用して紐付けられます。 なのでpnzrでデプロイする前にロードバランサーを作成しておき設定にarnを記述する必要があります。 ロードバランサーのarnに紐づくわけではないことにご注意ください。

"LoadBalancers":[
    {
        "ContainerName":"sample",
        "ContainerPort":80,
        "TargetGroupArn": "taget group の arn"
    }
]

おおよそのデプロイするための準備はこれで完了です。 pnzrにdeployオプションと先ほど作った設定ファイル名を渡すことでデプロイできます。

$ pnzr deploy sample.json

ECSの設定項目は多岐にわたるのでここに書いてない設定項目は多々あります。 設定項目はほぼtask definition通りなので、先に手動で設定を行って確認し、それをもとにjsonを書くとわかりやすいと思います。

設定ファイルを分割する

pnzrでは設定ファイルを分割することができます。 例えば本番環境と検証環境を分けてデプロイする時環境に依存する項目だけを切り出して管理するのに役立ちます。

まず本体となる設定ファイルを main.json とします。 main.jsonの中身を次のようにします。

{
    "ECS":{
        "TaskDefinition":{
            "ContainerDefinitions":[
                "Environment": $env
            ]
        }
    }
}

$env と書かれたjsonのフォーマットにそぐわないものが登場しました。 $env はpnzrの設定ファイルにおける変数です。

本体となる設定ファイルを作ったら次に本体に埋め込まれる情報を仕込んだファイルを作ります。 次のようなファイルを vars/config.json として作ります。

{
    "env" : [
        {
            "Name": "FOO",
            "Value": "var"
        }
    ]
}

デプロイのタイミングで本体の設定ファイルと分割設定の入ったディレクトリを指定することで設定が埋め込まれます。

$ pnzr deploy -f main.json -vars_path vars/

設定が埋め込まれることによって次のような設定としてデプロイされます。

{
    "ECS":{
        "TaskDefinition":{
            "ContainerDefinitions":[
                "Environment": [
                    {
                        "Name": "FOO",
                        "Value": "var"
                    }
                ]
            ]
        }
    }
}

この時 vars ディレクトリの中は config.json のみでしたが複数のjsonが混在していても問題ありません。

pnzrで秘密情報を扱う

pnzrは設定ファイルの暗号化に対応しています。 Ansibleでいうところのvaultのようなものです。 pnzrにおいて設定の暗号化は vault オプションを利用します。

たとえば以下のような secure.json を暗号化してみます。

$ pnzr vault -encrypt -key_id ${KMS_KEY_ID} secure.json
{
    "db_password": "foo",
    
}

暗号化すると次のようになります

{"type":"kms","cipher":"EQECAHh5q0tFgkoZe9C6czjL/QJ6+DlDwjLL6N3YmGIcYUKyuwAAAKkwgaYGCSqGSIb3DQEHBqCBmDCBlQIBADCBjwYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAxLgpVvtPehqdE3J5YOytNu974QzkXRoMqhU1OUfRnI413s9W7iqs5LB8n2CjIDd0OxsHgUMb2F25tb+B0f5OBgPDvQ/VL/sj4hJwpbXjkTdebIaeFA16b4A3gFaPNQmHF"}

暗号化した設定は平文の設定ファイルと同じように扱うことができます。 暗号化したものをデプロイするときはkms key idをデプロイ時に指定します。 kms key idを指定すること意外は平文をデプロイする時と変わりません。 pnzrが自動的に暗号化したファイルを復号してECSにデプロイします。

# varsは平文と暗号文の入ったディレクトリ
$ pnzr deploy -vars_path vars/ -key_id ${KMS_KEY_ID} main.json

f:id:ieee0824x:20170526140931p:plain
fig4: 暗号化したファイルをデプロイする図

暗号化した内容を編集したいときは vault-edit オプションを使用することで安全に編集することができます。 vault-editは環境変数に依存して起動するエディタが決定されます。
詳細はwikiをご覧ください。

さいごに

以上、簡単に転職会議でのDockerの運用についてご紹介しました。

些細な事ですがエンジニアのお仕事を楽にできれば幸いです。

超技術書典サークル参加と技術書製作におけるノウハウ

こんにちは、16卒入社の堤下です。 仕事ではジョブセンスリンクのiOS/Androidアプリを開発をしています。

みなさん、本を作ったことはありますか?
「もっと外に技術発信していきたいね」と話していたところ、4/29,30に超技術書典が開催されることを知り、有志で集まりサークル参加して本を頒布してきました。

f:id:roana0229:20170519002340p:plain

テーマは「それぞれ自由に書く!」ということで、以下のようになりました。

  • GoogleAppsScriptのライブラリ開発
  • MySQLで色々なグラフを書こう!
  • 自然言語処理によるフレンズ語生成
  • エンジニア職務経歴書の書き方
  • PHPのORマッパーを調べてみた
  • iOS アプリ開発の色々

本を書くのは技術を発信する1つの手段でしたが、書き始めてみると自分の持っている知識やノウハウを文章化する必要があり、後回しにしていた調べ物や曖昧だった部分を知る・理解する良い機会になりました。
今回は、その時の本を製作する流れと得たノウハウとを共有したいと思います。

本製作の流れ

1. テーマ・人数を決める

複数人で本を作ろうと思っている場合、まずはチャットや人伝で仲間を集めましょう。参加する側はテーマ・期間・ページ数がわかると参加しやすいです。

また、人数が集まったらキックオフミーティングをして、全体の流れや共著者それぞれのモチベーションも共有しておくことをおすすめします。

テーマ

アプリ開発や機械学習など特定の技術領域・分野で絞ることをおすすめします。テーマを絞らずに技術雑誌のような本も良いですが、タイトルや事前告知・会場で本の内容を伝えるのが難しくなってしまいます。

実際に今回作った本のテーマはバラバラで、一体なんと説明すれば良いんだ…?と悩んだ結果、会場ではポップを書いて対応しました(笑)

期間

本を出すイベントに合わせる形になります。

ページ数

本の制約として、ページ数は4の倍数にする必要があります。
また、本を作るということは表紙が必要だよね、と腰が重くなるかもしれませんが、技術書であれば表紙にイラストを利用しないパターンもあります。ただ、本を手にとってもらう第一歩にもなるため、少しでも目を惹くことを意識した方が良いでしょう。当日会場ではシンプルな本〜可愛いイラストの本まで様々でした。

2. 執筆環境を整える

著者は全員エンジニアかつ本の執筆は初めてだったので、出来る限り手軽かつ普段使っている環境に近くするため、Github + Re:View + md2review を利用しました。
執筆環境は開発環境を共通化しておくことと同じように重要なことです。インストール用のスクリプトやGemfileなど置いておくと良いでしょう。

まずはreview initコマンドで生成した状態のプロジェクトをPDF化できることを確認します。
※PDF化する際にはMacTexをインストールし、入稿用ファイルにはPDFにフォントを埋め込む必要があります。

$ review init article && cd article
$ review-pdfmaker config.yml
$ ls | grep book.pdf
book.pdf

続いて、執筆する際にはMarkdownで記述するため、md2reviewを利用してRe:View用の形式に変換した後にPDF化します。
また、基本的なアウトプットの設定は、config.yml, catalog.ymlで行います。

$ vim contents.md
# 記事を書く
$ md2review contents.md > contents.re
$ vim catalog.yml
# CHAPS: に contents.re を追記
$ review-pdfmaker config.yml

これでbook.pdfcontents.mdの内容が入っていれば、執筆環境の構築は完了です。

TechBoosterさんがRe:Viewを利用して出版しているリポジトリを公開しており、とても参考になります。
GitHub - TechBooster/C89-FirstStepReVIEW-v2: C89 初めてのRe:VIEW v2

3. 執筆する

さて、いよいよ執筆作業です。
執筆し始める前にいくつか決めておいたほうが良いことがあります。

  • ページ数
  • 文章の文体
  • 文章のレビュー
  • 締め切り

ページ数

全体ページ数から1人あたりのページ数目安を決めておきます。ある程度書いたところで、PDF化するとページ数の感覚を掴めると思います。
ただ、最初からページ数の目安に収まるように書くのではなく、多少超えるぐらいで書くと良いでしょう。
理由は、ページ数調整の際に追記するより、部分的な削除や言い換えや画像・コードのレイアウトによってページ数合わせる方が比較的調整しやすいためです。

文章の文体

大きくは「です・ます」調 or 「だ・である」調のどちらかになると思います。
必ずしも全ての文章を統一する必要はないですが、ベースはどちらでいくのかを決めておいたほうが読みやすいと感じました。全く統一できていない本を作った結果、身に沁みて感じています。

文章のレビュー

次項の「締め切り」にも関わってきますが、スケジュール的に余裕があるのであればお互いの文章をレビューすることをおすすめします。 エンジニアなら見慣れているPullRequestで文章をレビューしても良いでしょう。ただ、レビューの遅れにより進行が遅くなってしまっては本末転倒です。
そのため、

  • 文体は整っているか
  • 筋の通った文章になっているか
  • 技術的な間違いが含まれていないか

など、レビューの粒度・項目も重要です。

締め切り

執筆の最終締め切りは、入稿締め切りより数日余裕を持たせておく必要があります。入稿締め切りは、印刷所や印刷方法によって違ってきます。

また、最終締め切りとは別に初稿締め切りも決めておきましょう。
理由は、文章を書き終えたとしてもそのまま完璧な状態にするのは難しくPDFで確認すると、ページ毎の文章の切れ目・画像の大きさなどのレイアウトや誤字脱字などを調整しないといけない場合が多いためです。文章を全員が書き終えた時点で1冊の本としてPDFを作り、チェックすることをおすすめします。

締め切りに追われないと書かない人もいるので、締め切りには余裕を持たせましょう(笑)

4. 入稿する

「入稿する」といっても表紙+本文が完成していれば、利用する印刷所の入力フォームを埋めていくだけになります。
ここで失敗したら、意図しない状態の本になってしまう、と思うかもしれませんが明らかなミスがあれば印刷所から連絡が来ます。そこで修正すべき内容を聞いて、再度修正済みのデータを送ることができます。
日光企画さんを利用したのですが、初回入稿時に表紙データで不備があり、電話で丁寧に教えていただきました。

完成した本は、自宅に宅配することも可能ですがイベントと印刷所によっては、会場に直接搬入できる可能性もあるので、そうした場合には当日会場で確認することになります。

おわりに

いかがでしたでしょうか。本は作ってみたいけど内容が思いつかない人も多いと思います。ですが、意外と自分の中であたりまえのことが他人にとっては有益である場合も多くあります。
少しでも本を作りたいと思っている人の参考になれば幸いです。

転職会議の会員情報ページをReact化しました

f:id:livesense-blog:20170502163527p:plain はじめまして。16卒で入社したエンジニアの渡部です。

現在は転職会議のプロダクト開発グループに所属していて、最近は会員情報ページのフロントエンド開発を行っています。

今回はReact化にあたって直面した問題、それを解決するために採用・参考にした技術を幾つか紹介したいと思います。

コード例はTypeScriptで書いています。

目次

  • 型を欲する声の高まり(TypeScript)
  • action層の肥大化(redux-observable)
  • formのvalidation(Computing Derived Data(reselect))
  • viewをシンプルに保つ(High Order Components)

型を欲する声の高まり(TypeScript)

react-reduxで実装していると関数から関数へと値を引き回すので、型をつけて安心して実装したいという声がチーム内で高まってきました。

そこでTypeScriptを導入することにしました。

TypeScript

Flowtypeも有名ですが、TypeScriptを導入した理由は以下のとおりです。

  • チームに有識者がいた
  • vscodeの補完機能が魅力的だった。
  • jsファイルからの移行もany型を使ってそれほど苦なくできそうだった。

基本的にスムーズにjsからtsへの移行を進めることができましたが、いくつか詰まった点もあります。

Array.prototype.filterのtype predicateが効かない

type HogeArray = (string | number)[]type FugaArray = number[]にfilterを使って変換します。

const hoge: HogeArray = [1, "2", 3, "4", 5]
const fuga: FugaArray = hoge.filter((el): el is number => typeof el === "number")

しかし、type predicate(el): el is number => typeof el === "number"が効かず、hoge.filter((el): el is number => typeof el === "number")の型はtype HogeArray = (string | number)[]のままなので、上記コードはコンパイルで落ちます。

解決策としてはこんなのがあります。

https://github.com/Microsoft/TypeScript/issues/7657

lodashとの相性が悪い

lodashについては色々あるのですが、ここではmapValuesを例に挙げたいと思います。

type Profile = {
  name: string
  age: number
}
type Profiles = { [id: number]: Profile }

Profiles型のobjectにmapValuesを適用して各Profileをnameで置き換えます。 結果は以下のような型になります。

type Names = { [id: number]: string }

しかしながらlodash/mapValuesには、処理するvalueの型が引数と返り値で一致するように型がつけられています。 上述の処理では引数のvalueの型がProfile、返り値のvalueの型がstringなので、コンパイルエラーになってしまいます。

lodashはこのように型の指定がうまくいかないことがわりとよくあります。

action層の肥大化(redux-observable)

action層の肥大化をredux-observableを導入して解消したお話です。

redux-observable

RxJS

各所での採用実績を参考に私達はreduxを採用しています。

当初ミドルウェアにredux-thunkを採用していたので、apiとの通信といった非同期処理はaction層に書いていました。

こんな感じです。

// 通信の開始
function startConnection(connectionId: string){
  return {
    type: "START_CONNECTION",
    meta: { connectionId }
  }
}
// 通信の終了
function finishConnection(connectionId: string){
  return {
    type: "FINISH_CONNECTION",
    meta: { connectionId }
  }
}
// Error処理
function handleFetchError(error: string){
  return {
    type: "HANDLE_FETCH_ERROR",
    payload: { error }
  }
}
// 取得した値をstateに反映する
function fulFilledProfile(profile){
  return {
    type: "FULFILLED_PROFILE",
    payload: { profile }
  }
}
// mainの関数
function fetchProfile(uuid: string){
  return (dispatch) => {
    dispatch(startConnection(uuid))
    axios.get("/api/profile")
      .then(({data}) => dispatch(fulFilledProfile(data.profile)))
      .then(() => dispatch(finishConnection(uuid)))
      .catch((e) => dispatch(handleFetchError(e)))
  }
}

action層に単純なobjectを返す関数と関数を返す関数が混在していて、扱いづらくなっています。

async await構文を使うと見た目はスッキリしますが、action層にaction creator以外の関数が混じってしまう問題は解決しません。

// mainの関数
function fetchProfile(uuid: string){
  return async (dispatch) => {
    try {
      dispatch(startConnection(uuid))
      const {data} = await axios.get("/api/profile")
      dispatch(finishConnection(uuid))
    }
    catch(e) {
      dispatch(handleFetchError(e))
    }
  }
}

action層からこうした複雑性を逃がすために、私達はredux-observableを採用しました。

非同期処理やビジネスロジックを逃がすためのミドルウェアとして他にredux-sagaredux-logicがあります。

私達がredux-observableを選んだ理由は以下のとおりです。

  • チーム内でRxJSへの関心が高まった。
  • コードベースが少なかった。

ただ、RxJSをあちこちで使ってしまうとかえってコードの複雑さが増してしまうので、redux-observableのepic内にその使用を限定することにしました。

ではさっきのactionを書き直してみます。

//action
// 通信の開始
function startConnection(connectionId: string){
  return {
    type: "START_CONNECTION",
    meta: { connectionId }
  }
}
// 通信の終了
function finishConnection(connectionId: string){
  return {
    type: "FINISH_CONNECTION",
    meta: { connectionId }
  }
}
// Error処理
function handleFetchError(error: string){
  return {
    type: "HANDLE_FETCH_ERROR",
    payload: { error }
  }
}
// epic層に配置した非同期処理をキックする
function fetchProfile(connectionId: string){
  return {
    type: "FETCH_PROFILE",
    meta: { connectionId }
  }
}
// 取得した値をstateに反映する
function fulFilledProfile(profile){
  return {
    type: "FULFILLED_PROFILE",
    payload: { profile }
  }
}
//epic

const startConnection$ = (action$) => (
  acton$
    .map(({meta}) => startConnection(meta.connectionId))
)
const finishConnection$ = (action$) => (
  acton$
    .map(({meta}) => finishConnectino(meta.connectionId))
)
const fetch$ = (action$) => (
  action$
    .switchMap(() => api.get("/api/profile"))
    .map(({data}) => fulfilledProfile(data.profile))
)

const fetchProfileEpic$ = (action$) => (
  action$.typeOf("FETCH_PROFILE")
    .mergeMap((action) => Observable.concat(
      startConnection$(action$),
      fetch$(action$)
        .catch((e) => handleFetchError$(e)),
      finishConnection$(action$)
    ))
)

Epicについては簡略化したところがありますが、こんな感じです。

非同期処理や通信状態を管理するためのロジックをaction層から切り離すことができました。

formのvalidation(Computing Derived Data(reselect))

私たちはstateとviewを以下のような方針の下構成しています。

  • stateから重複等無駄な値を排除する
  • viewにロジックを持たせない

この2つのルールは相反する部分があるのですが、それをuserの平均年収を表示するアプリケーションを例に考えてみます。

viewにはロジックをもたせたくないので、平均年収はpropsとしてコンポーネントの外から与えられなければなりません。

しかし、stateにはuserそれぞれの年収を既に持っているので、平均年収を持つと情報が重複してしまいます。

viewで計算はしたくない、stateにも持たせたくない。

よってviewに渡す前にstateの値を再計算することになります。

Computing Derived Data

Computing Derived Dataは、stateとviewの間にselector層を設けてstateを再計算し、その結果をviewに渡す実装パターンです。

selectorはstateを受け取って計算を行い、新たな値を返す関数です。

実装例は下のようになります。

私たちはreselectを使ってselectorを定義しています。

reselect

// state
type User = {
  name: string
  salary: number
}
type State = {
  users: User[]
}

// selector
// reselect/createSelectorをつかった場合
const averageSalarySelector = createSelector<State, User[], number>(
  (state) => state.users,
  (users) => users.map(({salary}) => salary)
    .reduce((prev, curr) => prev + curr) / users.length
)
// reselectを使わない場合
function averageSalarySelector({users}: State): number {
  return users.map(({salary}) => salary)
    .reduce((prev, curr) => prev + curr) / users.length
}

// view
const AverageSalaryBase: React.SFC<{averageSalary: number}> = ({averageSalary}) => (
  <div>平均年収: {averageSalary} 万円</div>
)

const mapStateToProps = (state) => ({
  averageSalary: averageSalarySelector(state)
})

const AverageSalary = connect(mapStateToProps)(AverageSalaryBase)

さて、formのvalidationですが、validation errorもstateから計算可能な値です。 私たちはerrorをstateに持たせるのではなくselectorを使って計算してviewに渡すことにしました。

氏名編集フォームの例を見てみましょう。

ここでは、氏名が未入力のときにvalidation errorを表示するように実装します。

// state
type State = {
  profile: {
    name?: string
  }
}

// validator
const namePresenceValidator = createValidator<State, string | undefined, boolean>(
  (state) => state.profile.name,
  (name) => typeof name !== undefined
)

// view
const NameFormBase: React.SFC<{name: string, isValid: boolean}> = ({name="", isValid}) => (
  <div>
    { !isValid ? "氏名を入力してください" : <noscript /> }
    <input type="text" value={ name } />
  </div>
)

const mapStateToProps = (state) => ({
  name: state.profile.name,
  isValid: namePresenceValidator(state)
})

const NameForm = connect(mapStateToProps)(NameFormBase)

stateを再計算するロジックを単独で切り出すことで、テストもしやすくなりメンテナンス性も向上しています。

viewをシンプルに保つ(High Order Components)

前述の通り私達は極力viewをシンプルに保つようにしています。

とはいえviewからロジックを完全に排除するのは難しいので、関数として切り出して共通化することでcomponentをシンプルに保ちます。

例えば先の名前編集フォームを見てみましょう。

// view
const NameFormBase: React.SFC<{name: string, isValid: boolean}> = ({name="", isValid}) => (
  <div>
    { !isValid ? "氏名を入力してください" : <noscript /> }
    <input type="text" value={ name } />
  </div>
)

ここではisValidの値によってエラーの表示・非表示を切り替えています。

こんな感じでviewの表示・非表示を切り替えたいということはよくあるので関数として切り出すことにします。

type Component<T> = React.SFC<T> | React.ComponentClass<T>

function switchVisibility<T>(
  predicate: (props: T) => boolean,
  LeftComponent: Component<T>,
  RightComponent: Component<T>
): Component<T> {
  return (props) => predicate(props) ? <LeftComponent { ...props } /> : <RightComponent {...props } />
}

この関数はComponentを受け取ってComponentを返すHigh Order Components(HoCs)です。

High Order Components

HoCsを適宜使うことでviewが持つロジックを共通化することができ、またその関数単独でテストしやすくなります。

switchVisibility関数で先のエラー表示コンポーネントを実装してみます。

type PropsType = {
  message: string
  isValid: boolean
}

const ErrorMessageBase: React.SFC<PropsType> = ({message}) => (
  <div>{ message }</div>
)

const ErrorMessage = switchVisibility<PropsType>(
  ({isValid}) => !isValid,
  ErrorMessageBase,
  () => <noscript />
)

HoCsを使うのに便利なライブラリとして、recomposeがあります。

recompose

Readmeによれば、React Componentのためのlodashのような存在だということです。

switchVisibility関数をrecompose/branchを使って実装してみます。

使い方はこんな感じです。

const switchVisibility = branch(
  ({isValid}) => !isValid,
  (component) => component,
  renderNothing
)

const ErrorMessage = switchVisibility(ErrorMessageBase)

結び

以上羅列的にではありますが、私達がReact化にあたって使った技術を紹介させて頂きました。

React化の事例は数ありますが、本記事が読者さま方の参考になれば幸いです。

データ分析を支える「便利カラム」の問題点とその解決策

こんにちは、'16新卒入社で、Analyticsグループ所属の田中です。 仕事ではデータ分析基盤や機械学習システムの開発・運用を行っています。

今回はデータ分析基盤における「便利カラム」にまつわる問題と、それを解決するためのアーキテクチャについてご紹介します。

リブセンスのデータ分析基盤

みなさんの会社では、サービスのデータ分析をどのように行っていますか?

リブセンスにはデータ分析・活用の文化が根付いており、ディレクターや営業職の社員までもがSQLを用いてKPIのモニタリングや施策の評価を行っています。 分析活動を支えるために、社内では “Livesense Analytics” という全社横断のデータ分析基盤を構築・運用しています。

このような組織が形成されるまでのポイントについては次の資料で解説しています。

営業さんまで、社員全員がSQLを使う 「越境型組織」 ができるまでの3+1のポイント | リブセンス

Livesense Analyticsの全貌については次の資料で紹介しています。

リブセンスのデータ分析基盤の全貌 // Speaker Deck

少人数のチームで基盤を開発・運用するため、システムはAWS上に構築しています。 中核となる分析DBにはAmazon Redshiftを利用しており、数億件のレコードを集計するようなSQLを数秒から数分で実行することができます。

集計簡単化のための「便利カラム」

分析DBにはアプリケーションデータだけでなく、PVログのようなユーザの行動ログも保存しています。 これらのログにはURLや訪問日時などの生データに加えて、それらを加工して得られる二次データを付与しています。 この二次データ、いわゆる「便利カラム」には次のような値が含まれます。

  • URLをサービスの機能ごとに分類したページ種別
  • 連続する複数のPVをまとめるセッションID

これらのデータがあることで、ページ毎のPV数や複数PVにまたがるユーザ行動の持続時間の集計が簡単になります。

f:id:livesense-made:20170417160913p:plain

問題点1: 便利カラムの値の修正ができない

データ分析基盤を継続的に運用していると、しばしば便利カラムの値を修正したいことがあります。 例えば「これまでは前回PVから30分以上経過すればセッションが切れたとみなしていたが、その閾値を15分に短縮したい」といったケースです。

しかし、一旦DBに投入された過去データについては修正が難しい場合があります。 これは便利カラムの値がクライアントサイドやログ収集サーバで生成されているときに起こります。

以前のLivesense Analyticsでは、セッションIDはCookieに保存された前回PVのタイムスタンプをもとに生成されており、ページ種別はログ収集サーバで判定されていました。 このため、分析DBにログが投入されてからセッションの切れ目を変更するといったことは事実上不可能でした。

f:id:livesense-made:20170417160920p:plain

この問題の主な要因は、生データから二次データを生成するロジックの変更が想定されていないことです。

セッションIDなどの二次データは、URLやタイムスタンプなどの「事実」とは異なり、生データに対する「解釈」から生まれます。 セッション持続時間の閾値の例のが示すとおり、この解釈ルールは変更される可能性があります。 そのことが事前に想定されていなければ、あとからロジックの変更を適用することが難しくなってしまいます。

解決策1: バッチ層の導入

ロジックの変更を過去のデータに遡って適用するには、もとの生データから二次データを何度でも再生成できる仕組みが必要です。

そこで生データをAmazon S3などのストレージに保存し、バッチ処理で二次データを再生成する層を導入します。

f:id:livesense-made:20170417161227p:plain

通常の場合はバッチ処理を1日1回程度定期的に実行し、その都度に新たな生データから二次データを生成してDBに投入します。

二次データの生成ロジックに変更が生じた場合、バッチ処理の実装を変更し、過去の全ての生データから二次データを再生成してDBの中身を入替えます。

後者では蓄積された大量の生データを処理する必要がありますが、あらかじめHadoop MapReduceのような分散処理基盤を利用しておけば、処理の実装を切り替えずに済みます。 Amazon Elastic Mapreduce (EMR)などのオンデマンドサービスを使えば、必要に応じてクラスタを立ち上げられるため、コストを抑えることができます。

問題点2: バッチ処理の複雑化とパフォーマンス低下

バッチ層の導入によって「便利カラム問題」は解決できそうです。 しかしこの仕組みを長期的に運用しているといくつかの問題が生じると考えられます。

まず、生データが細切れになっているとバッチ処理のオーバーヘッドが増大してしまうことがあります。 ログが短い間隔でバッファリングされている場合、生データは数MB〜数十MB単位の細切れになったオブジェクトとして蓄積されます。 これらの大量のオブジェクトの読込にオーバーヘッドがかかると、全データのバッチ処理のパフォーマンスが低下してしまいます。

また、ログ収集時の都合により生データの構造がバッチ処理に適していないことがあります。 例えばユーザのマウスイベントを複数件束ねてログ収集サーバににPOSTしている場合、イベントを集計する処理を行う前にそれらを複数のレコードに分解する必要があります。

f:id:livesense-made:20170417161232p:plain

個々の問題はそれほど複雑には見えないかもしれませんが、長期間にわたって生データをログ収集時のまま保存していると、いざ全件処理を行いたいときに時間がかかったりエラーが発生といった問題が起こり得ます。

解決策2: マスタデータ層の導入

バッチ処理の複雑化やパフォーマンスの低下を招く主な要因は、バッチ層が次の2つの処理を同時に行っていることです。

  1. 生データをバッチ処理に適した形に整形する
  2. 整形されたデータから二次データを生成する

特に1.は二次データの生成ロジックとは独立しており、本来データ収集時に一度行うだけで済むはずです。 そこで、生データに対する一度きりの加工処理を行うためのマスタデータ層を導入します。

f:id:livesense-made:20170417161240p:plain

このマスタデータ層では、生データを次のような性質を満たすよう加工します。

  • 解釈によって変わることのない一次データのみを保持している
  • オブジェクトの粒度や形式がバッチ処理に適した状態になっている

二次データの生成処理とは異なり、マスタデータ層では定期的な差分処理のみを考えればよく、過去ログに対する全件処理は通常発生しません。 マスタデータ層を設けることで、二次データの生成処理をシンプルに保ち、パフォーマンスを向上させることができます。

ラムダアーキテクチャに向けて

実は上記のアーキテクチャは、ラムダアーキテクチャと呼ばれるデータ分析基盤の設計指針を参考にしたものです。

Lambda Architecture » λ lambda-architecture.net

本来のラムダアーキテクチャでは、データがマスタデータ層・バッチ層を通って利用可能となるのを待たずに速報値の分析ができるよう、スピード層と呼ばれる別のデータフローを構築します。 しかし今回の要件ではリアルタイム性は求められない(前日までのログが利用可能であればよい)ため、スピード層は用意していません。

この記事では説明しませんが、ラムダアーキテクチャについて詳しく知りたい方には次の書籍をおすすめします。

O'Reilly Japan - スケーラブルリアルタイムデータ分析入門

バッチ層・マスタデータ層に使われる技術

さて、ここまでバッチ層・マスタデータ層のアーキテクチャについて説明してきました。 最後にこれを実現した技術スタックを簡単にご紹介します。

もともとの分析基盤をAWS上に構築していたため、新しく開発したバッチ層・マスタデータ層でもAWSのサービスを活用しました。 生データ・マスタデータを保存するストレージにはS3、分散処理基盤にはEMRを採用しました。

分散処理フレームワークにはApache Spark (2系)を採用しました。 実装言語にはSpark本体の実装にも使われているScalaを採用し、パフォーマンスと型安全性を両立させるためDataset APIを用いて処理を記述しています。

開発で得られた知見は以下のような記事でも紹介しています。

yubessy.hatenablog.com

今後機会があれば実装に関するノウハウなども共有していきたいと思います。

おわりに

長くなりましたが、最後までおつきあいいただきありがとうございました。 今後もこのブログでは分析基盤やデータ活用に関する内容を投稿していきますので、どうぞご期待下さい。

事業を推進するために必要なエンジニアスキル

こんにちは、新卒2年目エンジニアの片岡です。 正社員転職メディア『ジョブセンスリンク』の開発を行っています。

job.j-sen.jp

リブセンスには職種の『越境』文化が根付いています。 セールスに必要なデータは営業担当者が自らSQLを書いて用意します。 エンジニアがディレクター的な働きをして機能設計に深く関わることはリブセンスにおいて自然です。

このような環境下で、私は『開発者の立場から事業を推進する』という指針を持って他職種の方たちと協働しながら開発を行ってきました。

『事業を推進するエンジニアに求められるスキル・姿勢とは?』という自らに課した問いに対して、新卒としてリブセンスで2年間を過ごした経験からたどり着いた自分なりの答えを共有させていただきます。

事業を推進するエンジニアに求められるスキル・姿勢とは

1.危機感を持って技術を学び続ける

入社当初、事業を推進するためには事業ドメインの理解やマネジメントなど「技術以外の総合力」が求められると私は思っていました。

しかし実務経験を経た今考え直すとむしろ逆で、事業を推進するエンジニアにこそ技術が求められるのだと感じています。 設計・コーディング・プロジェクト管理など様々な技術がなければ中長期的に見て効率的な開発を続けることができないからです。 変化の早い業界ですから、継続的に新技術のキャッチアップを行うことも必要です。

課題に対し、解決できる術をもたないエンジニアは等しく価値がありません。 事業を推進するエンジニアにとって、特定の技術スタックにこだわらず様々な技術領域に理解があることは必要不可欠だと思っています。

いま私は、「様々な技術領域に対して、広く、かつ浅くない理解を得る」という指針を持って継続的な技術的キャッチアップを心がけています。

2.自分の給与以上の価値を生み出すコードを書く

あなたが今日書いたコードは、今日もらった給与より価値を生みますか?

これは私が先輩からかけられた質問です。

エンジニアなら誰しも、技術的に正しいことをしたいと思っています。 メンテナブルで、再利用性が高く、完結で分かりやすいコーディングを誰しもが心がけています。 また、技術的にエキサイトしたいと思っています。

しかし、自分が書いたコードが自分の給与以上の価値を生み出しているか意識しながら開発している方はどれくらいいるでしょう?

例えば営業の方々は、個人として直接売上に責任を追っています。 それくらいの当事者意識を、私は持っていませんでした。 生み出す価値は、売上でなくとも社会への影響など異なるもので構いません。

if文のネストを気にするのと同じくらい自分の書くコードの価値を考えよう、と私は意識しています。

3.スピードvs品質の二項対立に囚われない

開発を行う中で、品質に関するトレードオフが発生します。 すなわち、品質を捨ててスピードを取るべきか速度を落として品質を高めるべきか、という議論です。

業務の中で、私はどちらの選択肢も見ました。 品質に倒したプロダクトは、事業的に求められる期限を守ることができないことがありました。 スピードに倒したプロダクトは、のちに破綻して『負債』と呼ばれています。

スピードvs品質のトレードオフでは、問題は解決しないことを私は学びました。

その上で、重要なのは「守るべき品質は何か」を見極めることだと思っています。裏を返せば「捨てる品質」の見極めです。

ミッションクリティカルであったりボトルネックになりやすいと思われるところは、エンジニアの職務として徹底的に品質に倒します。 逆に要件が変わりやすかったり、作り直すのが容易であるような部分については、容赦なく品質を落とすことも必要だと思っています。

4.他職種から信頼を得られる仕事をする

さきほど、「品質を見極めて容赦なく捨てることも必要」ということを書きました。 これは関係者の協力なしにはできません。

例えば、日時の入力フォームがあった場合、ファーストリリースに間に合わせることを優先して外部ライブラリのUIを使ったとします。 これは一般にデザイナーにとって面白くありません。 サイトカラーやUI設計があって、その中に外部ライブラリの違和感あるデザインが入り込むのはエンジニアにとっても好ましくはありません。

だとしても、必要なのは「入力フォームに日時が入力できる」ことであって、きれいで独自性が高く入力しやすい日時入力フォームを提供することではありません。(その入力フォームがUX上重要な役割を担うような場合は別ですが)

この場合、チームのデザイナーの職務ややりたいことを聞き、その上で自分たちが提供するものの本質的な価値について話し合い方針を決める必要があります。

私が担当した新規機能開発の事例では、ファーストリリースではデザインを妥協していただき、ABテストでデザインをいじるタイミングで1から綺麗に作り変えました。 このように、他職種の立場にたって対話することを心がけ、信頼を勝ち取ることも事業を推進するエンジニアになる上では重要なことだと思っています。

詳しくはDevLOVEというイベントの登壇資料にまとめてありますので、もしよければ読んでみてください。

speakerdeck.com

おわりに

『事業を推進するエンジニアに求められるスキル・姿勢とは』というテーマで4つの観点を紹介しました。

少しでもみなさんの開発の役に立てば幸いです。

良いチーム作りが成果創出につながった事例

f:id:livesense-made:20170104150246j:plain

皆さんはご自分のチームが、成果を生み出し続ける「最高に良いチーム」だと思いますか?

最近よくそんなことについて考えを巡らせている、リブセンスの風間です。 ジョブセンスリンクアプリ開発チームで、ディレクター兼プロダクト・オーナー(PO)をしています。 サービスを提供するユーザーさんのこともチームメンバーのことも幸せにしたいと欲張りながら奮闘し、失敗したり喜んだりする毎日を送っています。

昨年11月22日にリブセンス社内で「アプリ大会議」が開催されました。 アプリ大会議とは、ネイティブアプリに関わるリブセンス社員が互いの知見を持ち寄り、楽しく交流を深めて仲良くなることを目的としたLT大会です。 今回は、その中で私が発表した「良いチーム作りが成果創出につながった事例」をご紹介したいと思います。

POになんてなりたくなかった

今なら胸をはって「POの仕事が楽しいです!」と言えますが、実はPO就任した当時は嫌で嫌で…。正直に打ち明けると、とても怖かったんですね。 f:id:livesense-made:20170104154738j:plain   理由は以下2つ。

  • アプリの知見が社内になく、数字を伸ばせる自信がなかったから
  • リーダーの経験がなく、チームをリードできるか不安だったから

それをどうやって良い方向に持っていくことができたか? 今回はテーマを後者の「チームビルディング」に絞って、どんな取り組みをしてきたか、それがどう成果に繋がったかをお話したいと思います。

「自分よりも優秀なメンバーたち」

当時のチームにアサインされたメンバーは、全員が年上で経験/スキル豊富な人たちでした。一方で、POの自分はアプリ社内開発も未経験なら、リーダーになるのも初めてという頼りなさ。 こんな自分が信頼してもらえるだろうか…と強く不安を感じたことを覚えています。しかし、成果をあげて目標をクリアするためにはメンバーの協力が不可欠。気持ちを切り替えて、優秀なメンバーに協力してもらえるリーダーになるためにどうしたらいいか一つずつ考えていくことにしました。

「仲間のことをすきになる」

まずは、皆のことをすきになることに決めました。そうすることで本気で相手を理解し、協力し合いやすくなると考えたからです。 やってみたのはこのあたり。

  1. 接触頻度を増やす(1on1定期開催、理由をつけてランチ等)
  2. 相手の情報を知る(すきなこと、きらいなこと等)
  3. 相手の名前を沢山呼ぶ(リアルでもチャットでも)
  4. 共通体験を増やす(納涼船に乗る/UNOで遊ぶ等)

それぞれ効果を感じましたが、中でも意外とよかったのがUNOです。チームビルディングにUNO?と思われるかもしれませんが、年齢も職種も超えて盛り上がれるUNOは互いの距離を縮めるのに効果抜群。チームの雰囲気が一気によくなって、仕事も進めやすくなりました。その後、新メンバー加入後のUNOはチームの恒例行事になっています。

f:id:livesense-made:20170104163017j:plain

「ひとりひとりの考えを理解する」

次に、メンバーのことをより深く理解するために、1人ずつじっくり話をきかせてもらう時間を作りました。 何が好きで、どんな仕事をしてる時が楽しいか、ここで何を実現させたいか…等を1つずつ尋ねていき、これまでの失敗経験なども教えてもらいます。 そして、教えてもらった内容をもとに、皆の思いを実現させつつ同時にプロダクトも伸ばせる方法がないか考え、一つずつ実行してみることに。実際にどんなことを行ったかは、次のSTEP.1~3でご説明していきます。

STEP.1 戦いにそなえる

f:id:livesense-made:20170104153459j:plain シニアエンジニアさんが一番心配していたのが、リリースを急ぎすぎたことによる今後の運用に耐えられない実装状態でした。まずは私自身が課題を把握するために、専門的な話は「例え話」を使ってもらいながら理解して、修正工数を確保。一部機能を作り直す決断もしました。その結果、改善スピードを妨げる「負」が取り除かれ、機能追加がしやすくなりました。これまであまりここに工数を割くPOがいなかったとのことで、この件はとても喜ばれ、この時に信頼関係ができはじめたように思います。

STEP.2 作戦はみんなで

f:id:livesense-made:20170104150433j:plain 企画に参加したいという声をうけ、企画案はできるだけ早い段階で皆に相談することを徹底しました。「こんなことをやりたいけどどう思う?」と手描き案を共有し、皆の知恵をもらって案を改善することで、開発着手時には「これは僕/私が一緒に考えた企画だ」と思える状態を目指します。その結果、「アプリはPOがつくるもの」→「アプリをつくるのは私たち」という意識が根付き、チームメンバー全員が自分ごと感を持って改善に取り組むようになりました。 また、企画職だけでは考えつかない優れた案がうまれやすくなる、開発側が早めに要件を把握できて開発がスムーズになる等の効果もありました。

STEP.3 見積もりは信頼してまかせる

f:id:livesense-made:20170104155559j:plain 開発方針を考えるのが楽しい、設計が好きだという意見も大切にしました。 私はよく「締切に厳しい」と言われますが、自分から締切日を指定しません。私がやるのは、この施策は何のためにやるのか、次は何が控えているか、目的とロードマップをしっかり伝えて全員に理解してもらうことだけです。いつまでにできるか/どう作るべきかはメンバーを信頼して任せ、開発方針には細かく口出ししないようにしています。(このやり方がいいかどうかは、チームのメンバー構成にもよると思います。)

たまに、そのやり方だとゆるいスケジュールを引いてラクしようとされるんじゃないかと聞かれることもありますが、実際に皆が目的を充分に理解した上で自発的に考えるスケジュールは、毎回ビックリする程スピード感のあるもの。「POに言われたから急ぐ」ではなく、「自分でやるべきだと思った基準で全力でやる」という意識がチーム内に根付いた結果、事業部内の他のチームに驚かれる早さで改善サイクルが回るようになりました。

自分ごと感をもったチームは強い!

こうしてチーム全体が前のめりになって、リリースはどんどん高速になっていき、新しい機能が次々とアプリ内に追加されるようになりました。全員で議論を重ねながら改善を重ねた施策の効果は順調に伸び、季節が変わる頃には当初のチーム目標を大きく超える成果を出すことにも成功しました。

はじめは周囲に心配されていた小さなチームでしたが、少しずつ社内で事例共有させていただく機会も持てるようになり、2016年上半期にはリブセンスベストチーム賞にノミネートして頂くまでに成長しました。 f:id:livesense-made:20170104163055j:plain

さいごに

これらの取り組みの中で、個人的にとても嬉しかった「忘れられない出来事」があります。

弊社では定期的にエンジニア向けのサーベイを実施して、開発メンバーから意見や不満を拾い上げるということをしているのですが、なんとなくそのサーベイ結果を眺めていたある日、回答の中に自分に向けられたメッセージがあることにふと気が付いたのです。

【質問項目】仕事仲間は責任をもって精一杯クオリティの高い仕事をしているか【回答】 自分がアサインした時点でかなり不利な状況を押し付けられても前向きに日々改善を繰り返しているPOは素晴らしい。正直、自分の成績のみを考えたら違う案件や部署にいったほうが正解だと感じる。そうした案件にもかかわらず、また、その分野について詳しい知識を持っていないにも関わらず、ここまでサービスを成長させた手腕と責任感はすごい。

f:id:livesense-made:20170106210954j:plain

我武者羅に走りながらも常に不安を感じていた自分にとって、これはとても心に染み入る言葉でした。

チーム全員で力を合わせて戦うことに懸命になり続けた結果、それが成果につながり、お互いへの信頼や尊敬にもつながったことを実感したこの数ヶ月。今後は、さらに大きな成果を生み続けるチームであれるよう、POとしてもっともっと成長せねばと思います。

さて、そんな私たちジョブセンスリンクアプリ開発チームですが、次に掲げた新たな夢は大きく、目標達成への道のりは険しく、会社への貢献度だってまだまだです。今日も目黒の片隅のビルで、「転職経験の乏しいユーザーにこのUIは使いやすいだろうか?」「この機能は本当にユーザーに役立つだろうか?」と真剣に議論を重ねながら、最高に使いやすい転職アプリを目指して開発をしています。もしもあなたがスマートフォンで気軽に転職活動をしてみたいなとちょうどお考えでしたら、ぜひ一度このアプリを試していただけますと大変嬉しく思います。

itunes.apple.com

play.google.com

リブセンスで働くのは面白そうかもしれないな、という考えが少し頭をよぎったあなたは、ぜひ以下よりお気軽にご連絡ください。社員一同、あなたにお会い出来ることをたのしみにお待ちしております。

recruit.livesense.co.jp

ということで… 最後まで読んでくださり、ありがとうございました。

【本発表スライドを全て見たい方はこちら】

speakerdeck.com

Livesense式 開発合宿マニュアル part1

f:id:taise:20161112151149j:plain

Analyticsグループの大政です。
普段はデータ分析基盤や、レコメンド・エンジンの開発をしています。

開発合宿、やっていますか?
Livesenseでは、仕事の一環で開発合宿をすることもあれば、有志で集まって週末に開発合宿をすることもあります。
これまで5回ほど有志で開発合宿を行ってきた経験を元に、開発合宿を成功させるためのノウハウをお話したいと思います。

今回は、(1)なぜ開発合宿をやるのか、(2)どんなスタイルがあるのか、(3)おすすめの施設はどこかについてご紹介したいと思います。

続きを読む

レガシーコードの最適化とPHPバージョンアップ

f:id:boscoworks:20161021132156p:plain

 少し前のことになりますが、正社員転職サービス「ジョブセンスリンク」を構成するPHPアプリケーション群のPHPバージョンアップ対応と、それに合わせてレガシーコードの大幅な整理を行いました。
 「PHPのバージョンあげて、リファクタリングしたんだ」と一言で言えば簡単ですが、日々のサービス改善を滞らせず、システムのリニューアルを同時に進めていくのは多大な労力を要しました。
 今回はその仕事を主に担当した、キャリア事業部技術基盤チーム*1の海野がお届けします。お手柔らかにどうぞ。

ミッション

 PHPのバージョン問題。レガシーコードの山積。システムが歳を重ねるにつれ、必ず直面する大きな問題です。
 システムは、初めてリリースされた数年前の数倍の規模になっているでしょう。
 売上を支えるシステムを維持し、事業を加速させる施策を阻むこと無く、システムのリニューアルを進める。これが今回のミッションとなりました。
 このミッションを進める上で解決したかったのは、以下の2点です。

  • PHPバージョンアップ
  • 全社共有ライブラリの最適化

 今回は、この課題に対する取り組みについてお話します。

*1:ジョブセンスリンクのインフラ管理やシステム開発を主に担当しています

続きを読む