2022/9 API Gatewayでクラウド破産しない

佐々木誠
大阪

脱Lightsailしサーバーレスへの移行について紹介しましたが、構築する中でクラウド破産しないように気をつけたことがあったので紹介します。

クラウド破産

AWSのようなクラウドサービスでは、リソースを使った分だけ料金請求される従量課金制であることが多いです。

お試しで構築したサーバや設定を消し忘れたり、大量のアクセスがきてリソースを消費して、意図しない金額の料金を請求されてしまうことをクラウド破産などと言うことがあります。 破産

本ブログのサーバーレス構成は、誰でも閲覧できるのでDoS攻撃や悪戯による過アクセスがありえるので、クラウド破産しないよう対策が必要となりました。

スロットリング設定

API Gatewayにはスロットリングと言って、秒間あたりのリクエスト数に上限設定をすることができます。上限を超えたリクエスには429エラーが返されリソースは消費せず、Lambda等にも到達しないのでそれ以上の課金はされません。

API Gatewayのスロットリング設定には「レート」と「バースト」の2項目があり、トークンバケットアルゴリズムに基づいた制限設定となり、ちょっとややこしいです。 トークンバケットアルゴリズム

トークンバケットアルゴリズムはよく、水道の蛇口から出てくる水・バケツ・バケツから出ていく水に例えられます。 水道の蛇口から出てくる水の量を「レート」、水を一定量まで溜められるバケツの水量を「バースト」、バケツから出ていく水が「アクセス数」となります。

普段のアクセスでは

普段のアクセス

と言うロジックで自転車操業的に補填され、アクセスし続けられます。

その上で、瞬間的にアクセス数がスパイクした時に、バケツ(バースト)が活躍します。

スパイク

このように、上限制限する上では、平均してアクセスされる数の上限を「レート」、瞬間的にスパイクしたアクセス数の上限を「バースト」と考え設定しました。

そして設定する上では

の3観点でマージンを考え、設定する上での具体的な数値は

から算出しました。

上限アラート構築

スロットリング設定をした上で

状況を見て上限値を手動で見直す運用としたので、上限に達した場合にはSlackに通知するように設定をしました。

検出〜通知するためのAWSフローは、API Gateway → CloudWatch Logs → メトリクスフィルター → CloudWatch アラーム → SNS...という感じです。SNS以降はLambdaに流すもよし、Chatbotに流すもよし、運用者にとって便利な方法を選ぶのが良いと思います。 AWSフロー

AWSコンソールからの設定方法を紹介している記事はいくつか見つけたのですが、CDKでの設定紹介例は見当たりませんでした。 本ブログはCDKで作りましたので、例としてCDKサンプルコードを紹介します。

import { Construct } from 'constructs';
import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs';
import { Alarm, ComparisonOperator, TreatMissingData } from 'aws-cdk-lib/aws-cloudwatch';
import { Duration } from 'aws-cdk-lib';
import { SnsAction } from 'aws-cdk-lib/aws-cloudwatch-actions';
import { Topic } from 'aws-cdk-lib/aws-sns'
import { CfnApi, CfnStage } from 'aws-cdk-lib/aws-apigatewayv2';

export const sample = (scope:Construct, topic:Topic)=>{

    // CloudWatch Logs
    const log = new LogGroup(scope, 'APIGatewayLog', {
        // アクセス過多検知でいればいいので1週間
        retention: RetentionDays.ONE_WEEK
    });

    // HTTP API Gateawy
    const api = new CfnApi(scope, 'PublicAPI', {
        name: 'PublicAPI',
        protocolType: 'HTTP'
    });

    // ステージ
    const stage = new CfnStage(scope, `PublicAPIStage`, {
        apiId: api.ref,
        stageName: '$default',
        autoDeploy: true,
        defaultRouteSettings:{
            // スロットリング
            throttlingBurstLimit: xxx,
            throttlingRateLimit: xxx,
        },
        // ログ記録
        accessLogSettings:{
            destinationArn: log.logGroupArn,
            format: xxx,
        }
    });

    // メトリクスフィルター
    const metricFilter = log.addMetricFilter('TooManyRequests', {
        metricNamespace: 'Sample',
        metricName: 'TooManyRequests',
        // 検出パターン
        filterPattern: { 
            logPatternString: 'Too Many Requests' 
        },
        metricValue: '1',
        defaultValue: 0,
    });

    // ClouWatch アラーム
    const alarm = new Alarm(scope, 'TooManyRequests', {
        metric: metricFilter.metric({
            // 合計
            statistic: 'Sum',
            // 期間
            period: Duration.minutes(5),
        }),
        // 閾値
        threshold: 1,
        // 最新の期間またはデータポイントの数
        evaluationPeriods: 1,
        // アラーム状態になるためのデータポイントの数
        datapointsToAlarm: 1,
        comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
        treatMissingData: TreatMissingData.NOT_BREACHING,
        actionsEnabled: true,
    });
    alarm.addAlarmAction(new SnsAction(topic));
}

最後に

最初は、クラウド破産しないためにはどうしたら良いのか?わからず暗中模索し、「レート」と「バースト」の意味を理解するところで苦戦し、CDKに落とし込むのに苦戦しました。

苦戦ばかりでしたが各機能への理解が深まると、本当に良くできたサービスだと思うようになり、楽しくなりハマっていきました。

最後に、もし同じようなことでつまづいていらっしゃる方がいましたら、参考になればと幸いです。

こんな記事も読まれています