Serverless Framework と TypeScript で Hello & Version の Web API を実装

前回サーバーレスなアプリケーションのひな形を作りました。
今回は、ひな形で作られたAWS Lambdaのソースを Hello World にし、また新しくアプリケーションのバージョンを返す Version Web API を追加します。

シリーズの記事

環境
本記事の開発環境は以下となります。

  • Windows 10 64bit
  • Visual Studio Code
  • Node.js 9
  • TypeScript 2.7
  • Serverless Framework 1.26.0

主な実行環境について、また全部稼働まではいきませんが以下を想定しています。
※ Amazon API Gateway などは、Serverless Framework が自動設定するので省略、明示的に使うものとしてリストになります。

  • Amazon S3
  • AWS Lambda

ベースとなるソースコード

前回の続きとなりますので、こちらhttps://github.com/riotz-works/samples-hello-serverless/tree/0.0.1をもとに追加開発します。

本記事と一緒に作成する場合は前回のソースもしくはタグを指定してチェックアウトしたソースをお使いください。

1
2
3
c:\Temp\nlog>git clone --depth 1 -b 0.0.1 https://github.com/riotz-works/samples-hello-serverless.git
Cloning into 'samples-hello-serverless'...
(省略)

Hello Web API の改修

Serverless Frameworkのボイラープレートで生成した Lambda のプログラムはhandler.jsで以下のようになっています。
これを Hello World の Web API へ改修するにあたり、このままでresponsebodyを変えるだけでよさそうです。今回はmessage'Hello Serverless !!'にし、inputは削除することにします。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { APIGatewayEvent, Callback, Context, Handler } from 'aws-lambda';

export const hello: Handler = (event: APIGatewayEvent, context: Context, cb: Callback) => {
const response = {
statusCode: 200,
body: JSON.stringify({
message: 'Go Serverless Webpack (Typescript) v1.0! Your function executed successfully!',
input: event,
}),
};

cb(null, response);
}

そしてプロジェクトのディレクトリ直下で汎用的な名前のhandler.jsだと、このあと追加する Version Web API の配置などで困ります。
まずはディレクトリ配置を見直し、Lambda のハンドラー・ファンクションのソースが入っていることがわかるようにsrc/handler配下に置おきます。
続いて Hello Web API のハンドラーに対応したファンクションのファイル名としてgreetings.tsとすることにします。

ファイル名と配置を変えたので Lambda の定義をしているserverless.ymlを修正します。
helloファンクションのハンドラーはhandler.helloと定義されていましたが、greetingsファクションsrc/handler/greetings.helloハンドラーに変更します。

Hello Web API ができました。
この時点でserverless deployコマンドを使いデプロイできます。(–aws-profile [profilename] を忘れずに)

Version Web API の作成

Hello Web API が作れましたがボイラープレートのままだったので、新しく Web API を追加します。
とりあえず、アプリケーションのバージョンを返す Version Web API を作ることにします。
仕様は「HTTP GET /versionpackage.jsonversionを返す」にします。

まずは、Lambda のハンドラーのソースを配置するsrc/handlersystems.tsを作ります。
他にシステム関連の Web API の想定はありませんが、システム関連 Web API のハンドラーを配置するsystems.tsversionのハンドラーを追加するイメージにしました。

コードは以下のようにします。(このコードを使う場合は、1行目の Copyright は書き換えてください)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* Copyright 2018 Riotz.works. */
import { APIGatewayEvent, Callback, Context, Handler } from 'aws-lambda';
import { version } from '../../package.json';

/**
* System Web API's AWS Lambda handler function.
*
* @param event – event data.
* @param context – runtime information of the Lambda function that is executing.
* @param callback – optional callback to return information to the caller, otherwise return value is null.
* @see http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html
*/
export const handle: Handler = (event: APIGatewayEvent, context: Context, cb?: Callback): void => {

const response = {
'body': JSON.stringify({
'version': version
}),
'statusCode': 200
};

if (cb !== undefined) {
cb(null, response); // tslint:disable-line
} else {
context.succeed(response);
}
};

主なキー・ポイントは以下になります。 先の Hello Web API と若干異なる部分 (context.succeed()とか) がありますが詳細は後日の記事として、基本的には変わりありません。

  • 3行目で、バージョンのもとになるpackage.jsonsystems.tsの相対パスで指定しversion要素をインポートします
  • 17行目で、インポートしたversion属性の値をレスポンス・ボディとしてセットします

Visual Studio Code の環境によってはエラーが出ないかもしれませんが、3行目のインポート文でエラーにあります。

何も表示されていなくてもserverless packageを実行するとエラーが出力されます。

1
2
3
4
5
C:\Temp\samples-hello-serverless> serverless package
Serverless: Bundling with Webpack...
(省略)
ERROR in C:\Develop\repos\riotz\samples-hello-serverless\src\handler\systems.ts
(3,25): error TS2307: Cannot find module '../../package.json'.

これはpackage.jsonのモジュール定義が存在しないためになります。
このように型定義などを確認してくれるのが TypeScript のありがたいところです。
一方で今回のは若干無茶な実装でした。標準モジュールには存在しませんし、適切なモジュール定義をとってくることも難しいでしょう。さりとてpackage.jsonと何か別ファイルでversionのダブルメンテはしたくないです。。。 ということで、モジュール定義を追加することにします。
ソースディレクトリのsrctypes.d.tsを追加します。.d.tsファイルでしたらとくに命名規則はなくtyping.d.tsもよく見かけます。

コードは以下のようにします。(1行目の Copyright は書き換えてください)

1
2
3
4
5
6
7
8
9
10
/* Copyright 2018 Riotz.works. */

/**
* Module declaration for JSON file.
*/
declare module '*.json' {

/** version element of package.json using Version API. */
const version: string | undefined;
}

*.jsonなので、モジュール定義が見つからなかった JSON ファイルの型定義になります。
その中にversionが文字列として存在することを定義しますが、これはすべてての JSON ファイルに当てはまることではないので、型定義はstring | undefinedとしドキュメントで注釈しました。
頑張るとpackage.json固有のモジュール定義もできたのですがversionを取得するだけにしては複雑になりすぎたので、このあたりを落としどころとしました。

最後にserverless.ymlへ Version Web API の Lambda 定義を追加します。
functions: greetings:の下に追加します。

1
2
3
4
5
6
systems:
handler: src/handler/systems.handle
events:
- http:
method: get
path: version

Version Web API ができました。

デプロイ

AWS 環境へデプロイします。前回のデプロイ と 稼働テスト同様に、AWS のプロファイを参照してserverless deploy --aws-profile [profilename]を実行します。

endpointshelloversionの URL が出力されるので、それぞれブラウザでアクセスすると、プログラムした JSON が返ってきます。

ソースコード

今回作成した部分までのソースを GitHub へアップしました。
https://github.com/riotz-works/samples-hello-serverless/tree/0.0.2


Hello Web API はボイラープレートのレスポンスを改修しただけだったのでとくに大きな変化はありませんでした。一方で Version Web API は新しく追加となりましたし、両方のプログラムを配置するためにプロジェクトの構造も変化させました。

Hello と Version で実装レベルが異なっている部分はありますが、これについては次の記事で確認しながら合わせこみをしていきます。できれば TypeScript のコンパイラ設定や Lint にまでふれたいです。

Serverless Framework と TypeScript でサーバレス開発事始め

前回の記事から時間が空いてしまいましたが、ちょうど新しいプロダクトの開発を始めるところなのでServerless FrameworkTypeScriptで開発する際の事始めについてまとめます。
例によって Hello World になりますが、こんな感じで作り始めをお伝えします。

シリーズの記事

環境
本記事の開発環境は以下となります。

  • Windows 10 64bit
  • Visual Studio Code
  • Node.js 9
  • TypeScript 2.7
  • Serverless Framework 1.26.0

主な実行環境について、今回は準備なので全部稼働まではいきませんが以下を想定しています。
※ Amazon API Gateway などは、Serverless Framework が自動設定するので省略、明示的に使うものとしてリストになります。

  • Amazon S3
  • AWS Lambda

想定アプリケーション

ウェブサイトで、アクセス時に JavaScript で Web API を呼び出し “Hello World” の挨拶を取得します。 まずは動作させるところを目標にシンプルな形で進めます。

ウェブサイトはAmazon S3に HTML を配置し静的ウェブサイトのホスティングを使います。
本格稼働するにはAmazon CloudFrontAmazon Route 53などが必要となりますが、まずは稼働を目指し S3 だけで進めます。

Web API はサーバーレスで作りたいのでAWS Lambdaを使います。
インスタンスやコンテナーの管理が不要で、純粋にアプリケーションの開発やメンテに集中できるのサーバーレスにこだわりたいです。
こちらも本格稼働にあたってはAmazon API Gatewayにカスタム・ドメインを割り当てたりしますが、まずは稼働を目指し自動割り当てで進めます。

開発環境の構築

コーディングにためのエディターを用意します。こちらは手慣れたもので大丈夫です。今回は Visual Studio Code を使いました。
こちらhttps://code.visualstudio.comから取得できます。

続いて Node.js をインストールします。こちらhttps://nodejs.orgからダウンロードしてインストールします。今回は 9.5.0 Current をインストールしました。

プロジェクトの作成

Node.js がインストールできたら、プロジェクトのディレクトリを作成します。
今回はC:\Temp\hello-serverlessをプロジェクトのディレクトリとしました。

続いて Serverless Framework を 導入します。今回は Visual Studio Code の ターミナルを使っていますが、コマンド プロンプト からも同様のコマンドになります。
今回はプロジェクトごとに Serverless Framework を 導入したいので、-gなしでnpm install serverlessとしました。

1
2
3
4
C:\Temp\hello-serverless> npm install serverless
(省略)
+ serverless@1.26.0
added 302 packages in 44.148s

Serverless のモジュールが導入され、node_modulespackage-lock.jsonが作られています。

Serverless の AWS/TypeScript ボイラープレートからプロジェクトのひな形を作ります。
※ 今回はプロジェクト・ローカルにServerless Framework を導入しているためnode_modules\.bin\serverlessコマンドになります。node_modules\.binにパスを通しておくか、めんどくさい場合はnpm install -g serverless-gをつけてグローバルに入れてしまうのも手です。

1
2
3
4
C:\Temp\hello-serverless> node_modules\.bin\serverless create --template aws-nodejs-typescript
(省略)
Serverless: Successfully generated boilerplate for template: "aws-nodejs-typescript"
Serverless: NOTE: Please update the "service" property in serverless.yml with your service name

Node.js/npm プロジェクト関連のファイルが追加されます。

package.jsondevDependenciesに Serverless Framework が入っていないので、もう一度--save-devを付けてnpm install --save-dev serverlessを実行します。
※ 最初から--save-devしないのはpackage.jsonが作られているとボイラープレートが展開できないためです。

他のモジュールもインストールするため、npm installを実行します。

プロジェクトの設定

自動生成された設定が入っているので、プロジェクトに合わせた設定を行います。

package.jsonは、最小限namedescriptionauthorあたりを変更しておきます。
また、作成しているのはアプリケーションなのでモジュール公開の防止のために"private": trueを設定しておくとよいでしょう。privateの詳細はこちらpackage.json | npm Documentationをご確認ください。

serverless.ymlは、service: name:の値を変更しておきます。

tsconfig.jsonlibesnextを追加します。
2018年2月現在、これを追加しておかないとビルド時に参照しているモジュールで以下のようなエラーが発生するためです。

1
2
3
4
5
6
7
C:\Temp\hello-serverless> node_modules\.bin\serverless package

ERROR in C:\Temp\hello-serverless\node_modules\@types\graphql\subscription\subscribe.d.ts
(17,4): error TS2304: Cannot find name 'AsyncIterator'.

ERROR in C:\Temp\hello-serverless\node_modules\@types\graphql\subscription\subscribe.d.ts
(29,4): error TS2304: Cannot find name 'AsyncIterable'.

プロジェクトのビルド

serverless packageコマンドでビルドします。

1
2
3
4
5
6
7
8
9
C:\Temp\hello-serverless> node_modules\.bin\serverless package
Serverless: Bundling with Webpack...
ts-loader: Using typescript@2.6.2 and C:\Temp\hello-serverless\tsconfig.json
Time: 2085ms
Asset Size Chunks Chunk Names
handler.js 2.94 kB 0 [emitted] handler
handler.js.map 3.18 kB 0 [emitted] handler
[0] ./handler.ts 350 bytes {0} [built]
Serverless: Packaging service...

TypeScript がコンパイルされ、JavaScript とソースマップが作られ、.serverlessディレクトリに AWS CloudFormation のテンプレートと Lambda のデプロイ Zip が作られます。

デプロイと稼働テスト

ビルドに成功したらserverless deployコマンドで AWS 環境へデプロイします。
デプロイにあたっては AWS IAM ユーザー のアクセス・キーが必要となります。あらかじめ作成アクセス・キーを取得し、%USERPROFILE%\.aws\credentials(e.g.C:\Users\username\.aws\credentials) に記述しておきます。
IAM のアクセス権はPowerUserAccessでは足りず IAM 周りの権限追加が必要です。まだ絞り込みきれておらず結局AdministratorAccessを使ってしまっています。頑張らないと。。。

1
2
3
4
[hello]
region = us-west-2
aws_access_key_id = ZDEJREC4D1GM3DXXXX
aws_secret_access_key = ce4FH4Dcyh5DGh2dDGG4hDk6DYfhDAjrFhXXXX

デプロイを実行します。
serverless deploy --aws-profile hello--aws-profilecredentials[]で指定したプロファイル名を指定します。

1
2
3
4
5
6
C:\Temp\hello-serverless> node_modules\.bin\serverless deploy --aws-profile hello
(省略)
endpoints:
GET - https://ca3kg0hrXX.execute-api.us-east-1.amazonaws.com/dev/hello
functions:
hello: hello-serverless-dev-hello

無事にデプロイできると自動生成された API Gateway のエンドポイントが出力されます。

ブラウザでアクセスすると、handler.tsの実装の通り「Go Serverless webpack (TypeScript) v1.0! Your function executed successfully!」のメッセージと HTTP リクエストの内容が表示されます。

お掃除

serverless removeで AWS 環境をクリーンアップできます。

1
2
3
4
5
6
7
C:\Temp\hello-serverless> node_modules\.bin\serverless remove --aws-profile hello
Serverless: Getting all objects in S3 bucket...
Serverless: Removing objects in S3 bucket...
Serverless: Removing Stack...
Serverless: Checking Stack removal progress...
..........
Serverless: Stack removal finished...

ソースコード

今回作成した部分までのソースをGitHub へアップしました。
https://github.com/riotz-works/samples-hello-serverless/tree/0.0.1


初期設定に多少の作業がありましたが、Serverless Framework を使うことで簡単にデプロイとクリーンアップができました。

アプリケーションとしては Lambda でメッセージを返すレベルではありますが、インフラ面を考えると、これをしべて AWS マネジメントコンソールではかなりの手間がかかります。それを Serverless Framework が補ってくれるので、サーバレス開発のインフラ運用が削減できるメリットに加えて、インフラ構築も格段と楽になるのではないでしょうか。

また Try & Error を繰り返すうちに AWS 環境がついつい散かってしまい、よくわからないリソースを放置されるケースが出てきたりします。開発環境とはいえ Serverless Framework で環境を作るようにすると片付けも簡単ですね。

引き続き、このプロジェクトに設定やコードを加えてもう少し発展させていきます。

CSV ファイルのバッチ連携も AWS Lambda でサーバーレス

この記事はServerless Advent Calendar 2017の 7日目になります。

Serverlessconf Tokyo 2017で発表させていただく機会をいただき、Java チームが選択した TypeScript による AWS Lambda 開発のタイトルでお話をさせていただきました。

今回はそのシステムの裏手側、CSV ファイルを連携するバッチをサーバーレスで実現したアーキテクチャについてご紹介します。

システム全体の概略図

ざっくりと下図のようなシステムで AWS 上に作られています。
スマートフォンなどから IoT 機器を操作するようなクラウド・サービスがあり、そのバックグラウンド・プラットフォームになります。認証や機器のデータ管理を行うシステムです。
このシステムは、図のAmazon API GatewayAWS Lambdaが、ペアになっているアイコン毎が1つ1つのマイクロサービスとして作られています。つまり 10を超えるマイクロ・サービスの集合体になります!

今回の話は、この図の右上部分 “Traditional DC”、企業の伝統的なデータセンターからデータを受ける部分です。
この部分は伝統とシキタリにのっとり CSV ファイルを受け取り、処理する仕組みが必要となります。

※ CSV の C は Comma と信じてましたが、Character や Column 説 (e.g.Wikipedia) があり広義には Comma に限らず特定の文字でデータを区切るファイルらしい。どうでもいいけど。。。

なぜ AWS Lambda ?

このようなシステムを AWS で実装するには、AWS BatchAWS Glueなどを使います。しかしながら、今回は Lambda で実装することにしました。

背景としては、以下のような点があります。

  • 少人数の DevOps チームであり、AWS のマネージド度合いが高いほど嬉しかった
  • データ量や処理量から想定してコスパが優れていた
  • 1回に連携するデータ量が Lambda の処理時間におさまる可能性が持てた

とくに2番目のコストについては、Glue に対して圧倒的によかったのが大きかったです。今回の処理は ETL に近いとはいえ、DWH 連携ではなく、DB のマスター・データ登録だったので Glue を活かしきれない点がありました。

なお Lambda の実行ランタイムは Java になります。
(Serverlessconf Tokyo 2017 の発表資料の通り、Java チームなので)

CSV ファイル連携のアーキテクチャ概要図


“Traditional DC” からは、Amazon S3に CSV を置いてもらうことにします。FTP/SFTP とのリクエストがありましたが、サーバーレス厨とは言わずとも、ここは S3 へのアップロードへお願いしたいところ、何としてでも承諾いただきます。(いただきました)

S3 からはAmazon SNSへイベント通知をし、Lambda を起動します。その Lambda で CSV をパースして DynamoDB へデータを登録します。

複雑なデータの加工や外部の API/データを参照するような場合は、DynamoDB Streams から Lambda を呼び出して後続の処理として実装します。このようにすることで、データの登録に特化させて処理数を稼ぐことができ、また DynamoDB Streams の後続処理は、登録されたデータ1件ごとの処理になるので時間的な余裕が生まれます。

ここでのポイントは S3 ⇒ SNS からの Lambda は DynamoDB への登録だけに特化し、他の処理をしないことです。このようにすることで、なるべく CSV ファイルの対応に処理を振り分けるようにし、時間内で処理できる量を増やすようにしています。

処理能力とアーキテクチャの改良

このような形にすることで、CSV を Lambda で処理できるようになります。
このアーキテクチャの処理能力は、CSV ファイル 1行のデータ量や、カラム数によって変わってくる部分がありますが、今回のケースでは 1ファイルで約 800行のレコードが処理できました!
て、まったく処理できてません。。。

これでは役に立たないので対応が必要となります。
Lambda は設定するメモリの使用量によって処理速度が変わるという話もあり、最大に設定してみましたが設定による差はとくにありませんでした。ファイルを読み込みながらの順次処理なので、ここでの処理性能はあまり変わらないのでしょう。また DynamoDB へアクセスするオーバーヘッドも処理性能では大きく変わらないはずです。

では、どうするのか。


S3 に置かれたファイルを DynamoDB へ登録する前に、ファイル分割する処理をはさみます。
1回の Lambda で DynamoDB へ登録できるのは余裕を見て 500件までとして、CSV ファイルを 500行ごとのファイルに分割して S3 へ再アップロードします。その 500行ごとのファイルを Lambda で DynamoDB へ登録する形にしました。

これにより、30,000行まで処理できるようになり現実的なラインまで持っていけるようになりました。S3 からの Lambda に変わりはないので、サーバーレスの形態も維持できます。

まとめ

サーバーレスでシステムを作ろうとしても、往々にして発生してしまう既存システムとのバッチ連携。そのために FTP/SFTP やら、CSV パースするための処理やらとどうしてもサーバ・インスタンスが欲しくなってしまい、せっかくのサーバーレスがサーバー有り(サーバーレスの反対語って何だろう)になってしまいます。

Glue に見合う処理であればよいのですが、今回のように小規模なデータ登録となるとオーバースペック気味にもなります。そんな時には S3 からの Lambda で処理することで、サーバーレスでバッチ連携をするのも手ではないでしょうか。

ポイントとしては以下になります。

  • ファイルは S3 に置いてもらう
  • S3 ⇒ SNS から Lambda を起動し DynamoDB へデータ登録に特化する
  • 1回の Lambda で処理できるデータ量は少ない、必要に応じてファイル分割の Lambda をはさむ

Serverless Advent Calendar 2017、楽しく、そして勉強になる記事がたくさんです。

昨日6日目は@RyoheiMorimotoさんのAWS LambdaをTest Runnerとして使ってみるでした。 ユーザ管理のシステムをサーバーレスで 34歳タローさんがハナコさんになれるかをテストしています。サーバーレス・テスト、おもしろいですね!

明日8日目は@narikeiさんです。何の記事か楽しみです!


では、ハッピー・サーバーレス・ライフ! ハッピー・ホリデー!!

Hello World !!

こんにちは、世界!
こんにちは、みなさん!!

わたしたちは Riotz.works です。

“a cheerful engineering team” を標榜する、わたしたちは
IT を中心に技術的なことやさまざまな挑戦が大好きで陽気で愉快なエンジニアの集団です。

これまで学んできたこと今挑戦していること、先々やってみたいことなど、をおもしろおかしく、まるでお祭り騒ぎのようにアウトプットしていきたい、そんな思いから『Articles | Riotz.works』の場を作りました。

またエンジニアにとっての最初の挨拶は、何といっても “Hello World !” ですね。
ということで最初のポストのタイトルも “Hello World !!” にしました。

この記念すべき Hello World はプログラム言語でいうと、どんな感じだろうかと「Hello worldプログラムの一覧 - Wikipedia」を眺めていたら、これかな!

1
import __hello__

Python のコードで、”プログラマーに必要な機能をすべて標準ライブラリーとして提供する「バッテリー同梱 (batteries included)」の思想を具現化するジョーク -Wikipedia」なのだとか。

さすがに「すべて」とまではいかないですが、エンジニアにとって必要なものがたくさんアウトプットされている場にしていきたいと考えています。

わたしたちのアウトプットがわずかでもお役に立てられれば、勉強させていただいているコミュニティへ少しでも還元できれば、そして新たな出会いと挑戦につながればと考えております。

どうぞ、よろしくお願いいたします!!

共有: