AWSのLambdaとAPI Gatewayを使ってサーバレス(EC2レス)なWebアプリケーションを作れるフレームワークServerless Frameworkを使ってサーバレスなサイト監視アプリを作ってみた。
セットアップ
Serverless Frameworkをnpmでインストール
(事前にnpmとNode V4をインストールしておく必要がある。)
$ npm install serverless -g
以下のコマンドでプロジェクトを生成する。
$ serverless project create _______ __ | _ .-----.----.--.--.-----.----| .-----.-----.-----. | |___| -__| _| | | -__| _| | -__|__ --|__ --| |____ |_____|__| \___/|_____|__| |__|_____|_____|_____| | | | The Serverless Application Framework | | serverless.com, v0.4.2 `-------' Serverless: Initializing Serverless Project... Serverless: Enter a name for this project: (serverless-eyxuls) プロジェクト名を入力 Serverless: Enter a universally unique project bucket name: (serverless-eyxuls) プロジェクトで使用するS3のバケット名を入力 Serverless: Enter an email to use for AWS alarms: (me@serverless-eyxuls.com) アラームの通知先のメールを入力 Serverless: Enter the ACCESS KEY ID for your Admin AWS IAM User: Admin権限を持つIAMユーザのアクセスキーを入力 Serverless: Enter the SECRET ACCESS KEY for your Admin AWS IAM User: ↑のIAMユーザのシークレットアクセスキーを入力 Serverless: Select a region for your project: ↓から使用するAWSのリージョンを選択 us-east-1 us-west-2 eu-west-1 > ap-northeast-1 Serverless: Creating stage "dev"... Serverless: Creating region "ap-northeast-1" in stage "dev"... Serverless: Creating your project bucket on S3: serverless.ap-northeast-1.serverless-web-monitor... Serverless: Deploying resources to stage "dev" in region "ap-northeast-1" via Cloudformation (~3 minutes)... Serverless: Successfully deployed "dev" resources to "ap-northeast-1" Serverless: Successfully created region "ap-northeast-1" within stage "dev" Serverless: Successfully created stage "dev" Serverless: Successfully initialized project "serverless-web-monitor"
入力情報を元に、CloudFormationを使ってdev環境のAWSリソース(S3のバケットやIAM Roleとか)が作成される。
プロジェクトの構成
createコマンドで生成されたプロジェクトは↓のファイル構成になっている。
.env .gitignore README.md admin.env package.json s-project.json s-resources-cf.json _meta |__resources |__s-resources-cf-dev-apnortheast1.json |__variables |__s-variables-common.json |__s-variables-dev.json |__s-variables-dev-apnortheast1.json
Serverless Frameworkに関するファイルは↓
- .env
この設定ファイルのstageを変更すると変更したstageにデプロイされるように誤解するかもしれないけど、ローカルのテストのみで使われるファイル。
※ stageやregionを変更する際は、serverlessのenv setコマンドを使うこと。
(.gitignoreに記載されており、バージョン管理の対象外) - admin.env
プロジェクト作成時に入力したアクセスキーとシークレットアクセスキーが保存されている。
(.gitignoreに記載されており、バージョン管理の対象外) - s-project.json
プロジェクトの設定と著作者の情報を設定 - s-resources-cf.json
CloudFormationのテンプレートファイル - _meta
stage、region毎のCloudFomationのテンプレートや変数ファイルが保存されているディレクトリ。
(デフォの.gitignoreには記載されていないけど、ドキュメントでは記載されていると書かれているので、追記してバージョン管理の対象外にする)
ComponentとFunction
このプロジェクトのままでは何のコードも書けないので、続いてComponentを作成する。
Componentの作成
↓のコマンドでcomponentを追加する。
$ serverless component create sites Serverless: Installing "serverless-helpers" for this component via NPM... Serverless: ----------------- serverless-helpers-js@0.1.0 node_modules/serverless-helpers-js └── dotenv@1.2.0 Serverless: ----------------- Serverless: Successfully created new serverless component: sites
Functionの作成
$ serverless function create sites/show Serverless: Successfully created function: "sites/show"
するとプロジェクトに下記構成が追加された状態になる。
ComponentとFunctionの粒度は、RESTでいうリソース=Component、アクション=Functionの粒度で作成する。
今回はWebサイトの監視を行うツールを作成するのでリソース=Siteとし各アクション(show,create,index,delete)をfunctionとして追加すると↓のような構成になる。
sites |__lib |__index.json |__node_modules ... |__create |__event.json |__handler.js |__s-function.json |__delete |__event.json |__handler.js |__s-function.json |__index |__event.json |__handler.js |__s-function.json |__show |__event.json |__handler.js |__s-function.json |__package.json |__s-component.json
各Functionのファイル
- s-function.json
API Gatewayのエンドポイントの設定情報と対応するLambda Functionの設定情報
※ デフォルトで生成されるファイルはmemorySizeが1024MBになってるけど、そんなに使うこと無ければ最小の128MBとかでもOK。 - handler.js
Lambda Functionのハンドラ - event.json
ローカルでテスト実行する際に使用するリクエストデータを設定
Viewテンプレート
API Gatewayにはリクエストやレスポンスを処理する際にデータの形式を変換できるマッピングテンプレートが用意されている。s-function.jsonのrequestTemplatesとresponseTemplatesにそれぞれContent-Typeをキーにして使用するテンプレートを指定する。このテンプレートエンジンには、Apache Velocityが使わている。
ただServerless Frameworkの場合、テンプレートはs-function.json内に記述するかComponentもしくはProject直下のs-templates.jsonに記述する必要がありいずれもJSON形式である。Velocityで記載できるのは良いのだけど、JSONの値としてViewを書くのはツラいので(外部ファイル使えたりするのか?)、今回は、ejsというJavaScriptベースの軽量テンプレートエンジンを利用して、Lambda Function内でテンプレート適用したHTMLをビルドする構成にした。
var path = require('path'), fs = require('fs'), ejs = require('ejs'); module.exports.handler = function(event, context) { var filePath = path.join(__dirname, 'このファイルのディレクトリからみたテンプレートとなるHTMLファイルのパス'); var html = fs.readFileSync(filePath, 'UTF-8'); html = ejs.render(html, {key: "value"}); context.succeed(html); return context.done(); };
handler.jsに↑なコードで、テンプレートのHTMLファイルを読み込んで、ejsでパラメータをバインドすることができる。
エンドポイントのURLの取得
REST APIを公開するアプリケーションであれば意識する必要はないが(RESTful hypermedia APIとか利用するなら必要)、Webアプリケーションで画面の遷移を伴う場合、遷移先のURLが必要になる。Lambdaからレスポンスとして返すHTML内でそのリンクを記述する必要があるのだけど、Lambda単体ではどういうURLでアクセスされてきたのかは分からない。
そういうケースでは、API GatewayのItegration Requestのマッピングテンプレートを利用する。
- $input.params().header
- $context.resourcePath
- $context.stage
を組み合わせることでURLが取得できる(相対パスであればstageだけ分かれば問題ない)。
Serverless Frameworkでは、各Functionのs-function.jsonのrequestTemplatesに上記マッピングパラメータを任意のキーにマッピングすれば、handler.js内のeventオブジェクトに対象に値がセットされた状態で受け取れる。
AWSリソースの利用
Serverless FrameworkはAPI Gateway と Lambdaを組み合わせたフレームワークだけど、Lambdaのファンクションから他のAWSサービスを利用したいケースもある(DBとか)。今回はデータをDynamoDBにストアしようと思うので、DynamoDBが利用できるようs-resources-cf.jsonに、DynamoDBを利用できるようPolicyの追加と作成するDynamoDBのテーブル情報を定義する。
{ "AWSTemplateFormatVersion": "2010-09-09", "Description": "The AWS CloudFormation template for this Serverless application's resources outside of Lambdas and Api Gateway", "Resources": { "IamRoleLambda": { ... }, "IamPolicyLambda": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyName": "${stage}-${project}-lambda", "PolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "arn:aws:logs:${region}:*:*" }, // DynamoDBを利用できるようPolicyに↓を追加 { "Effect": "Allow", "Action": [ "*" ], "Resource": "arn:aws:dynamodb:${region}:*:table/${project}-sites-${stage}" } // ここまで ] }, "Roles": [ { "Ref": "IamRoleLambda" } ] } }, // DynamoDBのテーブル情報を定義 "SitesDynamo": { "Type": "AWS::DynamoDB::Table", "DeletionPolicy": "Retain", "Properties": { "AttributeDefinitions": [ { "AttributeName": "id", "AttributeType": "S" } ], "KeySchema": [ { "AttributeName": "id", "KeyType": "HASH" } ], "ProvisionedThroughput": { "ReadCapacityUnits": 1, "WriteCapacityUnits": 1 }, "TableName": "${project}-sites-${stage}" } } // ここまで }, ... }
記述したら上記リソースをAWSの環境に↓のコマンドで反映する。
$ serverless resources deploy Serverless: Deploying resources to stage "dev" in region "ap-northeast-1" via Cloudformation (~3 minutes)... Serverless: Successfully deployed "dev" resources to "ap-northeast-1"
完了したらAWS上のDynamoDBに↑で定義したテーブルが作成されている。
s-resources-cf.jsonに定義したテンプレートを各ステージ毎に展開した結果が、_meta/resourcesと_meta/variablesにそれぞれ保存される。
デプロイ
serverless dash deployコマンドを実行すると、コンソール上でインタラクティブなデプロイができる。
$ serverless dash deploy _______ __ | _ .-----.----.--.--.-----.----| .-----.-----.-----. | |___| -__| _| | | -__| _| | -__|__ --|__ --| |____ |_____|__| \___/|_____|__| |__|_____|_____|_____| | | | The Serverless Application Framework | | serverless.com, v0.4.2 `-------' Use the <up>, <down>, <pageup>, <pagedown>, <home>, and <end> keys to navigate. Press <enter> to select/deselect, or <space> to select/deselect and move down. Press <ctrl> + a to select all, and <ctrl> + d to deselect all. Press <ctrl> + f to select all functions, and <ctrl> + e to select all endpoints. Press <ctrl> + <enter> to immediately deploy selected. Press <escape> to cancel. Serverless: Select the assets you wish to deploy: site-monitor/sites function - site-monitor/sites endpoint - site-monitor/sites@sites~GET - - - - - > Deploy Cancel Serverless: Deploying functions in "dev" to the following regions: ap-northeast-1 Serverless: ------------------------ Serverless: Successfully deployed functions in "dev" to the following regions: Serverless: ap-northeast-1 ------------------------ Serverless: site-monitor/sites (serverless-web-monitor-site-monitor-sites): arn:aws:lambda:ap-northeast-1:711951283832:function:serverless-web-monitor-site-monitor-sites:dev
デプロイ対象にfunctionを選択すればfunctionがLambda Functionとして、endpointを選択すればAPI Gatewayにデプロイされる。デプロイが完了するとendpointのURLにアクセスすればデプロイしたアプリケーションが確認できる。
(serverless function deployやserverless endpoint deployコマンドで直接デプロイも可能。)
ログの確認
Lambda Functionの実行ログは、Functionのディレクトリに移動して↓のコマンドで確認できる。(-tはtailオプション)
$ sls function logs -t START RequestId: dcb213d1-ea4c-11e5-ba13-e730e1d0921a Version: 33 END RequestId: dcb213d1-ea4c-11e5-ba13-e730e1d0921a REPORT RequestId: dcb213d1-ea4c-11e5-ba13-e730e1d0921a Duration: 13.66 ms Billed Duration: 100 ms Memory Size: 1024 MB Max Memory Used: 9 MB ...
※ serverlessはコマンドとしては長いのでslsというエイリアスが定義されている。
コード
今回作ったコードは↓
https://github.com/azuchi/serverless-web-monitor
今のところできるのはサイトのDynamoDBへの登録・確認と、登録されているサイトへの監視リクエストの発行。
今後レスポンスコードによってSNSによる通知の実装やサイトの削除など実装していく。
Plugin
その他Serverless Frameworkでは、以下のようなPluginが提供されている。
- Plugin Boilerplate
Serverless FrameworkのPluginの作成を支援するためのスタータープロジェクト。 - Serve
API GatewayをローカルでシミュレートするPluginで、API Gatewayの呼び出しを全てlocalhostに対して行う。 - Offline
Serveと同様API GatewayとLambdaをローカルでエミュレートするPlugin - Alerting
Lambda Functionに対してCloud Watchのアラームを追加するPlugin - Optimizer
Lambda Functionのサイズを削減し、実行パフォーマンスを向上させるPlugin - CORS
CORS (Cross-origin resource sharing)サポート用のPlugin - CloudFormation Validator
CloudFormationのテンプレートファイルの検証を行うPlugin - Prune
古いバージョンのLambda Functionを削除するPlugin - Base-Path
API Gatewayのエンドポイントにベースとなるパス(/apiみないな)を設定するPlugin - Test
Serverless用のテストフレームワーク - SNS Subscribe
Lambda Functionに簡単にSNS Notificationを適用するPlugin - JSHint
Lambda FunctionにJSHintを適用するPlugin - Webpack
Optimizer PluginをフォークしたPluginで、OptimizerがProduction環境のLambda Functionの最適化を行うのに対し、こっちは開発環境のLambda Functionの最適化を行うPlugin - Serverless Client
Serverlessプロジェクトのweb clientをS3のバケットにデプロイするPlugin
ベストプラクティス
Serverless Frameworkのドキュメントでは以下のようなベストプラクティスが紹介されている。
- Admin権限を持つAWSのアクセスキーを使わない
スタートガイド等ではAdmin権限を持つアクセスキーを使っているが、実際には最大でもPowerUserAccess権限までが推奨される。 - Lambdaのコードベースはできるだけ小さくする
コードサイズが小さいほどLambda実行時のコンテナは速く起動し実行される。 - Lambdaコードの再利用
全てのServerless FunctionsはComponent内のlibフォルダを利用できるため、function内で同じようなコードロジックがある場合はlibフォルダ内に集約するのがオススメ。 - CloudFormationリソースを整理する
CloudFormationのリソースはs-resources-cf.jsonで定義できる。Serverless外でプロビジョニングするのも可能だけど、コードとセットで構成も管理しておくのがオススメ。 - 外部サービスの初期化はLambdaのコード外で行う
DynamoDBのようなサービスを使う場合、その初期化はLambdaコード外で行う。(Nodeのmodule initializerやJavaのstaticコンストラクタのような。)もしDynamoDBのコネクションの初期化をLambda Function内に書くと、Function実行時に毎回実行されることになる。
参考
- 公式ドキュメント
http://docs.serverless.com/v0.4.0/docs - サンプルプロジェクト
https://github.com/serverless/serverless-starter - serverless-graphql-blog
DynamoDBをストレージとして利用してるServerless Frameworkのプロジェクト
https://github.com/serverless/serverless-graphql-blog
所感
- API Gateway + Lambdaのコンポーネントの数が増えると設定を含めそれらの構成を管理するのが大事になってくるので、Serverless Framewortkのようにフレームワークで構成を管理できるのは便利。
- デプロイまでサポートしているので、Lambda FunctionやAPI Gatewayのエンドポイントの登録など自動でやってくれるの便利。
- responseTemplatesにキーが”text/html”で値がVelocityテンプレートなデータを設定するのは可能だけど、VelociityのViewデータを文字列でJSONファイル内に記載するのはシンドイ。外部ファイルを指定できると良いなー。(AWSのコンソールからは任意の形式でテンプレート記述できるので)
- node.jsなコード書き慣れてないので同期的なコード書いていろいろハマった。