(レポート) DVO209: JAWS: 高度にスケーラブルなサーバーレスフレームワーク #reinvent

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、虎塚です。

「DVO209 - JAWS: The Monstrously Scalable Serverless Framework – AWS Lambda, Amazon API Gateway, and More!(日本語タイトル: JAWS: 高度にスケーラブルなサーバーレスフレームワーク)」を聴講したので、レポートします。

発表は、Austen Collins (@austencollins) さんとRyan Pendergast (@rynop) さんです。

背景

サーバーレスAWSとは何か

サーバーレスとは、サーバーのことを考えなくてよいアーキテクチャです。AWSには次のようなサービスがあります。

  • AWS Lambda: イベント駆動のコンピュータリソース
  • Amazon API Gateway: AWS Lambdaで利用できるREST APIを構築できる

サーバーレスAWSとは、AWS LambdaとAmazon API Gatewayを組み合わせて実現します。これによって、開発者はPaaS/BaaSをつなぐ仕事から解放されます。

AWS LambdaはAWSクラウドの中心となる可能性を秘めている」とはjanakiram MSVの言葉です。

なぜサーバーレスであるべきか

スケールが容易だから

  • AWS Lambdaはコンテナ内で動作するが、開発者がコンテナを扱わなくてよくなる!
  • コンテナのオーケストレーション/オートスケールが提供される
  • Amazon API GatewayはDDoSとRate Limiting(応答頻度の制限)が提供される
  • アプリケーションログとメトリクスの分散がビルトインされている

コストが安いから

たとえば、次のようなリクエストを処理するシナリオを考えてみましょう。

シナリオ: 16000リクエスト/日 (平均200ms) = 3,200,000 ms/日

  • 2台のEC2で処理した場合: $2.97/日
    • 2台とも1年リザーブドインスタンスを使用すると仮定
    • 可用性のためMulti-AZ構成にすること、大量にリクエストが来た場合のバーストなどを考慮して、さらに余分のサーバが必要になる
  • AWS Lambdaで処理した場合: $0.05/日
    • 1024MBのメモリを利用、高可用性とバーストは提供される
    • 実行時間100msあたりの費用は$0.0000016

このように、従来EC2を使っていたインフラをAWS Lambdaにスイッチすることで、ドラスティックな節約が可能になります。

サーバーレスなワークフロー自体の魅力

  • アプリケーションを分離できるだけでなく、エンドポイントも分離できる
  • マルチコンテナへのコードデプロイが提供される
  • 新しい環境やリージョンに、あっという間にプロビジョンできる
  • DevOpsタスクもできるだけ少なくできる

しかし、サーバーレスはアナーキー

たくさんのAWS Lambdaを使うと何が起きるでしょうか。大量のAWS Lambdaを使うということは、大量のコンテナを使うことを意味します。

複数のバージョン、複数の環境にまたがるデプロイ、複数のリージョンにまたがるデプロイ……といった問題が発生します。そして、そのような状況でもAWSリソースはセキュアに保たなければなりません。

JAWSとは

JAWSとは、AWSサービスを使ったサーバーレスのWeb/モバイル/IoTアプリケーションを開発するための無償のOSSフレームワークです。JavaScriptで実装されています。

ソースコードはgithubでホストされていて、4,400スターを集めています(re:Invent後には、4,700スターに至っていました)。

2ヶ月前(2015年8月)に登場した時には、Hacker Newsでも大変話題になりました。(c.f. Show HN: JAWS – A JavaScript and AWS Stack | Hacker News

JAWS=CLIです。JAWSで作成したアプリケーションは、要するにAWS Lambdaの集合です。JAWSは、ざっくりしたサーバーレスプロジェクトのための構造、自動化、最適化を提供します。

Version 1 リリース

セッションと相前後してversion 1がリリースされました。V1では次のことが意図されました。

  • サーバーレスなフレームワークの地ならしをすることと、一般にAWSでアプリケーションを作るためのフレームワークを作ること
  • AWSベストプラクティスにしたがいAWS上のタスクを自動化して、学習曲線をなだらかにし、新しく使いはじめる人にもAWSをより使いやすいものにする
  • JAWSを採用する人たちが、AWSのリージョンや環境をまたぐプロダクションベースで使えるようにするとともに、弱点にも習熟してもらう

デプロイされたJAWSプロジェクトの詳解

JAWSでは、開発ステージ、テストステージ、プロダクションステージというステージ(環境)の概念があります。JAWSのプロジェクトは、2つのリージョンにまたがって作成されます。プロジェクト内で共有されるデータはS3に配置されます。

  • JAWSはプロジェクトを作成するためのCloudFormationテンプレートを用意しています
  • CloudFormationスタックを各環境/リージョンにデプロイすることで、プロジェクトを完全にレプリケートできます
  • サーバーレスプロジェクトの完全な分離とレプリケーションが可能です

デモ: jaws-frameworkのインストール

npmを使ってjaws-frameworkをローカルマシンにインストールします。

% sudo npm install jaws-framework -g
[...]
% which jaws
/usr/local/bin/jaws

デモ: 新規プロジェクトの作成

jawsコマンドを使って新規プロジェクトを作成します。

% jaws project create
       ____   _____  __      __  _________
      |    | /  _  \/  \    /  \/   _____/
      |    |/  /_\  \   \/\/   /\_____  \
  /\__|    /    |    \        / /        \
  \________\____|__  /\__/\__/ /_________/ v1 (BETA)

       *** The Server-Less Framework ***

JAWS: Enter a project name:  (jaws-4JehtY4eg)
JAWS: Enter a project domain (You can change this at any time:   (myapp.com)
JAWS: Enter an email to use for AWS alarms:  (you@yourapp.com)
JAWS: Enter a stage for this project:  (dev)
JAWS: Select a region for your project:
  > us-east-1
    us-west-2
    eu-west-1
    ap-northeast-1
JAWS: Select an AWS profile for your project:
  > default
    [...]
JAWS: Creating CloudFormation Stack for your new project (~5 mins)...
JAWS: Preparing your runtime and installing jaws-core module...
npm WARN package.json {project-name}@0.0.1 No license field.
jaws-core-js@0.0.2 node_modules/jaws-core-js
└── dotenv@1.2.0
JAWS: Your project "{project-name}" has been successfully created in the current directory.

JAWSではCloudFormationを使うため、project-nameにはキャメルケースの使用が推奨されています。

CloudFormationスタックが作成されるのに5分ほどかかります。この作業はリージョンごとに1回だけ実行すればよく、今後のデプロイはもっと高速に実行できます。

CloudFormationスタックの作成が完了すると、ローカルには次のようなファイルとディレクトリの階層が作成されます。(ちなみに、セッションのデモではWebStormで表示されていました)

% tree .
.
`-- {project-name}
    |-- README.md
    |-- admin.env
    |-- aws_modules
    |-- cloudformation
    |   `-- dev
    |       `-- us-east-1
    |           `-- resources-cf.json
    |-- jaws.json
    |-- lib
    |-- node_modules
    |   `-- jaws-core-js
    |       |-- README.md
    |       |-- awsm.json
    |       |-- env
    |       |   `-- index.js
    |       |-- node_modules
    |       |   `-- dotenv
    |       |       |-- Contributing.md
    |       |       |-- README.md
    |       |       |-- config.js
    |       |       |-- dotenv.png
    |       |       |-- lib
    |       |       |   `-- main.js
    |       |       |-- package.json
    |       |       `-- test
    |       |           |-- config.js
    |       |           `-- main.js
    |       `-- package.json
    |-- package.json
    `-- tests

14 directories, 17 files

プロジェクト直下にあるadmin.envファイルのADMIN_AWS_PROFILEで、利用するAWSプロフィールを設定できます。

jaws.jsonの確認

ローカルのプロジェクトルート/jaws.jsonファイルを確認します。

{
  "name": "{project-name}",
  "version": "0.0.1",
  "location": "https://github.com/...",
  "author": "",
  "description": "",
  "domain": "{project-domain}",
  "stages": {
    "dev": [
      {
        "region": "us-east-1",
        "iamRoleArnLambda": "arn:aws:iam::123456789012:role/dev-{project-name}-r-IamRoleLambda-XXXXXXXXXXXX",
        "iamRoleArnApiGateway": "arn:aws:iam::123456789012:role/dev-{project-name}-r-IamRoleApiGateway-XXXXXXXXXXXXX",
        "jawsBucket": "jaws.dev.useast1.{project-domain}"
      }
    ]
  }
}

上記のjsonが示すとおり、AWS環境には次のような要素が作成されています。プロジェクト作成時に指定したproject-nameとproject-domainが、AWSリソースの名前に使用されます。

  • IAM Role
    • dev-{project-name}-r-IamRoleApiGateway-XXXXXXXXXXXX
    • dev-{project-name}-r-IamRoleLambda-XXXXXXXXXXX
  • IAM Group
    • dev-{project-name}-r-IamGroupApiGateway-XXXXXXXXXXXXX
    • dev-{project-name}-r-IamGroupLambda-XXXXXXXXXXX
  • IAM (inline) Policy to Roles & Groups
    • dev_-{project-name}-_api-gateway
    • dev_-{project-name}-_lambda
  • S3バケット
    • jaws.dev.useast1.{project-domain}

JAWSプロジェクト構造の紹介

JAWSでは、AWSM(amazon web services modules)と呼ぶモジュールを使ったモジュラーアプローチをとっています。これはアプリケーションの構築にAWSリソースを使うように強制するものです。

AWSMとは

  • aws-module
    • 特定のタスクのための1つまたは複数のAWS Lambdaです
    • AWS Lambdaがサポートするすべての言語をサポートします

JAWSを使うと、aws-moduleの管理にコマンドを利用できます。ツールをエコシステムから自分のプロジェクトにインストールできます。

awsm.jsonは、AWS Lambda、Amazon API GatewayやCloudFormationテンプレートのスニペットに依存する他のAWSリソースからなります。

JAWSは、インストール時にCloudFormationで生成されるリソースを、ユーザのプロジェクトに追加します。インストール、カスタマイズ、デプロイが簡単にできます。

デモ: AWSモジュールの作成

jawsコマンドでの作成

aws-moduleを使ってみましょう。ここでは、Quick Start用のgreetingsモジュールを作成します。

% cd {project-name}
% jaws module create greetings hello
JAWS: Successfully created greetings/hello

先ほどJAWSプロジェクトを作成した時に作成されたaws_modulesディレクトリの下に、greetings/helloディレクトリが新規に作成されました。

% tree .
.
|-- README.md
|-- admin.env
|-- aws_modules
|   `-- greetings
|       |-- awsm.json
|       `-- hello
|           |-- awsm.json
|           |-- event.json
|           |-- handler.js
|           `-- index.js
[...]

npmインストール

npmを使って、既存のAWS LambdaコードをAWSモジュールとしてインストールすることもできます。ここでは、サンプルとして提供されているawsm-imagesモジュールをインストールします。

% npm install awsm-images --save
npm WARN package.json {project-name}@0.0.1 No license field.
-
> awsm-images@0.1.0 postinstall /PATH/TO/JAWS-PROJECT/node_modules/awsm-images
> jaws postinstall awsm-images npm

JAWS: Copying /PATH/TO/JAWS-PROJECT/node_modules/awsm-images/awsm to /PATH/TO/JAWS-PROJECT/aws_modules/awsm-images
JAWS: Merging in Lambda IAM Policy statements from awsm
JAWS: Merging in CF Resources from awsm
JAWS: Successfully installed awsm-images
JAWS:  WARN  This aws module uses env vars MAKE SURE to run jaws env list to see which ones need to be set
awsm-images@0.1.0 node_modules/awsm-images
├── shortid@2.2.2
├── bluebird@2.10.2
├── gm@1.20.0 (array-series@0.1.5, array-parallel@0.1.3, debug@2.2.0)
├── request-promise@0.4.3 (chalk@1.1.1, lodash@3.10.1, request@2.65.0)
└── aws-sdk@2.2.9 (xmlbuilder@0.4.2, xml2js@0.2.8, sax@0.5.3)

上記の実行ログのとおり、aws_modulesディレクトリの下にaws-images/thumbnailディレクトリが作成されました。

|-- aws_modules
|   |-- awsm-images
|   |   |-- awsm.json
|   |   `-- thumbnail
|   |       |-- awsm.json
|   |       |-- event.json
|   |       |-- handler.js
|   |       `-- index.js

デモ: AWSモジュールの構造

awsm-imagesモジュールのawsm.jsonを確認します。

% cat aws_modules/awsm-images/thumbnail/awsm.json
{
  "lambda": {
    "envVars": [
      "IMAGE_RESIZE_BUCKET",
      "JAWS_DATA_MODEL_STAGE"
    ],
    "deploy": true,
    "package": {
      "optimize": {
        "builder": "browserify",
        "minify": true,
        "ignore": [],
        "exclude": [
          "aws-sdk",
          "gm"
        ],
        "includePaths": [
          "node_modules/awsm-images/node_modules/gm",
          "node_modules/awsm-images/node_modules/aws-sdk"
        ]
      },
      "excludePatterns": []
    },
    "cloudFormation": {
      "Description": "",
      "Handler": "aws_modules/awsm-images/thumbnail/handler.handler",
      "MemorySize": 1024,
      "Runtime": "nodejs",
      "Timeout": 6
    }
  },
  "apiGateway": {
    "cloudFormation": {
      "Type": "AWS",
      "Path": "images/thumbnail",
      "Method": "GET",
      "AuthorizationType": "none",
      "ApiKeyRequired": false,
      "RequestTemplates": {
        "application/json": "{\"url\":\"$util.urlDecode($input.params('url'))\"}"
      },
      "RequestParameters": {
        "integration.request.querystring.integrationQueryParam": "method.request.querystring.url"
      },
      "CacheNamespace": "String",
      "CacheKeyParameters": [],
      "Responses": {
        "default": {
          "statusCode": "200",
          "responseParameters": {
          },
          "responseTemplates": {
            "application/json": ""
          }
        }
      }
    }
  }
}

envVarsには、モジュールが実行時に必要とする環境変数が定義されています。また、コードのパッケージをminifyするかどうかといった設定があります。

デモ: 環境変数の設定

モジュールに必要な環境変数を定義します。次のコマンドで、設定済みの環境変数を確認できます。

% jaws env list dev us-east-1
JAWS: Getting ENV file from S3 bucket: jaws.dev.useast1.{project-domain} in us-east-1
JAWS: ENV vars for stage dev:
JAWS: ------------------------------
JAWS: us-east-1
JAWS: ------------------------------
JAWS_STAGE=dev
JAWS_DATA_MODEL_STAGE=dev
[...]

jaws envコマンドでは、プロジェクトのS3バケットにある.envファイルを取得/更新します。

たとえば、先ほどのawsm.jsonに定義されていたとおり、awsm-imagesでは、環境変数IMAGE_RESIZE_BUCKETが必要です。同じく必要なJAWS_DATA_MODEL_STAGEは、すでに定義されていましたが、IMAGE_RESIZE_BUCKETはまだないようです。そこで、次のようにenv setコマンドで設定します。

% env set dev us-east-1 IMAGE_RESIZE_BUCKET {BUCKET-NAME}

コマンドを実行すると、S3の.envファイルに環境変数IMAGE_RESIZE_BUCKETが追記されます。jaws env listコマンドで定義済みの環境変数をもう一度取得してみると、追加した環境変数が表示されます。

デモ: モジュールのデプロイ

次のコマンドを利用すると、JAWSダッシュボード (CLI) を利用して、AWS Lambdaやエンドポイントをデプロイできます。

% jaws dash
JAWS: Dashboard for project "myProject"
 -------------------------------------------
 Project Summary
 -------------------------------------------
    Stages:
       dev us-east-1
    Lambdas: 2
    Endpoints: 2
 -------------------------------------------
 Select Resources To Deploy
 -------------------------------------------
    awsm-images/thumbnail
      L) lAwsmImagesThumbnail
      E) /images/thumbnail - GET
    greetings/hello
      L) lGreetingsHello
      E) /greetings/hello - GET
    - - - - -
  >   Deploy Selected -->
JAWS: -------------------------------------------
JAWS:  Dashboard:  Deploying Lambdas...
JAWS: -------------------------------------------
JAWS: Lambda Deployer:  Packaging "lGreetingsHello"...
JAWS: Lambda Deployer:  Saving in dist dir /var/folders/w1/wxm71x750d33lvn14yc9615h0000gn/T/lGreetingsHello@1444662549730
JAWS: Getting ENV file from S3 bucket: jaws.dev.useast1.{project-domain} in us-east-1
JAWS: Lambda Deployer:  Bundled file written to /var/folders/w1/wxm71x750d33lvn14yc9615h0000gn/T/lGreetingsHello@1444662549730/bundled.js
[...]

「Select Resources To Deploy」の「L」はAWS Lambdaを、「E」はエンドポイントを表します。「L) lGreetingsHello」と「E) /greetings/hello - GET」の両方を選択状態にして、「Deploy Selected」の行でEnterを押すことで、デプロイが開始されます。

デプロイ中は、上記のようにコマンドラインでログを見ることができます。AWS Lambdaコードを保存して圧縮した後、AWSへのアップロードやS3の設定取得をおこない、エンドポイントが出力されるまでの様子を確認できます。

Quick Startのhelloモジュールをデプロイすると、「https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/」というエンドポイントが返されます。このURI末尾に「greetings/hello」をつけてアクセスすると、JSONでレスポンスが返ります。

{"message":"Your JAWS lambda executed successfully!"}

なお、レポートを省略しますが、awsms-imagesモジュールを使って画像をリサイズするデモも行われました。

JAWSでのAWS Lambdaファンクションのデプロイ

AWS Lambdaでは、cold startのため起動時コストがかかります。JAWSでは、AWS Lambdaにビルトインされている最適化を使って、レスポンスタイムをいくらか短くなるように工夫されています。

AWS Lambdaのデプロイプロセスは、次のとおりです。

  • AWS Lambdaコードをパッケージングし、最適化する(minifyなど)
  • 圧縮してタイムスタンプをつけたAWS LambdaコードをS3に配置する
  • AWS LambdaのためのCloudFormationテンプレートを、上記のS3キーを含めてアップデートする
  • AWS Lambdaをデプロイするために、CloudFormationスタックを必要なregion/環境に対してアップデートする

JAWSでのエンドポイントのデプロイ

なお、Amazon API Gatewayはパワフルですが、新しいサービスのため、CloudFormationでまだサポートされていません。

JAWSでは、CloudFormationのシンタックスを真似て自前のAPI Gateway SDKをビルトインし、ユーザがREST APIを構築する手助けをしています。

最適化のtips

  • AWS Lambdaは、ユーザのモジュールに対する薄いラッパーであるべきで、ユーザコードは再利用性、テスト容易性、AWS独立性を保つべきです
  • デプロイするコードのフットプリントはできるだけ小さくしましょう
  • モジュールはLambdaファンクションのハンドラーの外側に置きましょう
  • 稀なアクセスしかないとAWS Lambdaのメモリが増大し、多くのトラフィックがある時はほどほどになります。AWS Lambdaが常に暖機されているようにしましょう

大事なビジョン

  • JAWSプロジェクトをすぐに作ることができること
  • あらかじめ用意された大量のaws-moduleから必要なものを選択して、開発者が素早くアプリケーションを作れること
  • すべてのファンクションは一度デプロイしてしまえば、あとは簡単にアップデートできること
  • すべてのリージョンにデプロイすること(ぜひそうしましょう。サーバーレス以前よりもずっと安いのだから)
  • Amazon API Gatewayのレイテンシーベースのルーティグに期待

サーバーレスアプリケーションは、これらによって可能な限り安く、そして高度にスケーラブルになります。

おわりに

JAWSフレームワークは公開されて間がない上に、今流行のサーバーレスを支援する仕組みということで、セッションにはプログラマと思しき参加者が大勢つめかけていました。

API Gatewayに対応するための謎の独自実装(CloudFormationがAPI Gatewayに対応するまでのワークアラウンドだと思いますが)や、最適化まわりなど、わからないことがまだまだありますが、これを機に触ってみようと思います。

それでは、また。