GenUのバックエンド (CDK) 詳細解説　⑤CloudFrontWafStack スタックの解説

## はじめに

皆さん、こんにちは。

私は業務でデータ利活用基盤を取り扱っていること、2024 AWS Japan Top Engineer に選出されたということから、[AWS GenU](https://aws-samples.github.io/generative-ai-use-cases-jp/) およびそれに必要なデータ基盤の探求 ([Snowflake](https://www.snowflake.com/ja/), [dbt](https://www.getdbt.com/), [Iceberg](https://iceberg.apache.org/), etc) に取り組む必要があると考えています。

本投稿では、[GenU のバックエンドである CDK コード](https://github.com/aws-samples/generative-ai-use-cases-jp/tree/main/packages/cdk)を詳細に解説します。
自身そして閲覧して頂いた皆様の GenU への理解が少しでも深まり、生成 AI の民主化につながっていければと考えています。

## 前回までのおさらい

前回までで、以下が完了しました。

- [①AWS CDK のセットアップ](https://qiita.com/siruko/items/fd25fdcf89615cb85262)
- [②AWS CDK の動作確認](https://qiita.com/siruko/items/73169f986b4173e3d3a5)
- [③GenU の概要](https://qiita.com/siruko/items/625801b9e1847b305c1e)
- [④GenU CDK スタックの概要](https://qiita.com/siruko/items/8570ed43f7162ea4b907)

GenU の CDK は最大で以下の 6 つの子スタックを作成します。

- `CloudFrontWafStack`
- `RagKnowledgeBaseStack`
- `AgentStack`
- `GuardrailStack`
- `GenerativeAiUseCasesStack` ※メインスタック
- `DashboardStack`

今回は GenU 内の `CloudFrontWafStack` スタックを解説していきたいと思います。

### CloudFrontWafStack スタック

CloudFrontWafStack は WebACL のスタックです。
アーキテクチャ図でいうと、以下の赤枠の部分にあたります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/270561a5-772d-407d-9ca9-55251196eb75.png)

CloudFrontWafStack の実体は [`packages/cdk/lib/cloud-front-waf-stack.ts`](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/main/packages/cdk/lib/cloud-front-waf-stack.ts) にあります。

```typescript:packages/cdk/lib/cloud-front-waf-stack.ts
import { CfnOutput, Stack, StackProps } from 'aws-cdk-lib';
import {
  Certificate,
  CertificateValidation,
  ICertificate,
} from 'aws-cdk-lib/aws-certificatemanager';
import { HostedZone } from 'aws-cdk-lib/aws-route53';
import { Construct } from 'constructs';
import { CommonWebAcl } from './construct/common-web-acl';
import { ProcessedStackInput } from './stack-input';

interface CloudFrontWafStackProps extends StackProps {
  params: ProcessedStackInput;
}

export class CloudFrontWafStack extends Stack {
  public readonly webAclArn: string;
  public readonly webAcl: CommonWebAcl;
  public readonly cert: ICertificate;

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

    const params = props.params;

    if (
      params.allowedIpV4AddressRanges ||
      params.allowedIpV6AddressRanges ||
      params.allowedCountryCodes
    ) {
      const webAcl = new CommonWebAcl(this, `WebAcl${id}`, {
        scope: 'CLOUDFRONT',
        allowedIpV4AddressRanges: params.allowedIpV4AddressRanges,
        allowedIpV6AddressRanges: params.allowedIpV6AddressRanges,
        allowedCountryCodes: params.allowedCountryCodes,
      });

      new CfnOutput(this, 'WebAclId', {
        value: webAcl.webAclArn,
      });
      this.webAclArn = webAcl.webAclArn;
      this.webAcl = webAcl;
    }

    if (params.hostName && params.domainName && params.hostedZoneId) {
      const hostedZone = HostedZone.fromHostedZoneAttributes(
        this,
        'HostedZone',
        {
          hostedZoneId: params.hostedZoneId,
          zoneName: params.domainName,
        }
      );
      const cert = new Certificate(this, 'Cert', {
        domainName: `${params.hostName}.${params.domainName}`,
        validation: CertificateValidation.fromDns(hostedZone),
      });
      this.cert = cert;
    }
  }
}
```

このスタックでは、以下の 2 つのリソースを作成しています。

- `CommonWebAcl`
- `Certificate`

なお、CloudFront ディストリビューションは GenerativeAiUseCasesStack スタックで作成されるため、このスタックでは作成しません。

#### CloudFrontWafStack > CommonWebAcl リソースセット

CommonWebAcl は CloudFront 用 WebACL のリソースセットです。
以下のソースコードが CommonWebAcl の定義です。

```typescript:packages/cdk/lib/cloud-front-waf-stack.ts (抜粋)
if (
  params.allowedIpV4AddressRanges ||
  params.allowedIpV6AddressRanges ||
  params.allowedCountryCodes
) {
  const webAcl = new CommonWebAcl(this, `WebAcl${id}`, {
    scope: 'CLOUDFRONT',
    allowedIpV4AddressRanges: params.allowedIpV4AddressRanges,
    allowedIpV6AddressRanges: params.allowedIpV6AddressRanges,
    allowedCountryCodes: params.allowedCountryCodes,
  });

  new CfnOutput(this, 'WebAclId', {
    value: webAcl.webAclArn,
  });
  this.webAclArn = webAcl.webAclArn;
  this.webAcl = webAcl;
}
```

CommonWebAcl の実体は [`packages/cdk/lib/construct/common-web-acl.ts`](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/main/packages/cdk/lib/construct/common-web-acl.ts) にあります。

```typescript:
import { Lazy, Names } from 'aws-cdk-lib';
import { CfnIPSet, CfnWebACL, CfnWebACLProps } from 'aws-cdk-lib/aws-wafv2';
import { Construct } from 'constructs';

export interface CommonWebAclProps {
  scope: 'REGIONAL' | 'CLOUDFRONT';
  allowedIpV4AddressRanges?: string[] | null;
  allowedIpV6AddressRanges?: string[] | null;
  allowedCountryCodes?: string[] | null;
}

export class CommonWebAcl extends Construct {
  public readonly webAclArn: string;

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

    const suffix = Lazy.string({ produce: () => Names.uniqueId(this) });

    const rules: CfnWebACLProps['rules'] = [];

    const commonRulePropreties = (name: string) => ({
      name,
      action: { allow: {} },
      visibilityConfig: {
        sampledRequestsEnabled: true,
        cloudWatchMetricsEnabled: true,
        metricName: name,
      },
    });

    const generateIpSetRule = (
      priority: number,
      name: string,
      ipSetArn: string
    ): CfnWebACL.RuleProperty => ({
      priority,
      ...commonRulePropreties(name),
      statement: {
        ipSetReferenceStatement: {
          arn: ipSetArn,
        },
      },
    });

    const generateIpSetAndGeoMatchRule = (
      priority: number,
      name: string,
      ipSetArn: string,
      allowedCountryCodes: string[]
    ): CfnWebACL.RuleProperty => ({
      priority,
      ...commonRulePropreties(name),
      statement: {
        // ルール間の条件はOR判定になるので、同一ルール内でAND条件で指定する
        andStatement: {
          statements: [
            {
              ipSetReferenceStatement: {
                arn: ipSetArn,
              },
            },
            {
              geoMatchStatement: {
                countryCodes: allowedCountryCodes,
              },
            },
          ],
        },
      },
    });

    const hasAllowedIpV4 =
      props.allowedIpV4AddressRanges &&
      props.allowedIpV4AddressRanges.length > 0;
    const hasAllowedIpV6 =
      props.allowedIpV6AddressRanges &&
      props.allowedIpV6AddressRanges.length > 0;
    const hasAllowedCountryCodes =
      props.allowedCountryCodes && props.allowedCountryCodes.length > 0;

    // IP v4 と v6 それぞれでルールを定義する
    if (hasAllowedIpV4) {
      const wafIPv4Set = new CfnIPSet(this, `IPv4Set${id}`, {
        ipAddressVersion: 'IPV4',
        scope: props.scope,
        addresses: props.allowedIpV4AddressRanges ?? [],
      });
      if (hasAllowedCountryCodes) {
        // Geo制限を行う場合は、IP制限とのAND条件にする
        rules.push(
          generateIpSetAndGeoMatchRule(
            1,
            `IpV4SetAndGeoMatchRule${id}`,
            wafIPv4Set.attrArn,
            props.allowedCountryCodes ?? []
          )
        );
      } else {
        rules.push(
          generateIpSetRule(1, `IpV4SetRule${id}`, wafIPv4Set.attrArn)
        );
      }
    }

    if (hasAllowedIpV6) {
      const wafIPv6Set = new CfnIPSet(this, `IPv6Set${id}`, {
        ipAddressVersion: 'IPV6',
        scope: props.scope,
        addresses: props.allowedIpV6AddressRanges ?? [],
      });
      if (hasAllowedCountryCodes) {
        // Geo制限を行う場合は、IP制限とのAND条件にする
        rules.push(
          generateIpSetAndGeoMatchRule(
            2,
            `IpV6SetAndGeoMatchRule${id}`,
            wafIPv6Set.attrArn,
            props.allowedCountryCodes ?? []
          )
        );
      } else {
        rules.push(
          generateIpSetRule(2, `IpV6SetRule${id}`, wafIPv6Set.attrArn)
        );
      }
    }

    // IP制限なしのGe制限のみの場合は、Geo制限のルールを定義
    if (!hasAllowedIpV4 && !hasAllowedIpV6 && hasAllowedCountryCodes) {
      const name = `GeoMatchRule${id}`;
      rules.push({
        priority: 3,
        ...commonRulePropreties(name),
        statement: {
          geoMatchStatement: {
            countryCodes: props.allowedCountryCodes ?? [],
          },
        },
      });
    }

    const webAcl = new CfnWebACL(this, `WebAcl${id}`, {
      defaultAction: { block: {} },
      name: `WebAcl-${suffix}`,
      scope: props.scope,
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        sampledRequestsEnabled: true,
        metricName: `WebAcl-${suffix}`,
      },
      rules: rules,
    });
    this.webAclArn = webAcl.attrArn;
  }
}
```

この中では、以下の AWS リソースを生成しています。

- CloudFront ディストリビューション用の[CfnWebACL](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_wafv2.CfnWebACL.html)
  - IPv4([CfnIPSet](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_wafv2.CfnIPSet.html)) ルール (Ex. "192.168.0.0/24")
  - IPv6([CfnIPSet](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_wafv2.CfnIPSet.html)) ルール (Ex. "2001:0db8::/32")
  - 国コードルール (Ex. "JP")

これらは、[デプロイオプション](https://qiita.com/siruko/items/8570ed43f7162ea4b907#cloudfrontwafstack-%E3%81%AE%E4%BD%9C%E6%88%90%E6%9D%A1%E4%BB%B6)で設定することができます。
複数のルールが設定された場合は、すべてを満たす場合のみ許可されるルールを持った WebACL が作成されます。

ここで作成した WebACL の WebAclId が、`GenerativeAiUseCasesStack` スタックに渡され、CloudFront ディストリビューションに設定されます。

#### CloudFrontWafStack > Certificate:

Certificate はカスタムドメインアクセス用の ACM 証明書のリソースです。
以下のソースコードが Certificate の定義です。

```typescript:packages/cdk/lib/cloud-front-waf-stack.ts (抜粋)
if (params.hostName && params.domainName && params.hostedZoneId) {
  const hostedZone = HostedZone.fromHostedZoneAttributes(
    this,
    'HostedZone',
    {
      hostedZoneId: params.hostedZoneId,
      zoneName: params.domainName,
    }
  );
  const cert = new Certificate(this, 'Cert', {
    domainName: `${params.hostName}.${params.domainName}`,
    validation: CertificateValidation.fromDns(hostedZone),
  });
  this.cert = cert;
}
```

ここでは、以下の AWS リソースを生成しています。

- ACM (AWS Certificate Manager) の [Certificate](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_certificatemanager.Certificate.html)

[デプロイオプション](https://qiita.com/siruko/items/8570ed43f7162ea4b907#cloudfrontwafstack-%E3%81%AE%E4%BD%9C%E6%88%90%E6%9D%A1%E4%BB%B6)で以下を指定することにより、カスタムドメインアクセス用の ACM 証明書を作成することができます。

- ホスト名 (Ex. "genai")
- カスタムドメイン名 (Ex. "example.com")
- ホストゾーン ID (Ex. "Z0123456789ABCDEFGHIJ")

ここで作成したカスタムドメインアクセス用の ACM 証明書が、`GenerativeAiUseCasesStack` スタックに渡され、CloudFront ディストリビューションに設定されます。

次回は GenU 内の `RagKnowledgeBaseStack` スタックを解説していきたいと思います。

## (参考) GenU のバックエンド (CDK) 詳細解説投稿一覧

- [①AWS CDK のセットアップ](https://qiita.com/siruko/items/fd25fdcf89615cb85262)
- [②AWS CDK の動作確認](https://qiita.com/siruko/items/73169f986b4173e3d3a5)
- [③GenU の概要](https://qiita.com/siruko/items/625801b9e1847b305c1e)
- [④GenU CDK スタックの概要](https://qiita.com/siruko/items/8570ed43f7162ea4b907)
- [⑤CloudFrontWafStack スタックの解説](https://qiita.com/siruko/items/30439576ee7c63165d21)
- [⑥RagKnowledgeBaseStack スタックの解説](https://qiita.com/siruko/items/1223c9d22e73168a8809)
- [⑦WebSearchAgentStack スタックの解説](https://qiita.com/siruko/items/aef0a9599df60d47eb1e)
- [⑧GuardrailStack スタックの解説](https://qiita.com/siruko/items/d16e9fe27df4673d7554)
- [⑨GenerativeAiUseCasesStack > Auth スタックの解説](https://qiita.com/siruko/items/0c14040a1af132f42382)
- [⑩GenerativeAiUseCasesStack > Database, Api スタックの解説](https://qiita.com/siruko/items/5f249f11847f3829c18a)
- [⑪GenerativeAiUseCasesStack > CommonWebAcl, Web, Rag スタックの解説](https://qiita.com/siruko/items/1063daa2efe0b374a3fc)
- [⑫GenerativeAiUseCasesStack > RagKnowledgeBase, UseCaseBuilder, Transcribe スタックの解説](https://qiita.com/siruko/items/73d6006004caf8648594)
- [⑬DashBoard スタックの解説](https://qiita.com/siruko/items/73a75c7d0146d12ecbc3)
- [⑭GenU の Outputs の解説](https://qiita.com/siruko/items/afc14128a5b1a15ab69c)