AWSのLambdaとAPI Gatewayを使ってサーバレス(EC2レス)なWebアプリケーションを作れるフレームワークServerless Frameworkを使ってサーバレスなサイト監視アプリを作ってみた。

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のマッピングテンプレートを利用する。

http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/models-mappings.html#models-mappings-mappings

  • $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実行時に毎回実行されることになる。

参考

所感

  • API Gateway + Lambdaのコンポーネントの数が増えると設定を含めそれらの構成を管理するのが大事になってくるので、Serverless Framewortkのようにフレームワークで構成を管理できるのは便利。
  • デプロイまでサポートしているので、Lambda FunctionやAPI Gatewayのエンドポイントの登録など自動でやってくれるの便利。
  • responseTemplatesにキーが”text/html”で値がVelocityテンプレートなデータを設定するのは可能だけど、VelociityのViewデータを文字列でJSONファイル内に記載するのはシンドイ。外部ファイルを指定できると良いなー。(AWSのコンソールからは任意の形式でテンプレート記述できるので)
  • node.jsなコード書き慣れてないので同期的なコード書いていろいろハマった。