脱Lightsailしサーバーレスへの移行について紹介しましたが、構築する中でクラウド破産しないように気をつけたことがあったので紹介します。
クラウド破産
AWSのようなクラウドサービスでは、リソースを使った分だけ料金請求される従量課金制であることが多いです。
お試しで構築したサーバや設定を消し忘れたり、大量のアクセスがきてリソースを消費して、意図しない金額の料金を請求されてしまうことをクラウド破産などと言うことがあります。
本ブログのサーバーレス構成は、誰でも閲覧できるのでDoS攻撃や悪戯による過アクセスがありえるので、クラウド破産しないよう対策が必要となりました。
スロットリング設定
API Gatewayにはスロットリングと言って、秒間あたりのリクエスト数に上限設定をすることができます。上限を超えたリクエスには429エラーが返されリソースは消費せず、Lambda等にも到達しないのでそれ以上の課金はされません。
API Gatewayのスロットリング設定には「レート」と「バースト」の2項目があり、トークンバケットアルゴリズムに基づいた制限設定となり、ちょっとややこしいです。
トークンバケットアルゴリズムはよく、水道の蛇口から出てくる水・バケツ・バケツから出ていく水に例えられます。
水道の蛇口から出てくる水の量を「レート」、水を一定量まで溜められるバケツの水量を「バースト」、バケツから出ていく水が「アクセス数」となります。
普段のアクセスでは
- 「レート」の数だけ水が出ます
- アクセスがあるとバケツの水が減ります
- 減りますが「レート」の数だけ補充されます
と言うロジックで自転車操業的に補填され、アクセスし続けられます。
その上で、瞬間的にアクセス数がスパイクした時に、バケツ(バースト)が活躍します。
- 「レート」の水は、「バースト」の数だけバケツに溜めれます
- 瞬間的に「レート」より多くのアクセスがあっても、溜まっているバケツ(バースト)分は水に余裕がありアクセスできます
- 「レート」の水よりも多く、バケツの水も空になるぐらいのアクセス数の場合に初めて「上限」となります
このように、上限制限する上では、平均してアクセスされる数の上限を「レート」、瞬間的にスパイクしたアクセス数の上限を「バースト」と考え設定しました。
そして設定する上では
- 普通にアクセス数が年々伸びる場合
- 一瞬のスパイクアクセス
- DoS攻撃や悪戯による過アクセス
の3観点でマージンを考え、設定する上での具体的な数値は
- Google Analyticsによる過去のアクセス傾向
- 上限を緩めに設定し、1ヶ月程度の実運用
から算出しました。
上限アラート構築
スロットリング設定をした上で
- 普通にアクセス数が伸びていったのか
- 一瞬のスパイクアクセスだったのか
状況を見て上限値を手動で見直す運用としたので、上限に達した場合にはSlackに通知するように設定をしました。
検出?通知するためのAWSフローは、API Gateway → CloudWatch Logs → メトリクスフィルター → CloudWatch アラーム → SNS...という感じです。SNS以降はLambdaに流すもよし、Chatbotに流すもよし、運用者にとって便利な方法を選ぶのが良いと思います。
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に落とし込むのに苦戦しました。
苦戦ばかりでしたが各機能への理解が深まると、本当に良くできたサービスだと思うようになり、楽しくなりハマっていきました。
最後に、もし同じようなことでつまづいていらっしゃる方がいましたら、参考になればと幸いです。