ChatWork Creator's Note

ビジネスチャット「チャットワーク」のエンジニアとデザイナーのブログです。

インフラチームで導入しているflowの話

この記事はChatWork Advent Calendar 2017 - Adventarの、12日目の記事です。

こんにちは。インフラマネジメント部の @cw-ozaki です。 弊社のフロントエンドではTypeScriptを導入していたり、一部のサービスでScala.jsを導入しようとしていますが、インフラ側ではflowを導入しています。 わざわざ利用するツールを変えてまで何故flowを導入するのか。その魅力をご紹介したいと思います。

どこでflowを使うの?

インフラ側での主な利用用途はAWS Lambdaになります。 どうしてflowをAWS Lambdaで利用するのかというと、AWS Lambdaは動かすのに労力がかかるので、動かす前に問題を発見したいという思いが強いからです。 というのも個人的な経験則ですがAWS Lambdaでの開発で大体問題になるのは下記の3つです。

  1. 凡ミス
    • プロパティ名間違えたとか
  2. IAMの権限付与漏れ
  3. 他サービスとLambdaやStepFunctionsでのLambda間の結合ミス

これらの問題は一度動かしてみれば解決するのですが、AWS Lambdaはコードをアップロードする必要があるため、どうしても高速に開発のサイクルを回すことができません。 もし、依存した他のサービスがある場合はそちらの準備や破棄も必要になるため、さらに開発サイクルの速度は落ちてしまいます。

そこで動かす前に静的型付けチェックを行えるflowを導入することで1.と3.の問題は解決できます。 ただし、2.に関してはflowでも解決できないので別の方法を模索してください。(chaliceのAutomatic IAM policy generationをNodeで実現するツールが欲しいです)

ちなみに、それなら元々静的型付けを行う言語を利用すれば良いのでは?と思う方もいらっしゃるかもしれませんが、そこはコールドスタート問題があるためNodeを採用しています。(他にもエコシステムの豊富さなどの理由もあります)

flowとAWS Lambda

先ほど上げたflowの導入理由はそのままTypeScriptやScala.jsでの導入理由にもなります。 それでは他のAltJSと比べてflowのどこが良いかというと

  • AltJSとしてではなくチェック用のツールとして提供されている
  • 利用する方法がいくつかある
    • 生のJSで利用
    • JSを拡張した特殊な型の構文での利用
      • この場合はトランスパイルして型の除去を行える
    • コメントに型定義を付与しての利用
  • トランスパイルする場合はメジャーなbabelを利用できる
    • つまり今あるbabelプロジェクトをそのまま再利用できる
  • flowの機能として型の網羅率や、型のないコードへの型定義の付与、型定義の自動生成などもある

といったところで、今あるJavaScriptの知識をそのまま利用できる導入のしやすさがflowの魅力です。

中でもAWS Lambdaでflowを利用するにあたって最も重要なのがコメントに型定義を書けるComment Typeを利用できることです。 AWS Lambdaでトランスパイルしたコードを利用するとManagement Consoleで修正がしにくくなり、エラー時のスタックトレースの行数がわかりにくくなるなどの問題が発生します。 そこを解決するにはトランスパイル必須なTypeScriptやScala.jsでは解決できず、トランスパイルをしないという選択をできるflowでしか解決できない問題です。

creators-note.chatwork.com 弊社の @eielh が書いたGASの話でも同じような結論に至っているので興味ある方はご参照ください。

flowの始め方

flowの導入方法はとても簡単です。

$ npm install --save-dev flow-bin
$ $(npm bin)/flow init
$ $(npm bin)/flow check

上記のようにNodeプロジェクトでflow-binをインストールして、flow checkコマンドを実行すればそれだけで静的型付けチェックを行えます。 準備が出来たら簡単なコードを例にどのようにして型付けを行っているのか解説したいと思います。

/* @flow weak */
const AWS = require('aws-sdk');
const s3 = new AWS.S3();

exports.handler = function(event, context, callback) {
  let promises = event.Records.map((record) => {
    return new Promise((resolve, reject) => {
      let params = {
        CopySource: `/${record.s3.bucket.name}/${record.s3.object.key}`,
        Bucket: `${process.env.TARGET_BUCKET}`,
        Key: `${record.s3.object.key}`
      };
      s3.copyObject(params, (err, data) => {
        if (err) {
          reject(err);
        } else {
          resolve(data);
        };
      });
    });
  });
  Promise.all(promises).then((results) => {
    callback(null, results);
  }).catch((err) => {
    callback(err);
  });
};

今回は簡単にS3 Event NotificationのPut Eventを受け取って、別のバケットにPutされたオブジェクトをコピーするAWS Lambdaです。 型の適用具合をわかりやすくするためにAtomのNuclideでflowのカバレッジを表示すると下記のように表示されます。

f:id:cw-ozaki:20171212113358p:plain

見事に真っ青ですね。これはflowの型で言うとanyまたはemptyになっているためこのような表示になります。anyは何の型としても扱えるため型を利用しているとみなされず、emptyは型が未定義だったり、flowが型を推論することができないときに使われます。(ちょっと複雑な型を書くと結構あっさり推論できなくなるのでカバレッジは常に表示していることをお勧めします)

今の状態ではflowを導入したありがたみは全くないので、段階を追ってカバレッジで青くなっていくところを減らしていきます。

Lambda Functionの入出力の値に型を付与する

まずは/* @flow weak */と警告を弱めていたのを/* @flow */と通常の警告に戻します。

f:id:cw-ozaki:20171212113500p:plain

そうすると画像のようにLambda Functionの入出力のところで型定義がないとエラーが発生します。 そこで、このLambda Functionの引数と、戻り値に型アノテーションを付与します。

/*::
// http://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/notification-content-structure.html
type S3_Notification_Event = {
  // ...
};
// https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-prog-model-context.html
type LambdaContext = {
  // ...
};
// https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-prog-model-handler.html#nodejs-prog-model-handler-callback
type LambdaCallback<T> = (error: ?Error | ?mixed, result?: T) => void;

export type LambdaEvent = S3_Notification_Event;
export type LambdaResult = []; // FIXME: 後で適切な型に直す
*/

exports.handler = function(
  event/*: LambdaEvent */
  , context/*: LambdaContext */
  , callback/*: LambdaCallback<LambdaResult> */): void {
  // ...
};

細かい定義は長いので省略しています。 今回の場合はS3 Event NotificationのEventのため型を固定にしていますが、例えばAPI Gatewayのようにユーザーの値を受け取るような場合など型を信用できない場合はmixedという型にしてType Refinementsを用いて適切な型に変換するのが定石です。逆に言えば何かしらの要因で型が決定できないところを固定にすると型と実際の値がズレてしまいます。多少手間でも適切な型に変換するというのはflowを使っていく上で重要なことなので注意するようにしてください。

export type LambdaEvent = S3_Notification_Event;
export type LambdaResult = []; // FIXME: 後で適切な型に直す

また、このLambdaEventLambdaResultはわざわざexportして外部から読み出せるようにしていますが、これはStepFunctionsでAWS Lambdaを連鎖させるときに役立つので、よくこういう書き方をしています。 例えば、このLambda Functionの後に別のLambda Functionを繋ぐ場合に

/*::
import type {LambdaResult as S3CopyResult} from './s3-copy.js';

export type LambdaEvent = S3CopyResult;
export type LambdaResult = [];
*/

というように先ほど定義したLambdaResultを読み込んで、次のLambda FunctionのLambdaEventとして扱うことができます。

AWS SDKに型を付与する

次は外部モジュールのAWS SDKに対して型を付与します。 AWS SDKの場合はflow-typedには型定義がないため自分で用意する必要があります。(TypeScriptなら型定義があるのに・・・とよく言われますが、型定義の質が低いことが多いのであんまり使えないことの方が多いです。全てを諦めて型を書くマシーンになった方がいろいろ幸せです)

外部モジュールの型を定義するには.flowconfig[libs]セクションに指定したディレクトリでxxx.js.flowといったファイルを作成します。

// http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#copyObject-property
declare type S3_CopyObject_Params = {
  // ...
};
declare type S3_CopyObject_Result = {
  // ...
};

declare module 'aws-sdk' {
  declare class S3 {
    copyObject(params: S3_CopyObject_Params, callback: (err: Error, data: S3_CopyObject_Result) => void): void;
  }
}

このように外部モジュールを型定義することで、いつも通り利用するモジュールに型を付与することができます。 これでS3.copyObjectの結果の型もできたので、さきほど定義したLambdaResultの型を正しい型に変更します。

export type LambdaEvent = S3_Notification_Event;
export type LambdaResult = S3_CopyObject_Result[];

これによって、このLambda FunctionはS3 Event Notificationを受け取って、S3 CopyObjectの結果の配列を返すということが明示され、よりわかりやすくなりました。

このあたりの型定義とOpaque Type Aliaseを使ってS3_CopyObject_ResultS3.copyObjectを呼び出さないと作れない型にすれば、このLambda Functionでは必ずS3.copyObjectを呼び出し、その結果を返すというさらに制約を持たせれそうだと妄想しています。本当にできるのかは知らないのでチャレンジャーな方がいればぜひ結果を教えてください。

Lambda Environmentの型を定義する

これで、一通りカバレッジを網羅して、型の警告を消すことができましたが、実はまだ適切に型を付与できていない部分があります。

Bucket: `${process.env.TARGET_BUCKET}`

実はこのLambda Environmentで注入される値の型は?stringになっています。仮に注入を漏らした場合にBucketの値は''となってしまい、実行時エラーになってしまいます。意外にこの注入漏れというのはくせ者で、今回は実行時エラーになりますが、ものによってはそのまま正常に動作してしまうパターンがあります。 そのため、Lambda Environmentの値を適切な型に変換します。

function _env() {
  if (!process.env.TARGET_BUCKET) {
    throw new Error('Undefined environment variables `TARGET_BUCKET`.');
  }

  return {
    TARGET_BUCKET: process.env.TARGET_BUCKET
  };
}

const env = _env();

このようにType Refinementを適切に使うことで、env. TARGET_BUCKETには必ず文字列が入っていることを担保することができます。

最終的なLambda Function

// http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#copyObject-property
declare type S3_CopyObject_Params = {
  // ...
};
declare type S3_CopyObject_Result = {
  // ...
};

declare module 'aws-sdk' {
  declare class S3 {
    copyObject(params: S3_CopyObject_Params, callback: (err: Error, data: S3_CopyObject_Result) => void): void;
  }
}
/* @flow */
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
const env = _env();
/*::
// http://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/notification-content-structure.html
type S3_Notification_Event = {
  // ...
};
// https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-prog-model-context.html
type LambdaContext = {
  // ...
};
// https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-prog-model-handler.html#nodejs-prog-model-handler-callback
type LambdaCallback<T> = (error: ?Error | ?mixed, result?: T) => void;

export type LambdaEvent = S3_Notification_Event;
export type LambdaResult = S3_CopyObject_Result[];
*/

exports.handler = function(
  event/*: LambdaEvent */
  , context/*: LambdaContext */
  , callback/*: LambdaCallback<LambdaResult> */): void {
  let promises = event.Records.map((record) => {
    return new Promise((resolve, reject) => {
      let params = {
        CopySource: `/${record.s3.bucket.name}/${record.s3.object.key}`,
        Bucket: `${env.TARGET_BUCKET}`,
        Key: `${record.s3.object.key}`
      };
      s3.copyObject(params, (err, data) => {
        if (err) {
          reject(err);
        } else {
          resolve(data);
        };
      });
    });
  });
  Promise.all(promises).then((results) => {
    callback(null, results);
  }).catch((err) => {
    callback(err);
  });
};

function _env() {
  if (!process.env.TARGET_BUCKET) {
    throw new Error('Undefined environment variables `TARGET_BUCKET`.');
  }

  return {
    TARGET_BUCKET: process.env.TARGET_BUCKET
  };
}

最終的なLambda Functionは上記のようになります。実際には型定義部分は別ファイルに分離したり、S3.copyObjectPromise化するための関数を用意したりしますが、だいたいこんな感じです。

ここまできっちりと型を書けばだいたい動作してくれます。 もし、何か問題がおきるとすれば

  1. 仕様を満たしてない/仕様と違う
    • 例えばS3.copyObjectで保存するKeyが仕様と違うとか
    • 型定義を間違えてるとか
  2. IAMの権限付与漏れ
    • S3 Event NotificationでEventを発行したS3 BucketでGetObjectの権限がないとか

ぐらいかなと思います。 1.に関しては例えばSAM LocalLocalStackを使ったテストで解決できると思います。 2.に関しては実際にAWS上で動かすしかないのでSAMServerlessApexなどのLambda Frameworkを使って、E2Eテストとしてデプロイ→検証→破棄を回せる状況を作るしかないと思います。

個人的にはSAM LocalやLocalStackを使ったテストを動かせるようにするぐらいなら、E2Eテストに集約してしまった方が手っ取り早いとは思っていますが、このあたりはバランスかなぁと思います。例えば今回みたいに簡単なLambda Functionが複数ある場合はE2Eテストに集約した方が早いですし、ちょっと複雑なLambda FunctionがあるならSAM LocalやLocalStackでテストを書いた方がいいです。

まとめ

いかがでしたでしょうか。 AWS Lambda界隈では最近テスト手法が盛り上がっていますが、flowを使うことでテストの前に問題を検出するというのも一つの手かなと思います。 flowはなかなか使い勝手の良いツールなので、本記事でもし興味が出たらぜひ触ってみてください。

ChatWork Advent Calendar 2017、明日は @cw-nishiguchi です。

ノンデザイナーズでも少しの気配りで格好つく整え方

こんにちは、守谷(@emim)です。

この記事はChatWork Advent Calendar 2017 - Adventarの、11件目の記事です。お楽しみいただいていますでしょうか。(初めて知ったー!という方は、ChatWorkの他のデザイナー・エンジニアの記事も面白いので、是非ご覧ください!)

さて本題です。

ChatWorkのデザイン部は、サイトやサービスのUIデザインの他、セミナーなどのイベントに使われる資料のデザインや、オフィスの内装の確認・検収までおこなっています。

そんな中、他の職域のスタッフからたまに、「デザイナーが調整すると、ほとんど変えてないのに何か違う(キマる)んだけど、何が違うのか教えて欲しい」という声が上がります。

今回は、デザイナーでなくても簡単に「整う」調整技についてまとめていきます。

続きを読む

ドメインモデルの根拠とドメインモデル貧血症の対策について

ChatWork Advent Calendar 2017の10日目の記事です。

こんにちは。かとじゅん(@) です。

何を書こうかと悩んだのですが、社内で意見を聞いたところ、やはりDDD関連がよいとなりました。

この記事も、もう四年前ですっかり古くなりました。最近どういう観点で実践しているかまとめてみます。(DDD初級者という方は、まず上の記事を読むことをお勧めします)

DDDを実践するにあたっての個人的な問題点は2つあります。ひとつは、「いきなりドメインモデルを作ることができない」という問題。もうひとつは、ドメインモデルを作り上げても実装コードに役に立つ振る舞いが思いつかず、いわゆる「ドメインモデル貧血症*1」になりやすいという問題です。このような問題は、僕がコミュニティで関わった多くのエンジニアから耳にします。今日の記事はこの二点について考えてみましょう。相変わらず、長編なので時間があるときにお読みください(笑)

*1:詳しくはこちら参照 → http://bliki-ja.github.io/AnemicDomainModel/

続きを読む

東京オフィス自慢の「椅子」紹介

f:id:cw-hayama:20171208144526j:plain デザイン部のハヤマです。ChatWork Advent Calendar 2017の9日目の記事です。

11月に新しくなった東京オフィスは、「働き方をアップデートできるオフィス」をコンセプトに細部までこだわって作られています。中でも共有スペースに配置しているデザイナーズ家具の椅子は家具屋さん顔負けのラインナップです!

今回は、そんな自慢の「椅子」をご紹介します。

続きを読む

REST APIを実行して学ぶRust

こんにちは。火村です。 ChatWork Advent Calendar 2017の7日目の記事です。

さまざまなプログラミング言語を学ぶと、違う部分や同じ部分が見えてきて、プログラミングの本質が見えてきて楽しいです。 ところで、新しいプログラミング言語に挑戦するときは、皆さんどうしていますか? 定番は「Hello, World」の出力ですが、次のステップは他の言語でやったことあること簡単なことに挑戦してみることが多いのではないでしょうか。

というわけで、Rustを使ってチャットワークAPIを実行してみました。 そこからRustの様々な要素を掴んでもらいたいなというのが今回の内容です。

  • はじめに
  • 解説
  • 補足
    • エラーハンドリング
    • 文字列
    • 非同期リクエスト
  • まとめ
続きを読む