この記事は 【カヤック】面白法人グループ Advent Calendar 2024 の 23日目の記事です。
カヤック技術部の谷脇です。さて、皆さんはAWS Lambdaが非常に安く使えることをご存知でしょうか? Lambdaは1ヶ月あたり100万回のリクエストと総実行時間320万秒が無料です。これを超えたとしても非常に安く使えることが知られています。
例えばWebアプリケーションサーバーを例に出すと、ECSなどと違いリクエストドリブンであるという点は考慮する必要があるものの、シンプルな管理画面や社内ツールであればLambdaで十分に実装できます。
一方で罠も存在します。使おうとしたら余計にかかってしまったということがないように、最近私が社内ツールなどでLambdaを活用して実装していった時に編み出したTipsを紹介します。
前提
この記事で想定しているアプリケーションは以下の項目を満たすものです。
- HTTP(S)リクエストを受ける
- 低頻度にアクセスされる
- 社内アプリケーションやSaaSの管理画面などUGCやSNSと比べるとアクセス頻度が低いものを想定しています
- アプリケーション自身が容量があるファイルなどを返さない
何でHTTPリクエストを受けるか
Lambda関数はなんらかの形でトリガーして起動しなければいけません。Webアプリケーションサーバーの場合、HTTPリクエストを受けて起動し、HTTPレスポンスを返すものとなります。その場合、Lambdaでは以下の手段があります。
- API Gateway
- Application Load Balancer(ALB)
- Lambda Function URLs
- with CloudFront
API Gatewayが最も一般的だと思われますが、管理画面等では構成を複雑にしないためにここでは採用を避けます1。
ALBもLambdaに対してトラフィックを流せますが、使用量に関わらずALBは存在するだけで料金がかかってしまいます。2024年12月現在東京リージョンでは1ヶ月あたり$17.49です。
Lambda Function URLsは2022年に発表された比較的新しい機能です。Lambda単体でHTTPリクストを受け付けることができます。またLambda Function URLsは使用に際して追加料金がかかりません。一方で、この機能で発行されるURLは https://xxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/
のような形式です。自由にドメイン設定をできないため、Slack botのWebhookなどにはこれで十分ですが、人がブラウザから利用するようなアプリケーションには向いていません。
そこで、CloudFrontを経由してLambda Function URLsにアクセスすることで、自由にドメイン設定を行いつつ、Lambda関数を利用できます。CloudFrontは月間1TBまでのデータ転送と1000万件までのリクエストが無料です。今回話題にしているアプリケーションではまず無料枠を超えないかと思います。2
というわけで、私の場合はLambda Function URLsにCloudFrontを経由してアクセスすることが多いです。また、Lambda Function URLsの認証にIAM認証を使用して、CloudFrontのOAC機能を使用してアクセス制御しています。これにより、CloudFrontを経由せずに直接Lambda Function URLsにアクセスすることを防ぎます。
OACの場合に必要なコンテンツハッシュヘッダの生成
OACを用いている場合、POST/PUTリクエストを行う際にはリクエストボディのハッシュ値を計算し、x-amz-content-sha256
ヘッダに格納する必要があります。クラスメソッドさんの記事ではLambda@Edgeを使用してこのヘッダを生成しています。
私はSPAのAPIサーバーをLambdaで実装していたため、SPA側にこの計算処理を持たせました。以下にその部分のコードを示します。
const calcHash = async (body: string) => { const encoder = new TextEncoder().encode(body); const hash = await crypto.subtle.digest("SHA-256", encoder); const hashArray = Array.from(new Uint8Array(hash)); return hashArray.map((bytes) => bytes.toString(16).padStart(2, "0")).join(""); }; const oacFetcher = async ( input: string, init: { method: string; headers: Record<string, string>; body: string | undefined; }, ) => { let headers = init.headers; if (["POST", "PUT"].includes(init.method) && init.body !== undefined) { const hash = await calcHash(init.body); headers["x-amz-content-sha256"] = hash; } return await fetch(input, { ...init, headers }); };
このoacFetcher
を使うとPOST/PUTリクエストの際に自動でx-amz-content-sha256
ヘッダを付与してくれます。
使用例: アップローダー
この手法を用いて、ハイパーカジュアルゲームチーム内で試作したものを共有するためのアップローダーを作成しました。このアップローダーはSPAアプリケーションであり、CloudFrontでOAC経由でLambda Function URLsにアクセスする以外にもS3からフロントエンド用のJavaScriptやCSSも配信しています。また、アップロードされたファイルはS3に保存されます。また、これらのS3のリソースは署名付きCookieを用いてアクセス制御を行なっています。
アップロードされた際に記録するアップロードした人や日時、ファイルの説明などのメタデータも、節約と構成の簡素化のためにS3にJSONで保存しています。同時書き込みでJSONが壊れないようにS3 Notificationを用いてLambda関数を起動してJSONを更新しています。
以下に構成を示します。
この構成でそれなりに社内で使われているアプリケーションですが、月間のコストは$3以下です。
IPv6リクエストでNAT Gatewayの使用を回避
最近私が社内で改修をこなったアプリケーションに、チーム内で日報を行うSlack botがあります。このSlack botは元々Amazon ECS上で動作し、Slack RTM APIを使用していましたが、RTM APIはクラシックAppのみのサポートであり、クラシックApp自体の新規作成もすでにできないめ、近いうちに廃止される可能性があります。そこで、私たちは日報botをEvents APIに移行することにしました。
Events APIにすると、RTM APIのように常時起動しておく必要がなくなります。そのためコスト節約やインフラ管理の簡素化のためにもLambdaを使用することにしました。 一方でこの日報botはMySQLを必要としていました。Amazon RDS for MySQLを使用することになりますが、一般にRDSはインターネットから分離されたprivate subnet内に配置されるのが望ましいです。そのため、RDSはprivate subnetに配置しました。またRDSにアクセスするためにLambda関数もprivate subnet内に配置することにしました。
Lambda Function URLsはprivate subnet内のLambdaに対しても付与ができます。しかしSlack botであるLambda関数はインターネット側に存在するSlack APIを使用しなければなりません。一般にprivate subnet内からprivate subnet外にアクセスするためには、NAT Gatewayが必要です。しかしこのNAT Gatewayは先述したApplication Load Balancerと同様に存在するだけで料金がかかってしまいます。2024年12月現在東京リージョンでは1ヶ月あたり$44.64です。また、転送量に応じて追加料金がかかります。
そこで以下の2つのLambda関数を作成しました。
- Lambda Function URLsでEvents APIを受けるメインLambda関数
- VPC内
- Lambda Function URLsからきたリクエストをSlack APIをプロキシするプロキシLambda関数
- VPC外 IAM認証を使用
Slack APIをメインLambda関数が使用する際には、プロキシLambda関数を経由するようにしました。しかし、前述の「private subnet内からprivate subnet外にアクセスするためにはNAT Gatewayが必要」という制限に引っかかってしまうように思えます。しかし抜け道が存在します。それがIPv6によるアクセスです。
VPCにEgress-Onlyインターネットゲートウェイを設置できます。Egress-Onlyインターネットゲートウェイは送信専用のインターネットゲートウェイで、IPv6トラフィックのみを許可します。このEgress-Onlyインターネットゲートウェイを使用することで、Lambda関数からIPv6経由でインターネットにアクセスすることができます。また、Lambda Function URLsはIPv6に対応しています。なので、この構成が可能になります。もしSlack APIがIPv6に対応していた場合は、プロキシLambda関数は不要ですが、2024年12月現在ではIPv6に対応していないため、このような構成にしました。
一方で、AWSのサービスの中でもIPv6に対応していないものもあります。例えばSystems ManagerのParameter Storeには対応していません。私はLambda関数を使用する際には多くの場合、Systems ManagerのParameter Storeにシークレットなどを格納しています。今回の場合はSlack APIに用いるAPIトークンがそれにあたります。
代わりにSecret Managerを使用しました。Secret ManagerはIPv6に対応しているため、Egress-Onlyインターネットゲートウェイを使用しても問題なくアクセスできます。
以下に構成を示します。
まとめ
Lambdaは非常に安く使用することができ、社内アプリケーションのようにアクセス頻度が比較的少ない場合では特に有用です。また、Lambda Function URLsを使用することで、API GatewayやALBを使用することなく、LambdaをHTTPリクエストに対応させることができます。また、CloudFrontを経由することで、自由にドメイン設定をしたり、静的ファイルの配信も行うことができます。
また、VPC内に配置する際にはIPv6によるインターネットアクセスも節約のために有用です。このような構成での用途が広がるように、IPv6を喋るサービスが増えると良いなと思います。
皆さんも節約しながらLambdaを活用してみてはいかがでしょうか?