AWS Lambdaを用いてサーバーレスアーキテクチャの使い道を考えてみる

こんにちは。グルメ開発チームの齋藤です。

近年サーバーレスアーキテクチャの注目度が日に日に上昇してきています。
Microservicesの台頭とあわせて語られることも多くなったこのサーバーレスアーキテクチャ。
AWS Lambda、Google Cloud Functions、Azure FunctionsとFaaSも出揃ってきたこともあり、
今後のシステム設計においては、選択肢の1つとして十分な存在感を発揮する仕組みだと考えています。

今回はこの中からAWS Lambdaを使って、そもそもどういう事ができるのか、
どのような用途に向いているのか、と言った点を検証しつつ
実際の現行プロダクトへ取り込むことを考えた場合にどのような道があるか、をまとめてみようと思います。

サーバーレスアーキテクチャとは

そもそもサーバーレスアーキテクチャとは何か?という点に関してですが
該当するサービスがFunction as a Serviceと名付けられていることからも分かるとおり
Function、関数をサービスとして公開するイメージが一番近いのではないかと思います。
WebAPIと似たイメージがありますが、最大の違いはFaaSはインスタンスが常駐している必要がない、ということです。

WebAPIを公開しようと思うと、まずはサーバーを用意する必要があります。
運用していく上ではサーバーリソース、プロセス、エラーログ等の監視も必要でしょう。
もちろん本格運用する際には冗長構成も考えなければなりません。

一方FaaSのAWS Lambdaに関しては、こういったサーバの用意や運用が一切いりません。
開発する側としては起動トリガを指定して、後は処理を書いておくだけ。
インスタンスがないのでログ監視以外の各種監視も冗長構成も不要です。

留意点

一見万能に見えるAWS Lambdaですが、様々な制約もあります。

  • 使える言語は2016年11月時点ではPython、Node.js、Javaのみ
  • 基本的にはステートレスである
  • Function間でもコード共有は出来ない
    • 共通のロジックとかは使えない
    • ロジックそのものをLambdaファンクションにすることで共通化は可能
  • 300秒以内に完了する必要がある
    • オンラインではあまり気にならないレベルだが、バッチ処理を行う場合には要注意

利用する際には長所と短所はきちんと把握する必要がある、と言えるでしょう。

想定しうる用途

ざっと思いついた用途をあげてみます。

用途 詳細
サーバーリソース・エラーログ監視 CloudWatchのログをトリガにSlack通知
cron的な使い方 CloudWatchのスケジューラトリガで起動
bot LINE BotやFacebook Messangerなどのチャット系bot。
基本的に1つの会話につき1アクションなので、Function方式との相性は良さそう
簡易API API GatewayをトリガにしたWebAPIの提供
1リクエストごとにコネクション張ってしまう(コネクションプールのような仕組みがない)ので、RDSとの接続には向かない。使うならDynamoDB
ファイル操作 S3のファイル更新検知をトリガとして動く
例えばファイルの内容をDBに取り込む、とか。他のサーバーへ送信する、とか。

他にも多くの用途がありそうですが、ひとまず今回は簡易APIの用途で試してみたいと思います。

簡易APIを試す

APIを試すにあたって、以下のようなアプリを作ってみました。

構成図

内容としては非常にシンプルなTODOアプリです。
TODO内容を入力して登録、完了したらチェックボックスをONにする、タスクの状態はDBで保持しておく、といったものです。
本来は認証機能をもたせるべきですが、今回のスコープからは外れるため、認証無しでの処理としています。
なお、本記事ではあくまでLambdaがターゲットですので、アプリ側の詳細は省略します。

処理の流れは以下のようになっています。

No From To 処理概要
アプリ API Gateway タスク作成、状態更新、タスク取得などのイベントをトリガとして、APIをコールする
API Gateway Lambda APIに紐づくFunctionを実行する
Lambda DynamoDB 指定された処理に基づき、データ取得・作成・更新などを行う
DynamoDB Lambda 取得データ・更新結果などを返却する
Lambda API Gateway 処理結果を返す
API Gateway アプリ Lambdaから返された結果をアプリまで返却する
①’ Lambda Cloud Watch アクセス内容やコード内に埋め込んだログを出力する

では早速APIを作ってみたいと思います。

API GatewayでAPIの作成

まずはアプリから呼び出すことになるAPIを作成します。
これがなくてもAndroid / iOS向けに公開されている、AWS SDKを利用すればLambdaのFunctionをアプリから実行することは可能ですが
API Gatewayを経由することで、通常のWebAPIを利用するのと同じ感覚で実装できます。
今回はスピード重視ということでAPI Gateway経由での実行方式を採用します。

APIの作成自体はすぐに終わります。
API Gatewayから「APIの作成」を選択し、API名を入れるだけでOKです。

API Gateway

DynamoDBの準備

続いてDynamoDBで今回のタスクを登録するDBを作成しておきます。
「テーブルの作成」を選択したら、テーブル名とプライマリキーを入力して「作成」を選択。
一瞬で作成完了します。

DynamoDBでテーブル作成

LambdaでAPI GatewayトリガのFunctionを作成

続いてLambdaのFunctionを作成していきます。
サービスからAWS Lambdaを選択し、「Create a Lambda Function」を選択すると、blueprint選択画面が表示されます。

Blueprint選択画面

blueprintとは、Functionを作るにあたって利用可能なテンプレートのことです。
利用頻度の高いものが用意されており、今回は「microservice-http-endpoint」を採用してみます。

続いてトリガの設定に移ります。
今回はWebAPIライクに使いたいので、API Gatewayを選択します。
API nameに先ほどAPI Gatewayで作成したAPIの名前を指定し、SecurityはOpenとします。
(本来は認証を通すべきですが、ここは検証用のため一旦スコープ外とします)

トリガ設定

トリガが設定できると、後はFunction本体の設定になりますが
blueprintによってほぼ完了に近いコードが既に入っているはずです。
なお今回はNode.jsの4.3を使用します。

Function設定

コードの部分を確認すると分かるとおり、テンプレートの時点でGET/POST/PUT/DELETEそれぞれのメソッドに応じて処理を切り替えるようなコードになっています。
ただしこのままだと、呼び出し元でDynamoDBのAPI向けに整形したJSONを渡さなければならず、テーブル名も呼び出し元で指定する形になってしまいます。
これはAPI内でなるべく閉じたいところですので、ちょっと加工して以下のようにコードを修正します。

なおここでは一般的なREST APIの構成に合わせて

  • GET
    • Keyが指定されていればそのアイテムを返す
    • Key指定がなければすべてのアイテムを返す
  • POST
    • 新規作成
  • PUT
    • 更新
  • DELETE
    • 削除

として実装しました。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
'use strict';
console.log('Loading function');
const doc = require('dynamodb-doc');
const dynamo = new doc.DynamoDB();
exports.handler = (event, context, callback) => {
    console.log('Received event:', JSON.stringify(event, null, 2));
    const done = (err, res) => callback(null, {
        statusCode: err ? '400' : '200',
        body: err ? err.message : JSON.stringify(res),
        headers: {
            'Content-Type': 'application/json',
        },
    });
    var tableName = "sample-table";
    switch (event.httpMethod) {
        case 'DELETE':
            dynamo.deleteItem({
                    "TableName": tableName,
                    "Key": {
                        "task_id": event.task_id
                    }
                }, done);
            break;
        case 'GET':
            if (event.task_id)
                dynamo.getItem({
                    "TableName": tableName,
                    "Key": {
                        "task_id": event.task_id
                    }
                }, done);
            else
                dynamo.scan({ TableName: tableName }, done);
            break;
        case 'POST':
            dynamo.putItem({
                    "TableName": tableName,
                    "Item": {
                        "task_id": event.task_id,
                        "task_body": event.task_body,
                        "status": event.status
                    }
                }, done);
            break;
        case 'PUT':
            dynamo.updateItem({
                    "TableName": tableName,
                    "Key": {
                        "task_id": event.task_id
                    },
                    "AttributeUpdates": {
                        "task_body": {
                            "Action": "PUT",
                            "Value": event.task_body
                        },
                        "status": {
                            "Action": "PUT",
                            "Value": event.status
                        }
                    }
                }, done);
            break;
        default:
            done(new Error(`Unsupported method "${event.httpMethod}"`));
    }
};

コード本文以外はほぼデフォルトですが、Handlerがfunction実行時に呼び出される関数になりますので実際の関数名と合わせておくようにします。
ここでは「exports.handler」としています。

今回はファイルを分割するほどの大掛かりなプログラムを組まないため、コードを直接編集していますが
ローカルで実装したファイル群をzipで圧縮して、まとめてアップすることも可能です。
ある程度処理の規模が大きくなるようであれば、こちらを採用するといいと思います。

あとはボタンを押していけばFunctionが出来上がります。
試しにDynamoDBへデータを入れてからAPI GatewayのURLを叩くと、結果が取得できるはずです。

1
{"Items":[{"task_id":"1234","task_body":"to write tech blog","status":"0"}],"Count":1,"ScannedCount":1}

POSTでJSON形式のデータを渡せば、データの登録もできます。PUTやDELETEも同様です。 これでバックエンド側の処理は一通り用意ができたので、あとはアプリ側でREST API実行の処理を用意するだけです。

必要コスト

今回使用しているAWSのサービスは

  • Lambda
  • API Gateway
  • Cloud Watch
  • DynamoDB

の4つです。

これらを使うことで月々のコストがどのくらいかかるのか、という点も調査してみました。

前提

  • 1ヶ月あたりのAPI呼び出し回数を500万回とする
    • 無料枠の5倍程度のアクセスがある、という前提
  • Lambdaの利用状況は以下の前提とする
    • 1つのFunctionに割り当てるメモリは256MB
    • Function実行にかかった時間は0.5秒
    • 1回あたりのデータ転送量は2KBとする
  • CloudWatchの利用状況は以下の前提とする
    • ログの保持以外の機能は使用しない
    • 1回あたり出力されるログのサイズは2KBとする
    • アーカイブログは出力ログの1/5程度に圧縮されると仮定する
  • DynamoDBの利用状況は以下の前提とする
    • 月あたりのデータサイズは5GB程度とする(512Byte * 1000万件)
    • 1秒あたりの読み込み・書き込み件数は2とする(500万 / 30 / 24 / 60 / 60)
      • 1月あたりのAPI呼び出しが500万想定なので、全てどちらかに偏った可能性で考慮

スモールスタートという前提で、そこまで大量に使われない状態でどれくらいか、という観点での数値となります。

Lambda

Lamdbdaは無料枠があります。
この無料枠は2016年11月時点では無期限となっているため、範囲内であれば無料で利用可能です

  • 無料枠の範囲
    • 1ヶ月あたり100万件のリクエスト
    • 1ヶ月あたり400,000 GB-秒のコンピューティング時間
      • Lambda関数に割り当てたメモリ量によって無料利用枠で実行可能な時間が変動する
      • 128MBの場合、1ヶ月あたり3,200.000秒が無料枠として利用可能
  • 有料化後の金額
    • リクエスト数:0.0000002 USD/リクエスト
    • コンピューティング時間:0.00001667 USD / GB-秒
  • 想定金額
    • リクエスト料金 = (合計リクエスト - 無料利用枠) × 0.0000002 = (5,000,000 - 1,000,000) × 0.0000002 = 0.80 USD
    • コンピューティング料金 = (合計コンピューティング時間 - 無料利用枠) 0.00001667 = (625,000 - 400,000) × 0.00001667 = 3.75 USD
    • 合計:0.80 + 3.75 = 4.55 USD/月

API Gateway

API Gatewayも無料枠がありますが、こちらは最大12ヶ月まで、となっています。
無料期間であれば「1ヶ月あたり100万件のリクエスト」までは無料となります。

  • 無料枠
    • 1ヶ月あたり100万件のリクエスト(最初の12ヶ月のみ)
  • 有料化後の金額
    • リクエスト数:0.0000035 USD/リクエスト
    • データ転送量:0.09 USD/GB ※最初の10TB
    • キャッシュ:0.5GBで0.020USD
  • 想定金額
    • 無料期間
      • 呼び出し料金 = (合計呼び出し回数 - 無料利用枠) 0.0000035 = (5,000,000 - 1,000,000) 0.0000035 = 14.0 USD
      • データ転送料金 = データ転送量合計(GB) 0.09USD = (2KB 5,000,000 / 1024 / 1024) 0.09 = 9.53 0.09 = 0.8577 USD
    • 無料期間終了後
      • 呼び出し料金 = 合計呼び出し回数 0.0000035 = 5,000,000 0.0000035 = 17.5 USD
      • データ転送料金 = データ転送量合計(GB) 0.09USD = (2KB 5,000,000 / 1024 / 1024) 0.09 = 9.53 0.09 = 0.8577 USD
  • 合計
    • 無料期間:14.0 + 0.8577 = 14.858 USD/月
    • 無料期間後:17.5 + 0.8577 = 18.358 USD/月

CloudWatch

CloudWatchも無料枠があります。こちらは無期限です。

  • 無料枠
    • 最大 50 個のメトリクスからなる 3 つのダッシュボードが毎月利用可能
    • 5GBのログデータ取込
    • 5GBのログデータアーカイブ
    • 10カスタムメトリックス、10アラーム、1,000,000APIリクエスト
    • EBS, ELB, RDS, EC2の基本モニタリングメトリクス(5分間隔)
  • 有料化後の金額
    • ログの取込:0.76 USD/GB
    • ログのアーカイブ:0.0033 USD/GB
  • 想定金額
    • ログ取込料金 = (取込ログサイズ - 無料利用枠)[GB] 0.76 = (9.54 - 5) 0.76 = 3.45 USD/月
    • アーカイブログ料金は無視できるレベルなのでここでは計算対象外とする

DynamoDB

DynamoDBも無料枠があります。こちらも無期限での提供となっております。

  • 無料枠
    • 25GBのストレージ
    • 25ユニットの読み込み容量と25ユニットの書き込み容量
    • Amazon DynamoDBによる毎月2億件までのリクエスト処理に十分な容量
  • 想定金額
    • 計算式がやや複雑なため見積ツールで計算しました
    • 合計:0.57 USD/月

25GBまで無料ということもあり、この程度であればほぼ無料枠に収まってしまう、ということになります。

合計

サービス 金額
Lamnda 4.55 USD/月
API Gateway 18.358 USD/月
CloudWatch 3.45 USD/月
DynamoDB 0.57 USD/月
合計 26.928USD/月

※数値はあくまで目安です

1ドル110円とすると、およそ2962円といったところです。
これはEC2インスタンスでt2.smallのインスタンス1台を1ヶ月使った時の金額と同じくらいです。
小規模サービス、もしくはスマートデバイス向けアプリケーションであれば、安価にスタートできると言えるのではないでしょうか。

ただし使用しているサービスの数が多い分、スケールアップする際に増える料金も多くなります。
ここで出している数値は、無料利用枠の恩恵をかなり受けているため、たとえばアクセス数が2倍の1000万件になった場合、
単純に2倍した程度では収まらなくなります。
サービスの規模を考慮しながら使う必要がある、と言えるでしょう。

考察

WebAPIとしての利用

本番運用する場合はドメインの設定や認証の設定などあるので実際はもう少し時間かかるとは思いますが
単純なWebAPIを作成するだけなら1時間とかからずに公開まで持っていくことが出来ます。

そのため複雑なロジックを必要としない、シンプルな構成のAPIが取り急ぎほしい!というケースでは十分マッチするのではないかと考えます。
処理スピードも体感速度では全く気にならないレベルです。

既存のWebAPIの置き換え

APIの本数が増えてきて、共通処理が必要になってくると、FunctionからFunctionへの連携を考慮した作りにするか、
もしくはソースコードを多重管理するかのどちらかになってきます。

言語の選択肢も狭いですし、同じ言語を使用していたとしても、Function単位で分離させる必要があるため、そのまま移行するというわけにもいきません。
また、RDSとの相性がよくないため、DynamoDBに切り替えるなどの対応も必要になってきます。
そもそも既存の仕組みはあくまで常駐型のサーバーに載せることを前提として作られていることがほとんどのため
設計思想の異なるアーキテクチャにそのまま載せ替える、というのは現実的ではないでしょう。

監視ツールとしての利用

CloudWatchやS3、DynamoDBなどの変更をトリガとして起動させることが出来るため
監視ツールとしては幅広い用途で使えそうです。

ログやプロセスの監視、ファイルの更新検知、特定データの更新検知など。
特にDB更新をトリガとして動けるというのは強みではないかと思います。
さすがにこれだけで万全、というわけにはいかないものの、AWSでサービスを運営するのであれば監視ツールの1つとしては十分使えるはずです。

まとめ

Lambdaを始めとするFaaSの魅力は、やはり用意するまでのスピード感といえます。
グルメ開発チームで実際に活用するとした場合、ABテストやSEO施策などの速さを求められる分野でより効果を発揮できるのではないか、と考えています。
もしくはChatOpsのためのbotとしても気軽に用意ができるので、プロダクト開発だけではなく運用面でも効果的な使い方が出来るでしょう。

制約も多いですが、それゆえシンプルな設計に勝手になってしまう、とも言えるので
最初からサーバーレスを前提として設計されたAPIは、メンテナンスコストも低く抑えられるという効果があるのではないか、という仮説も立てられます。
既存APIの改修はともかく、新規に作成するAPIであれば、既存のシステムに追加するよりも、シンプルでメンテナンス性の高いものが作れるのではないか、と思います。

より速く、より小さく、が追求されていく現代のシステム開発において、選択肢として持っておくことは大きな意味を持つので
是非実際のサービスにも本格的に導入して、ベタープラクティスを蓄積していきたいと思います。

以上、AWS Lambdaの検証内容と、今後の展望共有でした。