レートリミットをAWS CDK でやってみた(WAF使用)

# レートリミットとは

レートリミットとは、リソースの過剰使用やサービス拒否攻撃を防ぐために使用されます。
サーバーへのリクエストの頻度を制御し、特定の時間内にクライアントやIPアドレスからのリクエスト数を制限するために使用されます。

今回はIPアドレスベースでの制限になります。

---
# 背景
最近では[ポケモン徹底攻略](https://yakkun.com/)さんのサイトが大量リクエストで落とされるなど、[DDos攻撃](https://ja.wikipedia.org/wiki/DoS%E6%94%BB%E6%92%83#DDoS%E6%94%BB%E6%92%83)対策も必要ですね。

https://twitter.com/poketettei/status/1595333013378060290

---

基本的に、サーバーサイドを使用するときは、Nestjsを使用していて、[Nestjs](https://docs.nestjs.com/security/rate-limiting)での実装方法もあります。
ですがサーバーサイドで実装すると、

- オリジンにリクエストは届くので、サーバーサイドがスケールしてマシーンリソースを食ってしまう（リソース割きたくない）
- 実装するには、Redisなどを用意して、IPアドレスを保存する仕組みが必要

---


以上を考えると、サーバーサイドにリクエストが届くよりも前段のほうでレートリミットかけたほうがいいので
今回はALBにアタッチします。場合によっては、cloudfrontにもアタッチできると思います。

---

# 参考

クラスメソッドさんで紹介されている、AWS WAF を使用した方法を、AWS CDKのコードに落としてみました。

https://dev.classmethod.jp/articles/tsnote-aws-waf-autoblock/

---

# 環境

```
"typescript": "~4.9.4"
"aws-cdk": "2.60.0",
```

---

# コード

![carbon (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183059/52e1d1e6-886c-a28b-9f54-87ffbf6b2536.png)


<details>
<summary>rate-limit-construct.ts</summary>

```ts:rate-limit-construct.ts
import { ApplicationLoadBalancer } from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import { CfnWebACL, CfnWebACLAssociation } from 'aws-cdk-lib/aws-wafv2';
import { Construct } from 'constructs';

export interface RateLimitConstructProps {
  limit?: number;
  immunityTime?: number;
  searchString?: string;
}

export class RateLimitConstruct extends Construct {
  public acl: CfnWebACL;

  constructor(scope: Construct, id: string, props?: RateLimitConstructProps) {
    super(scope, id);

    const aggregateKeyType = 'IP';
    const nonPriority = 0;
    const minLimit = 100; // See https://docs.aws.amazon.com/ja_jp/waf/latest/developerguide/limits.html
    const minImmunityTime = 300; // See https://docs.aws.amazon.com/ja_jp/waf/latest/APIReference/API_ImmunityTimeProperty.html

    const limit = props?.limit ?? minLimit;
    const immunityTime = props?.immunityTime ?? minImmunityTime;
    const searchString = props?.searchString ?? '/';
    const RuleAction = { Allow: { allow: {} }, Block: { block: {} } };
    const visibilityConfig = {
      metricName: `target:${searchString}`,
      sampledRequestsEnabled: true,
      cloudWatchMetricsEnabled: true,
    };

    this.acl = new CfnWebACL(this, CfnWebACL.name, {
      name: 'PcmStgRateLimit',
      defaultAction: RuleAction.Allow,
      scope: 'REGIONAL',
      visibilityConfig,
      challengeConfig: { immunityTimeProperty: { immunityTime } },
      description: `Limit ${aggregateKeyType} Base.Up to ${limit} Requests Per ${immunityTime} seconds`,
      rules: [
        {
          name: `${aggregateKeyType}BaseLimit${limit}Per${immunityTime}SecondsRule`,
          priority: nonPriority,
          action: RuleAction.Block,
          visibilityConfig,
          statement: {
            rateBasedStatement: {
              limit,
              aggregateKeyType,
              scopeDownStatement: {
                byteMatchStatement: {
                  fieldToMatch: { uriPath: {} },
                  positionalConstraint: 'STARTS_WITH',
                  searchString,
                  textTransformations: [
                    {
                      type: 'NONE',
                      priority: nonPriority,
                    },
                  ],
                },
              },
            },
          },
        },
      ],
    });
  }

  attachToALB(alb: ApplicationLoadBalancer, waf: CfnWebACL) {
    new CfnWebACLAssociation(this, `AttachToALB`, {
      resourceArn: alb.loadBalancerArn,
      webAclArn: waf.attrArn,
    });
  }
}
```
</details>



---

# 上記constructを使った例

![carbon (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183059/649cf93b-5814-11d0-cf96-41ce14d448d1.png)


<details>
<summary>sample-stack.ts</summary>

```ts:sample-stack.ts
import { RateLimitConstruct } from './rate-limit-construct';
import { ApplicationLoadBalancer } from 'aws-cdk-lib/aws-elasticloadbalancingv2';

const loadBalancer = new ApplicationLoadBalancer(this, ApplicationLoadBalancer.name, options);

const rateLimit = new RateLimitConstruct(this, RateLimitConstruct.name);

rateLimit.attachToALB(loadBalancer, rateLimit.acl);

```

</details>

- ApplicationLoadBalancerを初期化する`options`は適宜引数に渡してください。

---

# 参考

- https://dev.classmethod.jp/articles/tsnote-aws-waf-autoblock/#toc-3
- https://docs.aws.amazon.com/cdk/api/v1/docs/aws-wafv2-readme.html