はじめに
皆さん、こんにちは。
私は業務でデータ利活用基盤を取り扱っているため、dbtやIceberg、そして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 のスタックです。
アーキテクチャ図でいうと、以下の赤枠の部分にあたります。
CloudFrontWafStack の実体は 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 の定義です。
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
これらは、デプロイオプションで設定することができます。
複数のルールが設定された場合は、すべてを満たす場合のみ許可されるルールを持った WebACL が作成されます。
ここで作成した WebACL の WebAclId が、GenerativeAiUseCasesStack
スタックに渡され、CloudFront ディストリビューションに設定されます。
CloudFrontWafStack > Certificate
Certificate はカスタムドメインアクセス用の ACM 証明書のリソースです。
以下のソースコードが Certificate の定義です。
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
デプロイオプションで以下を指定することにより、カスタムドメインアクセス用の ACM 証明書を作成することができます。
- ホスト名 (Ex. "genai")
- カスタムドメイン名 (Ex. "example.com")
- ホストゾーン ID (Ex. "Z0123456789ABCDEFGHIJ")
ここで作成したカスタムドメインアクセス用の ACM 証明書が、GenerativeAiUseCasesStack
スタックに渡され、CloudFront ディストリビューションに設定されます。
次回は GenU 内の RagKnowledgeBaseStack
スタックを解説していきたいと思います。