Ponz Dev Log

開発のことから合間に読んだ本の感想まで、マイルドに書きます

Pythonによる新しいデータ分析の教科書 読了後感想

今回は自分にしては珍しいPythonの読書記事です。

データ分析と機械学習を使った予測モデル構築のお仕事をすることになり、 基礎練習として翔泳社から出版されている『Pythonによる新しいデータ分析の教科書』を読んだのでその感想になります。

Pythonによるあたらしいデータ分析の教科書 (AI&TECHNOLOGY)

Pythonによるあたらしいデータ分析の教科書 (AI&TECHNOLOGY)

ターゲット読者は誰か

この本でターゲットにしているのは、主に以下2点が当てはまる エンジニア です。

  • Pythonをある程度理解している
  • データ分析・予測モデルの作成を行いたい初学者〜中級者

「プログラミングは全くできないけど、Pythonを使えば簡単にできるかも」と安易に考えている方には向いていないです。 実行環境としてJupyter Notebookを使いながら、Pythonでガッツリとコーディングします。

何が書かれていたか

これからデータ分析・機械学習を行うエンジニアに向けて、以下4つのテーマが書かれています。

  1. エンジニアが求められている役割
  2. 分析に使う数学の基本
  3. ライブラリを使った分析・可視化・予測モデル構築
  4. データ収集と、自然言語・画像処理のためのデータ加工

内容的には学術的な部分には深入りせずに、機械学習を使った分析の各ステップに重点を置いています。 そのため、実務的にエンジニアが使うライブラリ・前処理・モデルの知識にページ数が割かれていて、Python機械学習そのものの説明はサラッと流しています。

また、機械学習の処理の手順に沿って、データの加工→可視化→モデルの構築→評価の順に章立てされているため それぞれでどのような処理が必要になるのか、手が掛かる部分はどこか、クラウドサービスを使いながらできるところはどこかと想像しながら学べます。大きな流れの中の立ち位置が明確なので、闇雲に手を動かしているという感じがしません。これはいいぞ!

章立てとともにこの本で重要なのが機械学習の手順として以下の図です。 本書10ページに掲載されているこの図は紹介されているライブラリ以外でも共通して覚えておくべきものです。

f:id:accelerk:20190813234632p:plain
機械学習の処理の手順

クラウドベンダーの提供するサービスを使うと多くの手順が省略・簡略化される傾向にありますが、自分が今どの処理に手をつけているのか、次はどの処理に進むのかを理解する助けになりますね。 機械学習の記事や用語を調べるとそれぞれのステップが個別に紹介されることが多いので、この図を見てようやく全体像を掴むことができました。

ライブラリの章では以下のものが紹介されています。どれも分析時に頻出するライブラリです。

  • データ加工
    • NumPy
    • Pandas(データ加工)
  • データ加工
    • Matplotlib
  • モデル構築と評価
    • scikit-learn

それぞれ手を動かしながら学べるところも、理解しやすいポイントです。1章の中でそれぞれ30~40ページほどの内容なのでとっつきやすいです。 データもアヤメの有名なデータセット(Iris dataset)を使って進めていたので、個人的には楽しかった。

さらに、ここで作成したモデルはpickle形式やjoblib形式でエクスポートすればクラウドベンダーのサービスで利用することもできます。 GCPだとAI Platformでscikit-learnのモデルをトレーニング・デプロイできるので、クラウド機械学習を一緒に使う方法を学ぶこともできますね。

cloud.google.com

総じて読みやすい。全体像から詳細を順序立てて進めて理解できる良書だったと感じました。

参考

機械学習そのものの説明はサラッと流されているので、GoogleがUdemyで配信している『はじめてのAI』を視聴することをオススメします。

https://www.udemy.com/google-jp-ai/

自分自身は予測モデル構築や機械学習には一切興味が出なかった人だったので、実務的にはどんなケースがあるのか、エンジニアとしてどのように解決するのかを学ぶのには良い入り口だったと思います。

メッセージング基盤のNATSを超ざっくり解説する

システム間を疎結合に保ちつつ非同期で通信しようとすると、必然的にメッセージング基盤が必要になります。

もしメッセージング基盤を手っ取り早く使うのであれば、クラウドベンダーのサービス、例えばAmazon SNSGoogle Cloud Pub/Subを使うのが一番です。(自分で環境を立てなくて済むならその方法を選択した方がいいというのが自論です)

ただし、手元に環境を用意したり接続用のロールやサービスアカウントを作るのは面倒です。

OSSはどうでしょうか?メッセージングやストリーミング処理のデファクトスタンダードとなっているKafkaという素晴らしいプロダクトがあります。

ただし、耐障害性もありスケーラビリティを実現するためにZookeeperの複雑性を受け入れなければいけないので、これまた面倒臭いです。(もちろん、Amazon MSKやConfluent Cloudのようなマネージドサービスを使う手もあります)

aws.amazon.com

では、もっと簡単にPub/Subスタイルのメッセージングを自前で用意できないのかと探して見つけたのがNATSでした。

nats.io

詳しいところは技術書典7に出す同人誌で書こうと思いますが、 NATSは何が特徴で何が他よりもすごいのか、頭の整理のためにもブログ記事として書き記しておきます。

Subject-based messaging

メッセージのやりとりは全てSubjectを通じて行います。

KafkaやAmazon SNSのようなPub/Sub型のメッセージングに慣れた方はメッセージの購読にTopicやSubscriptionを作成したでしょう。 驚くことにNATSには TopicやSubscriptionがありません 。PublisherとSubscriberの間に立つ登場人物は Subjectただ1つです

PublisherはSubjectを指定してメッセージを配信し、同じSubjectを購読しているSubscriberにNATSがメッセージをルーティングしてくれるのです。 「NATSは(メッセージ)キューではなくルータ」であると主張している方もいます。

Flexible Subject

全てのメッセージのやりとりはSubjectを通じて行うと書きましたが、配信先は1つに限定されません。 Subjectにはワイルドカードを使えます。

ワイルドカードにも2種類あり、特定の1階層のみ全てのメッセージを受け取る * (アスタリスク)と、特定の階層以下全て階層向けのメッセージを受け取る > (不等号) があります。

例えば、time.* というSubjectを購読するSubscriberは、Publisherが time.ustime.jp に向けて配信したメッセージのどちらからも受け取れます。ただし、time.jp.tokyo のようなさらに下の階層までSubjectが指定されてメッセージが配信され場合はSubscriberはメッセージを受け取れません。 time.> というもう1つのワイルドカードを使ったSubjectならば受け取ることができます。

Kafkaに慣れた方ならば、1Topicに対して複数のSubscriptionがぶら下がり、これらのSubscription全てをSubscribeしている状態をイメージしていただくと良いです。

3style messaging

メッセージングのスタイルにも3種類あります。

  • Pub/Sub
  • Request/Reply
  • Queue Group

Pub/Subを拡張してメッセージの返信(Request/Reply)もできることが特徴です。 AWSだと最近Temporary Queueというものができましたが、やっていることはこれと近いですね。

aws.amazon.com

また、Queue Groupのようなロードバランサーの仕組みがあるのも特徴ですね。

Zero Configuration

設定なしにメッセージングを始められるのも特徴です。 例えば以下のようなことを実現するのに設定は全く不要です。サーバーやクライアントを構成したら自動的に設定されます。

  • Subjectにメッセージを配信するための設定
  • Subjectをからメッセージを購読するための設定
  • SubjectからSubscriberへのメッセージ配信のロードバランシングの設定
  • 災害対策のためのNATSサーバークラスタ冗長化

Simple Client

クライアントも実装が楽チンです。クライアントライブラリ自体が30以上の言語の実装があり、インターフェイスもシンプルであるためです。 Node.jsのクライアントライブラリである nats.js を例にとると、以下のようにしてメッセージの配信・購読が可能になります。

github.com

Pub/Sub messaging with NATS

全てのメッセージがSubjectを通じてやりとりされるため、クライアント側も実装が簡単です。見た目もスッキリしていますね。

Deliver AT MOST ONCE

NATSはメッセージを1回しか配信しません。(At Most Once戦略)

これはディスクやDBのような2次ストレージにメッセージを保存していないためです。 裏を返すとメッセージを管理する手間が省けるのでシンプルではあると言えます。(物は言いようですね) この特性のため、受け取りに失敗した or 購読する前に配信されたメッセージを再送する手段がありません。

その一方でKafkaは稼働しているマシンのディスクにメッセージを書き込んでいるため、始点を指定してメッセージを再送できます。

また、ストリーミング処理のための拡張実装であるNATS StreamingやLiftbridgeはこの問題を解決して最低1回(At least once)配信できるようになります。

github.com

www.infoq.com


もう少し深掘りしたことや本番運用に向けたNATSのクラスタリング・認証認可の話は技術書典7で出す本をお楽しみに!!

AWSによるサーバーレスアーキテクチャ 読了後感想

サーバーレスアーキテクチャのまとまった書籍ないかなと探して見つけた一冊を読んでみたので、感想を含めてメモ書きです。

AWSによるサーバーレスアーキテクチャ

AWSによるサーバーレスアーキテクチャ

TL;DR

Youtubeクローンのサービス作成を通して、サーバーレスアーキテクチャでサービスを作るためのアーキテクチャ、考え方、コンポーネントの使い方と実装・テストといった設計からサービスの公開までの一連の要素を学べます。

特にアーキテクチャの原則や各章で説明されているコンポーネントの組み合わせ・実装はAWSに限らず汎用的に適用できるものだと手を動かしながら感じました。 それと個人的にはちゃんとテストを書こうって書いてあったのは好印象。

また、サーバーレスのコンポーネントとしてAWSのサービスを多用していますが、類似したサービスは他のクラウドベンダーやOSSにも存在するので置き換えて作ることもできそうです。

以下、この書籍からピックアップして所感を書き連ねます。

サーバーレスアーキテクチャの原則

この書籍で一貫して適用される原則です。具体的には以下の5つになります。

  1. オンデマンドでコードを実行するために、サーバーを自分で立てるのではなくコンピューティングサービスを使う
  2. 目的が1つでステートレスな関数を作る
  3. プッシュベースのイベント駆動パイプラインを設計する
  4. より厚く強力なフロントエンドを作る
  5. サードパーティサービスの活用する

1つのアプリケーションサーバーで実現していたことをサーバーレスアーキテクチャで実装するにはどれも重要な原則です。

(2)を突き詰めると(4)や(5)も自然と実施するようになるので、それぞれ独立した原則ではなく相互関係があるように思えます。

(3)のイベント駆動パイプラインはAPI Gatewayから接続されるようなサービスではなく、非同期サービス側だけで使うアーキテクチャ・実装に見えます。ですが、背後にこのようなアーキテクチャのパイプラインがあることを意識すると、同期的に処理するAPIインターフェイスや、APIとパイプラインの接続部分の実装が変わります。

サーバーレスのコンポーネント

この書籍で使用したコンポーネントは以下の通りです。

  • AWS Lambda ... Computing Service
  • Amazon S3 ... Storage and Event
  • Auth0 ... OAuth style authentication
  • Amazon SNS ... Notification
  • Amazon SES ... Email
  • Amazon CloudWatch ... Monitoring
  • Amazon CloudTrail ... Auditing
  • Amazon API Gateway ... Relate API with Lambda
  • Firebase Realtime Database ... Realtime datastore
  • AWS Step Functions ... State machine

こうやって使ったコンポーネントを見ると、サービスをたくさん使いました。 下の参考記事みたいにサーバーレスのサービスのアーキテクチャServiceful Serverlessと表現するのは的を得ていますね。

--> サーバーレスによる大変革

標準的だったり汎用的な機能はクラウドベンダーもしくはサードパーティーのサービスを使い、カスタムコードは単一の目的に特化した最低限だけ書く。そしてそのカスタムコードはサービスを繋ぐ(=グルーコードにする)。カスタムコードをいかに書かずに実現させるかを考えるのはゴリゴリコードを書くのとはまた違った楽しさがあって良いです。

Good Point

  • 実際にコードを書きながら学べる

    • これは言わずもがなですが、ただ読んでいるだけではなく手を動かしながらの方が理解できますね。
  • 自然とクラウドネイティブなサービスを作れる

    • まるでCNCFの定義のようなクラウドネイティブなアーキテクチャに結果的になっています。「管理しやすい」については疑問が残りますが、他の要素については概ね満たしていますね。
    • スケーラブルなサービスである
      • Lambdaやイベント駆動パイプラインを使用することで、特定部分がボトルネックにならない!
    • コンポーネントが互いに疎結合となっている
    • 耐障害性がある
      • 特定の部分が失敗したり応答不能になっても、他のサービスには影響しない。
    • 可観測性が高い
      • CloudWatch Metrics/LogsやCloudTrailを使うことでそれぞれのコンポーネントの状態を把握できるようになっている。
  • 上記の原則に沿って、サーバーを意識しない/自分で管理せずサービスをどのように作るのか明確であった。

    • 自分でEC2やベアメタルサーバーを立てずにLambdaを使う、フロントエンドを厚くする、サードパーティーサービスを活用して自分でコードを書かずに機能を実現できるといった具合です。
  • エラーハンドリングについてアーキテクチャ的解答があった。

    • 単純に実装としてDLQを使いましょうで済ませてないです。なんでDLQを使わないといけないんだっけ?の理由が自分の中ではフワッとした理解に留まっていたので、この解答には膝を打った。
      • (Why) マイクロサービスになるにつれて全てのサービスのトランザクションをまとめ上げるプロセスが存在しなくなるため、一連の処理の一部が失敗した場合のデータの整合性担保が難しくなる。
      • (What) アーキテクチャとしては、エラー通知のトピックとエラー処理サービスを立ててロールバックを実行する。
      • (How) 通知の具体的なコンポーネントとして、SNS/SQSを使ったDLQを使用する。

Bad Point

上記のように良書ではあるものの、残念 or ここは改善できそうというポイントもありました。

  • CI/CDパイプラインを作らず全てコマンド手動実行

    • package.jsonにパッケージングやデプロイ用のコマンドを書いてまとめてはいるものの、いちいち自分で実行しないといけないのは効率が悪いです。
    • 画像処理のためにffmpegを入れるLambdaのデプロイパッケージのZIPファイルは40MB近くあるため、ネットワークによってはupdate-function-codeが失敗します。これがすごいストレスです。S3にアップロードしてからコマンドを叩けばデプロイ可能ですが、AWS SAMやServerless Frameworkを使ったCI/CDパイプラインを作ればストレスフリーかつ時短になったはず。
    • 折角クラウドサービスを使ってるんだから、手作業でボトルネックができちゃうのは勿体無い点です。
  • 使用するサービスが古い

    • 面白みに欠けるという意味ではなく、すでにdeprecatedになっている機能や代替サービスの使用を前提としていたことがあって何度か心が折れそうでした笑
    • Auth0は認証用のウィジェットとして組み込みのUIではなく、Auth0がホストするUI(Universal UI)の使用が推奨されていました。こっちの方がライブラリ管理が少なくてベターに感じます。
    • Firebase Realtime Databaseの代わりに(beta版ではありますが)Firestoreを利用が最近は多いようです。
    • 認証は委譲トークン(Delegation Token)を使用する前提で記載されていましたが、この手法による認証方法は廃止されていました。
  • 長時間処理するバッチコンピューティングの利用や、既存のサーバーを使ったレガシーアプリケーションとの共存・マイグレーションについて言及がなかった。

    • 実際には一からサーバーレスで作ることは(特にエンタープライズ系のサービスだと)少ない訳ですし、サーバーありのサービスとの共存や割り切り方については何かしら意見が欲しかったところです。
    • 例えばAWS BatchやEMRを使うケース、ECRを併用するケースってどう切り分けているのかしら...??とか。

まとめ

上記のようにGood/Badポイントは双方あるもののが、総じてサーバーレスアーキテクチャと実装を理解し、イメージするには良書でした。 単にAWS Lambdaを使おうに止まらず、サービスを作るために必要なコンポーネントやそれぞれの役割、原則について非常によくまとまっていたので、これからサーバーレスに挑む人には是非おすすめしたい書籍です。

せっかくなので、IBM Cloudや最近認定を取ろうと目論んでいるGCP、もしくはOSSをふんだんに使ったケースでも同じ or さらに発展させられないか試してみようと思います。


以上。

ブラウザからREST APIでAmazon S3にファイルをアップロードするときに気をつけること

書籍『AWSによるサーバーレスアーキテクチャ』(翔泳社)を読み進めているときにS3のファイルアップロード関連でつまづいたところが多々あったので、気をつけるべきところとして手順と一緒にまとめます。

AWSに関わる動的な処理を全てJavaScriptで実装することを選択した場合、AWS Lambda上ではAWS SDK for JavaScript一択ではありますが、ブラウザの場合は生のJavaScriptで記述することも可能です。

今回はブラウザ側で実行されるS3関連のコードをSDKを使わずREST APIで扱った時のメモです。 (書籍に従って生JSでゴリゴリ書きましたけど、潔くSDKを使えばこういったところで躓かなかったのではと思わずにはいられない。)

www.shoeisha.co.jp

やること

  • 署名付きURLを発行する。
  • バケットのアクセス権限を設定する。
  • REST API経由でリクエストを投げる。

署名付きURLを発行する

AWS S3 署名バージョン4でリクエストする

  • ファイル名(オブジェクトのキー)、バケット名、有効期限を使って署名付きのURLを発行します。このとき、署名バージョン4でリクエストします。
    • V4以外は受け付けません。明示的に指定した方がいいかも。
    • ここはLambda + API Gatewayで実装すると扱いやすい。

AWS regions created before January 30, 2014 will continue to support the previous protocol, Signature Version 2. Any new regions after January 30, 2014 will support only Signature Version 4 and therefore all requests to those regions must be made with Signature Version 4

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/sig-v4-authenticating-requests.html

以下、コード例。

"use strict";

const AWS = require("aws-sdk");
const S3 = new AWS.S3({
    signatureVersion: 'v4'
});
const crypto = require("crypto");

function getSignedUrl(fileName) {
    const prefix = crypto.randomBytes(20).toString("hex");
    const params = {
        Bucket: process.env.UPLOAD_BUCKET,
        Key: prefix + "/" + fileName,
        Expires: 600
    };
    return S3.getSignedUrl("putObject", params);
}

module.exports = {
    getSignedUrl
};
  • オペレーション名を一緒に付与すること

    • オペレーションを指定するときは、putObjectとキャメルケースで指定すること。PutObjectと先頭大文字で指定するとエラーで弾かれる。
    • 公式ドキュメントには上記は明記されていないが、Sampleにはちゃっかりキャメルケースで指定されている。
  • ここで返却される署名付きURLは仮想ホスト形式で返されます。

    • http://{bucket-name}.s3.amazonaws.com
    • http://{bucket-name}.s3-aws-region.amazonaws.com

バケットのアクセス権限を設定する

バケットポリシー

まずはパブリックアクセス権限が付いていたら外します。 今回は署名付きURLでダウンロード/アップロードを行うため、リクエストの度にリクエスト者に権限が付与される仕組みにするためです。

バケットを選択 -> アクセス権限 -> バケットポリシーを開いてドキュメントを削除する。バケット一覧で該当のバケットに"非公開"と記載されていればOK。

常に最小限の権限を付与することが推奨されているため、不用意にパブリックアクセスを許可することは回避した方が良いです。

CORS設定

AWSとは別ドメインからアクセスすることになるので、どこから/どんなオペレーションが許可されるかバケットに設定する必要があります。 バケットを選択 -> アクセス権限 -> CORSの設定から以下のように設定を追加します。これだけ。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>POST</AllowedMethod>
        <AllowedMethod>PUT</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

あと地味なTipsとしては、1行目の<?xml...の記載は設定のエディタに書かなくても勝手に追加されます。

REST API経由でリクエストを投げる

PUTリクエストで投げる。

オブジェクトの新規作成をするんだからPOSTリクエストじゃね?と直感的に思いつきますが、PUTリクエスで投げます。POSTメソッドでリクエストを投げてもAccess Deniedと403が寂しく返ってくるだけです。 思い返してみれば、オペレーション名はPutObjectだもんね。

おそらく同一キーのオブジェクトに対しては上書きもできることから、POSTメソッドではなくPUTメソッドのリクエストにしているのではと推測します。 --> REST API Reference: PUT Object

Content-Typeはファイルのものを指定する

画像や動画をHTMLから送信する場合の定石だとContent-Type: multipart/form-dataでデータをバックエンドに送信しますが、S3へのアップロードに関しては単一ファイルの場合はmultipartリクエストを行うのは間違いです。 ファイル本体のContent-Typeを指定しましょう。

例えば、MP4の動画をアップロードする場合は、Content-Type: video/mp4といった具合です。

"use strict";

/**
 * ファイルをS3バケットにアップロードする。
 * @param {string} url アップロード先S3バケットの署名付きURL
 */
function upload(url) {
    // MP4の動画を添付したと想定。
    const uploadBtn = document.getElementById("uploadButton");
    const file = uploadButton.files[0];

    // S3バケットにアップロード実行。
    $.ajax({
        url: url,
        type: "PUT",
        data: file,
        processData: false,
        contentType: "video/mp4"
    })
    .done(response => {
        console.log(response);
        alert("Upload Finished!");
    })
    .fail(err => {
        console.log(err);
        alert("Failed to upload...");
    });
}

実はこれもちゃっかり公式ドキュメントに記載されています。日本語版にはないですけどね。 以下のExample2にJPEG画像の例が参考になります。 https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/RESTObjectPUT.html?shortFooter=true#RESTObjectPUT-responses-examples


ブラウザに含めるJSファイルのサイズを気にしてSDKを入れられない場合などに参考にしていただければと思います。

以上。

技術書典6 Knative本 読了後感想

実はタイトルの技術書展6には実は行けなかったのですが、 気になっていた本があったので思わずBoothでポチって読んで、手を動かした感想です。

感想を書く対象の本はこちらになります。 『Knativeの歩き方 KubernetesからServerlessを訪ねて』

toshi0607.booth.pm

感想

まず50ページ弱の文字通り薄い本なので、かなり読みやすいです。 構成は最も理解しやすいであろう順序で書かれているので、頭から読んで苦になりません。技術書として最高です。

特に、Serving --> Build --> Build + Serving --> Eventing + Servingの流れは素晴らしい。Serving + Buildのあたりなんか、もう明日には使えそう。

また、プラットフォーム依存の説明が少ないので、他のプラットフォームでも応用させやすい本ですね。

今やk8sはどのプラットフォームもサービスとして提供してるから当然ではあるのだが、GKE特有の説明もさらっと終わらせるので良い。公式にインストール方法が書かれているので、IBM CloudのIKS、AWSのEKS、AzureのAKS、Openshiftでも同様に動かせるはずです。

さらに言うと、説明が丁寧!読者を置いてけぼりにせずスラスラ読めます。

KnativeはCRD(Custom Resource Definition)を使って独自のKubernetesオブジェクトを作成することで様々な機能を拡張してますが、各CRDごとの役割を省かず丁寧に説明されています。独自の用語ばかりだとついていけなくて積み本行きになりがちですが、全くそんなことなかった。

Knativeについて思うこと

正直な話、コンテナ周りって扱うのつらさを感じるんですよね。便利なのは使ってて分かりますけど。 コンテナ単品だとランタイムやアプリをラップしているだけだし、Kubernetesでコンテナオーケストレーションすると今度はクラスタの管理やYAMLの記述に圧殺される。ネットワークやサービスメッシュは別プロダクトのIstioやLinkerdを使わないといけない。

こんなもどかしさを抱えているときに、「開発に集中する」ことを実現できるKnativeはかなり魅力的なプロダクトですね。 コンテナをビルドできる、そのままデプロイしてサービスを公開できる、イベントドリブンな構成も1つのプラットフォームでできてしまうのは驚き。 HTTPベースだけじゃなく、イベントを扱えるところも今までにない。

ただ、build, deploy, and manage modern serverless workloads と、サーバーレスを自称しているのどういうことだろうと考えこんでしまったのも事実。 なぜなら、今までServerless = FaaSという認識を持っていたから。CNCF Serverless Whitepaper v1.0でもFaaSを重点的に取り上げるくらいですしね。 思うに、文字通りプラットフォーム(インフラ構成やら、クラウドプロバイダーがどれやら、どの言語を使っているやら) = サーバーを意識させないところなんだろうな。もしくはプラットフォームの制約を受けないところ。

「開発に集中する」ことを実現している点では、開発者側から見ると既存のPaaS~FaaSと変わらないです。 CloudFoundryやHerokuで体験したコードを書いてコマンド一発でアプリができるという素晴らしい体験を、コンテナ・Kubernetesでオープンに実現しようというのがKnativeなんじゃないかなとこの本を読みながら考えさせられました。


以上。

Node.jsアプリでもAngularみたいにDIしながらアプリを作る

AngularではDependency Injection(DI)で自分で定義したServiceを各コンポーネントで使えるようにできます。Javaの世界でもSpring Frameworkを筆頭に、インターフェイスに対してどのような実装を紐づけるかをDIで実現しています。

ただ、、、サーバーサイドやCLIでNode.jsのアプリを作るとなると途端にDIをすることって少ないですよね。TypeScriptで型やインターフェイスを手に入れたのにClassをいちいちnewしたりimportするのは辛い...とても辛い!!

このポストではInversifyJSを使ってサーバーサイドのNode.jsのアプリでDIを行います。

概要

実現したいこと

  • Node.jsアプリでDIを使ってInterfaceと実装クラスを疎結合にする。
  • 生のJSではなく、TypeScriptを使ってClassとInterfaceの恩恵を受ける。
  • 題材はCLIアプリケーションとする。
  • InterfaceへのDIだけでなく、実装クラスのDIについてもやってみる。

題材

最近お仕事で自分でやったことを例とします。題材とシチュエーションは以下の通りです。ちょっとした自動化アプリです。

  • Java(Spring Framework)のWebAPIを作っています。
  • 入力チェックのテストコードを全項目網羅して正しく入力チェックエラーを返すことを確認したい。
  • 手で書いて目Grepで過不足がないか確認するのは効率がよくない&何かしら見落とすリスクがあるので自動生成する。
  • この自動生成をCLIインターフェイスとしたアプリケーションとして実装する。

InversifyJS

今回のように、JavaScript/TypeScriptでDIを実現するのに不可欠なライブラリです。

github.com

正体はTypeScriptで作られたIoCコンテナ(=DIコンテナ)のライブラリ。TypeScriptで使うことが推奨されています。

また、最近のTypeScriptの流れなのか、アノテーションを使っています。@Inject@InjectableといったJavaでもお馴染みのやつを使えます。 詳細はGitHubのREADME.mdを読んでいただければと。

実践

完成形

最初に最終形をお見せします。 ファイル・ディレクトリは以下の通りになります。

CLIのエントリーポイントがsrc/generator.tsです。 各ディレクトリはクリーンアーキテクチャもどきで構成しています。各ディレクトリと格納クラスの役割は以下の通りです。

Interfaceを定義する

まずはController、UseCase、RepositoryのInterfaceを定義します。全部書いているとキリがないので一部だけ。

import { CreateScriptRequest } from "./CreateScriptRequest";
import { CreateScriptResponse } from "./CreateScriptResponse";

export interface ICreateScriptUseCase {
  generate(request: CreateScriptRequest): CreateScriptResponse;
}

実装は何も入れていません。どのようにこのgenerate関数が実現されるかは実装クラスに委ねられます。そして使用する側は実装クラスではなく、Interfaceにのみ依存します。

実装クラスを書く

Interfaceを実装します。前述した通り、JavaのInjectアノテーションが出てきますね。 Interfaceに実装クラスを注入する対象は @Inject() 、他のクラスで注入させたい対象は @Injectable() を使用すればあとはただのアプリです。

import { ICreateScriptUseCase } from "./UseCaseInterface";
import { CreateScriptRequest } from "./CreateScriptRequest";
import { CreateScriptResponse, CreateScriptResponseDetail } from "./CreateScriptResponse";
import { CaseScriptListBlg } from "../business/CaseScriptListBlg";
import { CaseScriptBlg } from "../business/CaseScriptBlg";
import { injectable, inject } from "inversify";
import { TYPE } from "../common/Types";

/**
 * スクリプト生成ユースケースの実装クラス
 */
@injectable()
export class CreateScriptInteractor implements ICreateScriptUseCase {
  private readonly _listBlg: CaseScriptListBlg;
  private readonly _scriptBlg: CaseScriptBlg;

  constructor(
    @inject(TYPE.CaseScriptListBlg) listBlg: CaseScriptListBlg,
    @inject(TYPE.CaseScriptBlg) scriptBlg: CaseScriptBlg
  ) {
    this._listBlg = listBlg;
    this._scriptBlg = scriptBlg;
  }

  /**
   * スクリプト生成を実行する.
   */
  generate(request: CreateScriptRequest): CreateScriptResponse {
    // テストケースのリストを生成
    const caseList = this._listBlg.createCaseList(request);
    // ケース名を元にテストケーススクリプトを生成
    this._scriptBlg.generate(request, caseList);

    // レスポンス生成
    return this._createResponse(request.getOutputFilePath(), caseList.length);
  }

  private _createResponse(path: string, num: number): CreateScriptResponse {
    let detail = new CreateScriptResponseDetail();
    detail.setCaseName(path);
    detail.setCaseNum(num);
    return new CreateScriptResponse("OK.", [detail]);
  }
}

実装クラスと紐づける

最後に、inversify.config.ts にInterfaceと実装クラスの紐付けを定義します。ここで紐付けはこのクラスでしか行いません。 to()で実装クラスを紐づけるのですが、toSelf()なる関数があるらしい。(使い方は分からない...)

import "reflect-metadata";
import { Container } from "inversify";
import { TYPE } from "./Types";
import { IBaseController } from "../controller/IBaseController";
import { CreateScriptController } from "../controller/CreateScriptController";
import { ICreateScriptUseCase } from "../usecase/UseCaseInterface";
import { CreateScriptInteractor } from "../usecase/CreateScriptInteractor";
import { CaseScriptBlg } from "../business/CaseScriptBlg";
import { CaseScriptListBlg } from "../business/CaseScriptListBlg";
import { ICaseListRepository, IScriptRepository } from "../repository/RepositoryInterface";
import { CaseListRepository } from "../repository/CaseListRepository";
import { ScriptRepository } from "../repository/ScriptRepository";

const container = new Container();

// Controller
container
  .bind<IBaseController>(TYPE.CreateScriptCtrl)
  .to(CreateScriptController);

// UseCase
container
  .bind<ICreateScriptUseCase>(TYPE.CreateScriptUseCase)
  .to(CreateScriptInteractor);

// Business Logic
container.bind<CaseScriptListBlg>(TYPE.CaseScriptListBlg).to(CaseScriptListBlg);
container.bind<CaseScriptBlg>(TYPE.CaseScriptBlg).to(CaseScriptBlg);

// Repository
container.bind<ICaseListRepository>(TYPE.CaseListRepo).to(CaseListRepository);
container.bind<IScriptRepository>(TYPE.ScriptRepo).to(ScriptRepository);

export { container };

そして使いたいクラスをDIコンテナから取り出せば完成!

import { container } from "./common/inversify.config";
import { IBaseController } from "./controller/IBaseController";
import { TYPE } from "./common/Types";
const controller = container.get<IBaseController>(TYPE.CreateScriptCtrl); // <-- Here!!

まとめ

InversifyJSを使うと、Node.jsのアプリでもDIやるのは結構簡単ですね。 今回はCLIアプリで一からコードを書きましたが、Webアプリでフレームワークを使うときも同じように書けるはず。

Serverless FrameworkでKotlin & LambdaのサーバーレスAPIを作る

朝活.Kotlinで作ったServerless Framework × Kotlin × LambdaのサーバーレスAPIまとめです。

justincase.connpass.com

serverless.com

概要

  • 題材

    • 松屋のメニューの写真をひたすら貼るSlack Bot. (ちょっとしたジョークアプリです)
  • 上記のSlackBotのバックエンドをLambda関数で実装する.

    • API管理: API Gateway
    • ロジック: AWS Lambda <--ここをKotlinで書いた
  • CI/CDファーストを目標に、なるべくも手作業を減らしてビルド&デプロイを簡単にする.

やったこと

まずアプリケーションを作成するときに悩むのが、ボイラープレートコード(フレームワーク等で必要になる典型的なコード)の扱い。 ビジネスロジックを書くことに集中できるのがサーバーレスアプリケーションの強みの1つなので、LambdaとAPI Gatewayの接続部分や、エントリーポイントのHandlerクラスはなるべく書きたくないところ。

実はServerless Frameworkのテンプレートから雛形は自動生成可能です(プラットフォームによってはない)。 sls create --helpを打って調べる限り, 使えるテンプレートは3つありますね。KotlinでJS使うテンプレートなんてあるのか...

それでは雛形をコマンドラインから作成。事前にnpm i -g serverlessでserverlessコマンドは入れておきましょう。今回は"aws-kotlin-jvm-maven"を使います。

serverless create \
    --template aws-kotlin-jvm-maven \
    --path matsuya-finder \
    --name matsuya-finder

5秒くらいでテンプレートは作成されます。指定した--path以下は以下のようなディレクトリ構成になっています。

.
 |- pom.xml
 |- serverless.yml
 |- src
    |- main
       |- kotlin
          |- com
             |- serverless
                |- ApiGatewayResponse.kt
                |- Handler.kt
                |- Response.kt
  • ApiGatewayResponse.kt ... Lambda --> API Gatewayにレスポンスを返す時のデータの格納クラス。弄らずにそのまま使う
  • Handler.kt ... アプリケーションのエントリーポイント。ビジネスロジックはここにInjectする。
  • Response.kt ... アプリケーションのレスポンス。data classになっている。
  • serverless.yml ... Serverless Frameworkに管理されたアプリケーションのデプロイ定義。裏ではCloudFormationを使っている。

あとは業務ロジックのコードを好きに書きます!

最終的なコードは以下の図のような構成になりました。 アプリのコードが重くなければ、KtorやJavaliなどのWeb Application Frameworkを使うよりもスッキリしてますね!

f:id:accelerk:20190323235930p:plain
ディレクトリ構造 最終形

また、serverless.ymlは以下の構成になっています。このファイルが置かれている場所で sls deploy を発行すればデプロイ完了です。

service: chom-matsuya-finder
  
# Base Definition
provider:
  name: aws
  runtime: java8
  stage: dev
  region: ap-northeast-1

# Packaging information
package:
  artifact: target/matsuya-finder-dev.jar

# Individual Function Definitions
functions:
  main:
    handler: net.ponzmild.Handler
    name: matsuya-finder
    description: Fetch today's Matsuya menu.
    events:
      - http:
          method: post
          path: matsuya

Tips

  • Serverless Frameworkのコマンドを打つときは, 権限のあるプロファイルを使おう

    • IAMロールをつけたのになぜかUnauthorizedとなってしまうことありますが、だいたいはIAMロールをつけたユーザーでCLIを叩いてないことが原因です。
    • (使用例) sls deploy --aws-profile your-serverless-agent
  • そのままデプロイすると統合リクエストのタイプが"Lambda Proxy"になる

    • このタイプはリクエストのコンテントタイプを細かく指定しづらいです。
    • また、雛形として自動生成されるApiGatewayResponse.ktはLambda Proxyタイプ用のレスポンス定義です!これ以外のタイプは別途レスポンスのクラスを用意しましょう。
    • serverless.ymlintegration:lambdaを指定するか、Handlerクラス内でパースのどちらかで解決しなければなりません。
    • 今回は後者にしています(以下のコード)
class Handler : RequestHandler<Map<String, Any>, ApiGatewayResponse> {
    override fun handleRequest(input: Map<String, Any>, context: Context): ApiGatewayResponse {
        // API GatewayのイベントのJSONからリクエストボディのみ取り出す
        val inputJsonString = input.get("body") as String

        // 全て文字列なので, Mapに詰め替える
        var inputBodyMap: HashMap<String, String> = hashMapOf()
        val kvStrings = inputJsonString.split("&")
        kvStrings.forEach {
            val kv = it.split("=")
            inputBodyMap.put(kv[0], kv[1]) // 詰め替え
        }

        // And more...
    }
}

Kotlinも結構簡単にサーバーレスなAPIを作れるんですね。他にもS3のイベントやCloudWatch Eventsにも対応できるのでできる幅も広そう。 また、Androidユーザーはフロントエンドと同じ言語を使えるからいいですね。iOSユーザーのためにSwiftでもできるのかな?

以上。