aws cdkでs3にoacを適用しcloud frontからのみ参照出来るようにする

前置き
Next.jsでaws lambda × cloud frontのサーバレス構成にした際にNext.jsから出力されるpublicフォルダをS3に配置したときの備忘録です。
画像などをビルド対象に含めるとプロジェクトが重くなることがあるので静的ファイルはS3に配置しようという考えです。
実装についてはTypeScriptで記載しています。 環境構築などは出来ているものとして書いているので構築方法などは割愛しています。
ゴール
以下の状態とすることを最終的なゴールとします。
- cloud front経由でS3の参照をすることができる
- S3に保存しているファイルを直接参照ができない
実装
プロジェクトの作成
はじめにcdkプロジェクトを作成します。 以降はこのプロジェクト内で作業を行います。
1cd aws
2npx cdk init app --language typescript
AWSへデプロイするための準備
プロジェクトを作成した直後の状態だとAWSへの適用ができません。 「/bin/〇〇.ts」で環境情報を設定する必要があるので調整を行います。 ※〇〇はプロジェクトの作成時に指定したフォルダ名とかになっているはず
1#!/usr/bin/env node
2import * as cdk from 'aws-cdk-lib';
3import { TestStack } from '../lib/test-stack';
4
5const app = new cdk.App();
6new TestStack(app, 'TestStack', {
7 env: {
8 account: 'aws account id', // AWS認証情報
9 region: 'ap-northeast-1',
10 },
11});
12
次にpackage.jsonに以下のscriptを追加します。
1{
2 ...,
3 "scripts": {
4 "build": "tsc",
5 "watch": "tsc -w",
6 "test": "jest",
7 "cdk": "cdk",
8+ "cdk:deploy": "cdk deploy", // これと
9+ "cdk:destroy": "cdk destroy" // これ
10 },
11 ...,
12}
13
上記の実装が終わったら次のコマンドを実行します。
1npm run cdk:deploy
コマンド実行後AWSコンソールへログインを行い「CloudFormation」にスタックが作成されていれば準備完了です。

AWS ECRにCDK上からDocker Imageをデプロイするpackageの追加
必要なライブラリを追加します。
1npm install --save-dev cdk-ecr-deployment
AWS S3を構築するプログラムの実装
「/lib/〇〇-stack.ts」にてAWS環境を構築するためのプログラムを実装するファイルがあるため、S3を構築するための実装を行います。
1import * as cdk from 'aws-cdk-lib';
2import { Construct } from 'constructs';
3import * as s3 from 'aws-cdk-lib/aws-s3';
4
5export class TestStack extends cdk.Stack {
6 constructor(scope: Construct, id: string, props?: cdk.StackProps) {
7 super(scope, id, props);
8
9 // S3バケットの作成
10 const bucket = new s3.Bucket(this, `S3-Bucket`, {
11 removalPolicy: cdk.RemovalPolicy.DESTROY,
12 autoDeleteObjects: true,
13 });
14
15 // S3バケットの出力
16 new cdk.CfnOutput(this, 'S3BucketName', {
17 value: bucket.bucketName,
18 description: 'Name of the S3 bucket',
19 exportName: 'S3BucketName',
20 });
21
22 // S3バケットのURLの出力
23 new cdk.CfnOutput(this, 'S3BucketUrl', {
24 value: bucket.bucketWebsiteUrl,
25 description: 'URL of the S3 bucket',
26 exportName: 'S3BucketUrl',
27 });
28 }
29}
一旦この状態で再度デプロイしてS3が生成されいているか確認してみます。 以下の画像の通りCloudFormationを確認してS3が生成されていれば成功です。 ※この状態だと直接参照を有効にしていないのでファイルをアップしても見れないです。

Cloud Front部分の実装とS3の直接参照を無効化
Cloud Frontの構築部分の処理とS3を直リンクで参照できないようにします。 Cloud Front ⇒ S3間ではOACを設定し実行できるように調整を行います。
1import * as cdk from 'aws-cdk-lib';
2import { Construct } from 'constructs';
3import * as s3 from 'aws-cdk-lib/aws-s3';
4import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
5import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
6
7export class TestStack extends cdk.Stack {
8 constructor(scope: Construct, id: string, props?: cdk.StackProps) {
9 super(scope, id, props);
10
11 // S3バケットの作成
12 const bucket = new s3.Bucket(this, `S3-Bucket`, {
13 removalPolicy: cdk.RemovalPolicy.DESTROY,
14 autoDeleteObjects: true,
15 });
16
17 // S3バケットの出力
18 new cdk.CfnOutput(this, 'S3BucketName', {
19 value: bucket.bucketName,
20 description: 'Name of the S3 bucket',
21 exportName: 'S3BucketName',
22 });
23
24 // S3バケットのURLの出力
25 new cdk.CfnOutput(this, 'S3BucketUrl', {
26 value: bucket.bucketWebsiteUrl,
27 description: 'URL of the S3 bucket',
28 exportName: 'S3BucketUrl',
29 });
30
31 // Cloud Frontの作成
32 const distribution = new cloudfront.Distribution(this, 'MyDistribution', {
33 defaultBehavior: {
34 origin: origins.S3BucketOrigin.withOriginAccessControl(bucket)
35 },
36 });
37
38 // CloudFrontのURLの出力
39 new cdk.CfnOutput(this, 'CloudFrontUrl', {
40 value: distribution.distributionDomainName,
41 description: 'URL of the CloudFront distribution',
42 exportName: 'CloudFrontUrl',
43 });
44 }
45}
再度デプロイを行うとCloud FrontのURLとS3のURLがログで出力されます。 今アクセスしてもファイルが存在しないのでS3に適当な画像ファイルをアップしましょう。 そのあと「各URL/アップしたファイル」にアクセスして以下のように動けば成功です。
- Cloud FrontのURLにアクセス⇒アップした画像が表示
- S3のURLにアクセス⇒AccessDeniedの表示
終わり
以上で「aws cdk でS3にoacを適用しcloud frontからのみ参照出来るようにする」実装が出来ました。
この構成はNuxt3やNextをAWS Lambda × Cloud Front(サーバーレス構成)の構成でも使えるので是非参考にしてもらえればと思います。