FURYU Tech Blog - フリュー株式会社

フリュー株式会社の開発者が技術情報を発信するブログです。

API Gateway + Lambda + DynamoDBでサーバレスWEBアプリを作ってIP制限も掛ける②

こんにちは! ピクトリンク事業部商品技術開発部の橋本です。

前回に引き続き、API Gateway + Lambda + DynamoDBを使ったサーバレスなWebアプリを作っていきたいと思います。
API Gateway側の設定やアクセス制限、Lambdaとの疎通に関しては前回解説しましたので、今回はLambda関数の作り込みとDynamoDBからの値の取得について解説していきます。

今回解説するのは②の範囲

前回の記事はこちら tech.furyu.jp

Lambdaで使うモジュールの準備

Lambda上のNode.jsでnpmライブラリを使いたい場合、Lambdaレイヤー経由で呼び出す必要があります。
今回はDynamoDBから値を取得するためにaws-sdkを、HTMLテンプレートを使うためにejsを導入します。

こちらの記事を参考にリソースをデプロイします。 tech.furyu.jp

もしくは、AWSブラウザコンソール上で実施しても構いません。

その場合、ローカル環境にnpmを導入して利用するモジュールを導入します。
今回必要なのはaws-sdkとejsなので、 npm install aws-sdk ejs のようにします。
取得したモジュールの入った node_modules をzip化してLambdaレイヤーにアップロードすればOKです。

レイヤーを読み込む

Lambda -> [関数]の詳細画面から読み込みます。
関数の[コード]タブを下にスクロールしていくと[レイヤー]の項目があるので、[レイヤーの追加]をクリックします。

レイヤーを追加
[カスタムレイヤー]を選択し、作成したレイヤーを読み込みます。

レイヤーにランタイムやアーキテクチャが指定してあると、関数に合致するものだけが候補一覧に表示されます。
1つや2つの間はいいですが、数が増えてくると探すのが大変になってきます。
これらの設定値は空欄でもレイヤーを作成できますが、探すのが楽という恩恵はあとあと効いてきますので、なるべく埋めておくのがおすすめです。

レイヤーが追加された
レイヤーを追加すると、このような表示で確認できます。
レイヤー側からも、紐づく関数一覧が参照できます。
利用中のものを誤って消しにくくなっているのはありがたいですね。

あとは、関数のコード内で、

import ejs from 'aws-sdk';

のようにすると、関数の中でライブラリが使えるようになります。

LambdaからDynamoDBを参照する

LambdaでDynamoDBリソースを参照するのは簡単です(権限さえちゃんとついていれば!)。

import AWS from 'aws-sdk';

async function(event, context) {
    const db = new AWS.DynamoDB.DocumentClient();
    return new Promise((resolve, reject) => {
        const params = {
            TableName: 'DynamoDBのテーブル(スキーマ)名',
            Key: {カラム名: キーとなる値}
        }
        db.get(params, function(err, data) {
            if (err) {
                reject(err);
            } else {
                resolve(data.Item);
            }
        });
    });
}

取得した値は ejs テンプレートでHTMLに加工してreturnすれば、あとはAPI Gatewayがブラウザ側にレスポンスを返してくれるって寸法です。
コードを編集したら[Deploy]をクリックして反映します。

ブラウザで確認する前に、Lambda関数内のTestで動作確認を行います。
この段階でDynamoDBのスキーマが見つからないなどのエラーが出る場合、スキーマ名の間違いや、権限の不足が怪しいです。

DynamoDBから get (1レコード取得)や query (条件に合致する複数レコード取得)で値を参照する場合、テーブル自体への参照権限と、インデックスの参照権限が必要です。
scan(全件取得)の場合はインデックスへの参照権限は不要なようですが、全件取得してLambda側でフィルタするよりも取得条件で工夫したほうがDynamoDBやLambdaの費用的にお得に使えると思います。

前回の記事でも触れましたが、必要な権限について悩む時間を惜しんで

"Resource": "arn:aws:dynamodb:*"

とか書きたくなっちゃう気持ちもあります。
が、日頃から必要な最小限の権限に絞る癖をつけておくほうが、セキュリティ的にも不慮のやらかし対策にも良いです。
弊社では「ちょっと過剰じゃない?」みたいな権限をつけようとするとレビューでばしばしツッコまれます。

さらなるカスタマイズ

同時実行数を制限する

DDoS攻撃や、プログラムのミス、ヒューマンエラーによって想定以上に負荷がかかるのを防ぐため、実行数に制限をかけるのもいいでしょう。
とくにLambdaはAWSアカウントごとに同時実行数の上限が決まっています。

Lambda はアカウントに対し、1 つのリージョン内のすべての関数全体での合計数 1,000 を上限とした同時実行をデフォルトで提供しています。 docs.aws.amazon.com

今回のWEBアプリは 使う人数が限られており、たまに使う程度のもの という想定のため、同時実行可能数にかなり制限をかけても問題ないはずです。

Lambda -> [関数] -> [設定]タブ -> [同時実行]から予約された同時実行数に制限を掛けられます。

同時実行数に制限をかけました

実際にWEBアプリを使ってみて、もう少し制限を緩めたいなとなったときに調整しても良いですね。

API Gateway側に制限を加えることもできます。
API Gateway -> [API] -> [ステージ] -> [設定]からスロットリングの有効化にチェックを入れ、レートを設定します。

API Gatewayのレートに制限をかけました

API Gatewayの料金体系はリクエスト数に対する課金、Lambdaは関数が実行されるのにかかった時間に対して料金が発生しています。
あらかじめリソースに制限をかけておくことで、不慮の事故によって莫大な請求が来る恐怖も、ある程度予防できます。

WAF(WEBアプリケーションファイアウォール)で防御力を高める

API GatewayをREST APIモードで作成している場合、WAFを連携できます。 (HTTP APIでは不可)
前回の記事でリソースポリシーによるIP制限をかけているので今回は不要と判断してもいいかもしれません。

WAFの料金体系はルールの数 + リクエスト数による従量課金制なので、リクエスト数が少ない想定の今回の要件の場合、比較的安価に運用できるでしょう。

aws.amazon.com

WAFを導入すると、基本的なインジェクション対策や、同一IPからの連続アクセスを防止するレート制限などが掛けられます。
また、セキュリティの専門家であるベンダーが提供しているマネージドルールの適用もできます。

アタック手法とセキュリティは日夜進歩を続けており、古い防御のままでは意味を為さなかったりすることもザラです。
そこで、セキュリティベンダーのマネージドルールを適用しておけば、セキュリティのプロ集団であるベンダーがルールをチューニングして最新状態を保ち続けてくれるメリットがあります。
ただし、WAF自体の利用料金に加えてマネージドルール自体の利用料金が別途掛かるため、アプリの要件と照らし合わせて必要性を判断することになるでしょう。

カスタムドメイン(独自ドメイン)をつける

Route53で管理しているドメインを割り当てることもできます。
あらかじめドメイン名と証明書をRoute53で登録しておき、API Gateway -> [カスタムドメイン名]から連携します。
ドメインによって割り当てるAPI Gatewayのステージを変えることもできるので、テスト用ドメインにテスト用ステージを紐付けて使うみたいな使い方もできて便利です。
証明書もACMで払い出しておけば無料で自動更新できます。

aws.amazon.com

忘れた頃にやってくる証明書更新作業の手間がなくなるのは結構うれしいです。
更新作業は大した作業ではないですが、1年に1度程度の作業なので手順がうろ覚えになってたりします。
昔は5年に1度とかの時期もあったそうですよ、証明書の更新。

自分しか利用しないWEBアプリの場合はドメイン保持にかかる費用を節約したいな、と個人的には思いますが、外部に公開する場合はカスタムドメインをつけておいたほうが覚えやすく、見栄えもするので、利用者にとってフレンドリーです。
誰にも見られない家の中ならジャージなどの楽な格好でいますが、外に出るときには多少身だしなみに気を付ける、くらいのニュアンスとでもいいましょうか。

見栄えをリッチにしたい

ejs で簡単なテンプレートの出し分けをするだけじゃなく、フロント側でも動きや見栄えに凝ったことをしたい!
という欲求がある場合、API Gatewayがブラウザに返却しているのは普通のHTMLドキュメントなので、CDNが使えます。
ejsテンプレートの<head>内にCDNの記述を加えておけば、Vue.jsReact.jsFont AwesomeBootstrapなどお好きなフロントエンドのフレームワークやライブラリを活用できます。
CDNでChart.jsを読み込んで、DBから取得したデータをグラフ表示するWEBアプリなんてのも良いかもしれません。

静的リソースを呼び出す

画像や動画、css、フォントなどの静的リソースを呼び出したい場合、S3に配置したそれらを呼び出すこともできます。
S3側のバケットポリシーでリソースの取得 "s3:GetObject" を許可する必要があります。

docs.aws.amazon.com

が、そうなるとCloudFrontでデータをキャッシュしたくなってきますし、API Gateway + Lambda + なにかしらのDB + S3 + CloudFront と構成が複雑化していくようなら、要件とのズレを疑ってみるタイミングかもしれないです。
見直してみるとEC2/ECSを利用した構成のほうが要件に合致して、すっきり作れるかも?

または、静的ファイルホスティングさえできればいい場合、S3を介さずに raw.githubusercontent などからリソースを直接呼び出してみるのもアリだと思います。

おわりに

API Gateway + Lambda + DynamoDBでサーバレスWEBアプリを作ってみました。
やろうと思えば、Linux上に自前でアプリケーションサーバを構築するのとおおむね遜色ないWEBアプリが実現できそうです。
ただし、ドメインにWAFにログ監視に……といろいろやろうと思うと、その分利用料金という形でハネ返ってくるので、必要な要件を見極める必要があるなと感じました。いろいろできると、いろいろやりたくなりがち。

負荷に応じてオートスケールしたいとか、アクセスログを解析してA/Bテストしてアプリのブラッシュアップをといった欲求がある場合は、EC2やECSを利用したほうが良さそうです。

そういった複雑な用途はなく、安く簡単なWEBアプリを作りたい場合は、この構成で十分なものが作れると思います。
WEBアプリを使わないタイミングでは料金が発生しないのが本構成の大きな強みなので、個人ないし少人数での利用が要件に含まれる場合に真価を発揮できるはずです。