0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-03-18

はじめに

皆さん、こんにちは。

私は業務でデータ利活用基盤を取り扱っているため、dbtIceberg、そしてAWS GenUに取り組む必要があると考えています。特に AWS Japan Top Engineer として、GenUを扱い、その活用を広めることが責務だと感じています。

しかし、私はこれまで CloudFormation を好んで使っており、(逆張り思考も重なって)Cfn テンプレートをシンプルかつ汎用性・拡張性の高い形で作ることに注力してきました。そのため、改めてGenU の CDK コードを読もうとしても、なかなか理解が進みませんでした。

そこで、CDK を学びながら、その過程を記事としてまとめることにしました。

前回までのおさらい

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

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

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

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

CloudFrontWafStack スタック

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

image.png

CloudFrontWafStack の実体は packages/cdk/lib/cloud-front-waf-stack.ts にあります。

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 の定義です。

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 にあります。

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
    • IPv4(CfnIPSet) ルール (Ex. "192.168.0.0/24")
    • IPv6(CfnIPSet) ルール (Ex. "2001:0db8::/32")
    • 国コードルール (Ex. "JP")

これらは、デプロイオプションで設定することができます。
複数のルールが設定された場合は、すべてを満たす場合のみ許可されるルールを持った WebACL が作成されます。

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

CloudFrontWafStack > Certificate

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

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 証明書を作成することができます。

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

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?