CloudFront Hosting ToolkitをAWS CDKのL3 Constructとして使ってみた
CloudFront Hosting ToolkitをAWS CDKのL3 Constructとして使用したい
こんにちは、のんピ(@non____97)です。
皆さんはCloudFront Hosting ToolkitをAWS CDKのL3 Constructとして使用したいなと思ったことはありますか? 私はあります。
先日、CloudFront Hosting Toolkitという、CLIまたはAWS CDKでフロントエンドのホスティングとCI/CDパイプラインを用意するツールを紹介しました。詳細は以下記事をご覧ください。
こちらのツールはAWS CDKのL3 Constructとして使用することも可能です。L3 Constructとして使用することで、周辺環境もまとめてデプロイすることが可能です。
実際に試してみたので紹介します。
いきなりまとめ
- AWS CDKのL3 Constructとして使用すれば、5分ほどでフロントエンドのホスティングとCI/CDパイプラインを用意することができる
- CloudFront Hosting ToolkitをAWS CDKのL3 Constructとして使用する際は以下に注意
- Lambda Layerで使用するAWS CRTのコードが正しくバンドルされるように、別途Lambda Layerで使用するパッケージを定義しているディレクトリで
npm ci
を叩く必要がある - ドメイン名やRoute 53 Hosted ZoneのIDを指定するだけでは、カスタムドメインの設定はされない
- 手動でACMで発行した証明書のARNを指定する必要がある
- ALIASレコードの設定も手動で行う必要がある
- Lambda Layerで使用するAWS CRTのコードが正しくバンドルされるように、別途Lambda Layerで使用するパッケージを定義しているディレクトリで
やってみた
デプロイするWebアプリケーション
CloudFront Hosting ToolkitでデプロイするWebアプリケーションはNext.jsの以下サンプルを使用します。
Chapter 5のNavigating Between Pagesまで完了させています。
また、ビルド時に画像を最適化するためにNext Export Optimize Images
を使用しました。
用意したコードは以下GitHubリポジトリに保存しています。
ローカルで動作することを確認しましょう。
pnpm dev
で起動します。
$ pnpm dev
> @ dev /<作業ディレクトリ>
> next dev
info - [next-export-optimize-images]: Configuration loaded from `/<作業ディレクトリ>/export-images.config.js`.
info - [next-export-optimize-images]: Configuration loaded from `/<作業ディレクトリ>/export-images.config.js`.
▲ Next.js 15.0.0-rc.0
- Local: http://localhost:3000
✓ Starting...
✓ Ready in 1820ms
localhost:3000
にアクセスすると以下のように表示されます。
/dashboard/customers
にアクセスすると、以下のように表示されます。
アクセスした際、裏側では以下のようにコンパイルが走っています。
○ Compiling / ...
✓ Compiled / in 2.3s (573 modules)
GET / 200 in 2517ms
✓ Compiled in 231ms (281 modules)
✓ Compiled /dashboard/customers in 332ms (571 modules)
GET /dashboard/customers 200 in 443ms
L3 Constructで指定できるパラメーター
CloudFront Hosting ToolkitをAWS CDKのL3 Constructとして使用するにあたって、定義を確認します。
export * from './hosting';
export * from './repository_connection';
./hosting
と./repository_connection
のみのようです。
それぞれの定義は以下のとおりです。
import { Construct } from "constructs";
import { HostingConfiguration } from "../bin/cli/shared/types";
interface IParamProps {
hostingConfiguration: HostingConfiguration;
buildFilePath: string;
cffSourceFilePath: string;
connectionArn?: string;
certificateArn?: string;
}
/**
* Custom CDK Construct for hosting resources.
*
* This construct sets up hosting based on the provided configuration,
* build file path, and optional connection and certificate ARNs.
*
* @param scope - The Construct scope in which this construct is defined.
* @param id - The identifier for this construct within the scope.
* @param params - Parameters for configuring hosting resources.
* - `configuration` (required): The IConfiguration object representing the hosting configuration.
* - `buildFilePath` (required): The path to the build file for the hosting resources.
* - `connectionArn` (optional): The ARN of the connection resource (if applicable).
* - `certificateArn` (optional): The ARN of the certificate resource (if applicable).
*/
export declare class Hosting extends Construct {
constructor(scope: Construct, id: string, params: IParamProps);
}
export {};
import { Construct } from "constructs";
import { HostingConfiguration } from "../bin/cli/shared/types";
/**
* Custom CDK Construct for setting up a repository connection.
*
* This construct creates a connection to a hosting repository using the provided configuration.
*
* @param scope - The Construct scope in which this construct is defined.
* @param id - The identifier for this construct within the scope.
* @param params - Parameters for configuring the repository connection.
* - `repoUrl` (optional): The URL of the hosting repository.
* - `branchName` (optional): The name of the branch in the repository.
* - `framework` (optional): The framework used for hosting.
* - `s3bucket` (optional): The name of the Amazon S3 bucket for hosting content.
* - `s3path` (optional): The path within the S3 bucket where content is stored.
* - `domainName` (optional): The domain name associated with the hosting.
* - `hostedZoneId` (optional): The ID of the Route 53 hosted zone associated with the domain.
*/
export declare class RepositoryConnection extends Construct {
readonly connectionArn: string;
readonly repoUrl: string;
constructor(scope: Construct, id: string, hostingConfiguration: HostingConfiguration);
}
指定できるパラメーターとしては多くないですね。自作したCloudFrontディストリビューションのConstructを渡すといったことは現時点ではできないようです。
それぞれで共通利用される定義は以下のとおりです。
#!/usr/bin/env node
export interface CDKCommand {
label: string;
cmd: any;
}
export type CommonAttributes = {
domainName?: string;
hostedZoneId?: string;
};
export type HostingConfiguration = ({
repoUrl: string;
branchName: string;
framework: string;
} & CommonAttributes) | ({
s3bucket: string;
s3path: string;
} & CommonAttributes);
export interface IChoice {
title: string;
value: string;
}
export interface IConnection {
arn: string;
name: string;
region: string;
}
export interface IHosting {
domain: string;
source: string;
type: string;
pipeline: string;
}
export interface Dictionary<T> {
[key: string]: T;
}
export interface PackageJson {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
scripts?: Record<string, string>;
}
export declare const FrontendFramework: {
REACT: string;
VUE: string;
ANGULAR: string;
NEXT: string;
BASIC: string;
};
export interface CNAMES {
key: string;
value: any;
}
export interface PromptItems {
title: string;
value: string;
}
デプロイメントの初期化
デプロイメントの初期化をして、CloudFront Functionsのコードなどを生成します。
$ npx cloudfront-hosting-toolkit init
--------------------- Static hosting configuration wizard : GitHub Source Code Repository Based -------------------
To facilitate the deployment of the necessary infrastructure for website hosting, certain information is required.
cloudfront-hosting-toolkit will aim to find as much relevant data as possible.
Collecting information about the GitHub repository from /<AWS CDK作業ディレクトリ>
✔ Please provide your GitHub repository URL … https://github.com/non-97/nextjs-dashboard.git
✔ What is the name of the branch you would like to use? Hit Enter to confirm or change the selection. … main
Collecting information about the frontend framework used to enable the provision of the appropriate build configuration.
✔ Which framework did you use for website construction? Press Enter to confirm or change the selection. › Next.js Framework
✔ Do you own a domain name that you would like to use? › Yes
✔ Please provide your domain name in the following formats: www.mydomainname.com or mydomainname.com ? … cf-hosting-toolkit.non-97.net
✔ Where is the authoritative DNS server of this domain? › Route 53 in this AWS Account
✔ Please type the hosted zone ID … Z05845813ALGIYU6AAH34
----------------------------------------------------
Here is the configuration that has been generated and saved to cloudfront-hosting-toolkit/cloudfront-hosting-toolkit-config.json file.:
> GitHub repository: https://github.com/non-97/nextjs-dashboard.git/main
> Framework: nextjs
> Domain name: cf-hosting-toolkit.non-97.net
> Hosted zone ID: Z05845813ALGIYU6AAH34
--
> Configuration file generated /<AWS CDK作業ディレクトリ>/cloudfront-hosting-toolkit/cloudfront-hosting-toolkit-config.json
> Build configuration generated /<AWS CDK作業ディレクトリ>/cloudfront-hosting-toolkit/cloudfront-hosting-toolkit-build.yml
> CloudFront Function source code generated /<AWS CDK作業ディレクトリ>/cloudfront-hosting-toolkit/cloudfront-hosting-toolkit-cff.js
The initialization process has been completed. You may now execute 'cloudfront-hosting-toolkit deploy' to deploy the infrastructure.
生成されたファイルは以下のとおりです。
{
"repoUrl": "https://github.com/non-97/nextjs-dashboard.git",
"branchName": "main",
"framework": "nextjs",
"domainName": "cf-hosting-toolkit.non-97.net",
"hostedZoneId": "Z05845813ALGIYU6AAH34"
}
version: 0.2
phases:
build:
commands:
- n install 18.17.0
- npx npm install
- npx next build
- npx next export
- cd out
- echo aws s3 cp ./ s3://$DEST_BUCKET_NAME/$CODEBUILD_RESOLVED_SOURCE_VERSION/ --recursive #don't change this line
- aws s3 cp ./ s3://$DEST_BUCKET_NAME/$CODEBUILD_RESOLVED_SOURCE_VERSION/ --recursive #don't change this line
import cf from 'cloudfront';
const kvsId = '__KVS_ID__';
// This fails if the key value store is not associated with the function
const kvsHandle = cf.kvs(kvsId);
function pointsToFile(uri) {
return /\/[^/]+\.[^/]+$/.test(uri);
}
var rulePatterns = {
"/$": "/index.html", // When URI ends with a '/', append 'index.html'
"!file": ".html", // When URI doesn't point to a specific file and doesn't have a trailing slash, append '.html'
"!file/": "/index.html",// When URI has a trailing slash and doesn't point to a specific file, append 'index.html'
};
// Function to determine rule and update the URI
async function updateURI(uri) {
let pathToAdd = "";
try {
pathToAdd = await kvsHandle.get("path");
} catch (err) {
console.log(`No key 'path' present : ${err}`);
return uri;
}
// Check for trailing slash and apply rule.
if (uri.endsWith("/") && rulePatterns["/$"]) {
return "/" + pathToAdd + uri.slice(0, -1) + rulePatterns["/$"];
}
// Check if URI doesn't point to a specific file.
if (!pointsToFile(uri)) {
// If URI doesn't have a trailing slash, apply rule.
if (!uri.endsWith("/") && rulePatterns["!file"]) {
return "/" + pathToAdd + uri + rulePatterns["!file"];
}
// If URI has a trailing slash, apply rule.
if (uri.endsWith("/") && rulePatterns["!file/"]) {
return "/" + pathToAdd + uri.slice(0, -1) + rulePatterns["!file/"];
}
}
return "/" + pathToAdd + uri;
}
// Main CloudFront handler
async function handler(event) {
var request = event.request;
var uri = request.uri;
//console.log("URI BEFORE: " + request.uri); // Uncomment if needed
request.uri = await updateURI(uri);
//console.log("URI AFTER: " + request.uri); // Uncomment if needed
return request;
}
なお、いずれもawslabs/cloudfront-hosting-toolkit/resources/build_config_templates
やawslabs/cloudfront-hosting-toolkit/resources/cff_templates
に保存されています。
Node.js 20やpnpm
を使用したかったのでbuildspac.yml
のみ修正しました。修正後は以下のとおりです。
version: 0.2
env:
variables:
NEXT_EXPORT: true
phases:
install:
runtime-versions:
nodejs: 20
build:
commands:
- npm install -g pnpm
- pnpm i
- pnpm export
- cd out
- echo aws s3 cp ./ s3://$DEST_BUCKET_NAME/$CODEBUILD_RESOLVED_SOURCE_VERSION/ --recursive #don't change this line
- aws s3 cp ./ s3://$DEST_BUCKET_NAME/$CODEBUILD_RESOLVED_SOURCE_VERSION/ --recursive #don't change this line
L3 Constructのプロパティでこれらのファイルを指定します。RepositoryConnection()
のプロパティはTypeScriptファイル内で直接指定しました。
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { Hosting, RepositoryConnection } from "@aws/cloudfront-hosting-toolkit";
import * as path from "path";
export class WebsiteStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const config = {
repoUrl: "https://github.com/non-97/nextjs-dashboard.git",
branchName: "main",
framework: "nextjs",
domainName: "cf-hosting-toolkit.non-97.net",
hostedZoneId: "Z05845813ALGIYU6AAH34",
};
const repositoryConnection = new RepositoryConnection(
this,
"RepositoryConnection",
config
);
const hosting = new Hosting(this, "Hosting", {
hostingConfiguration: config,
buildFilePath: path.join(
__dirname,
"../cloudfront-hosting-toolkit/cloudfront-hosting-toolkit-build.yml"
),
connectionArn: repositoryConnection.connectionArn,
cffSourceFilePath: path.join(
__dirname,
"../cloudfront-hosting-toolkit/cloudfront-hosting-toolkit-cff.js"
),
});
}
}
最新のコードは以下GitHubリポジトリに保存しています。
デプロイ
デプロイします。
npx cdk diff
の結果は以下のとおりです。
$ npx cdk diff
Stack WebsiteStack
IAM Statement Changes
┌───┬──────────────────────────────────────────────────────┬────────┬──────────────────────────────────────────────────────┬──────────────────────────────────────────────────────┬─────────────────────────────────────────────────────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼──────────────────────────────────────────────────────┼────────┼──────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────┤
.
.
(中略)
.
.
Parameters
[+] Parameter BootstrapVersion BootstrapVersion: {"Type":"AWS::SSM::Parameter::Value<String>","Default":"/cdk-bootstrap/hnb659fds/version","Description":"Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"}
Mappings
[+] Mapping LatestNodeRuntimeMap LatestNodeRuntimeMap: {"af-south-1":{"value":"nodejs20.x"},"ap-east-1":{"value":"nodejs20.x"},"ap-northeast-1":{"value":"nodejs20.x"},"ap-northeast-2":{"value":"nodejs20.x"},"ap-northeast-3":{"value":"nodejs20.x"},"ap-south-1":{"value":"nodejs20.x"},"ap-south-2":{"value":"nodejs20.x"},"ap-southeast-1":{"value":"nodejs20.x"},"ap-southeast-2":{"value":"nodejs20.x"},"ap-southeast-3":{"value":"nodejs20.x"},"ap-southeast-4":{"value":"nodejs20.x"},"ca-central-1":{"value":"nodejs20.x"},"cn-north-1":{"value":"nodejs18.x"},"cn-northwest-1":{"value":"nodejs18.x"},"eu-central-1":{"value":"nodejs20.x"},"eu-central-2":{"value":"nodejs20.x"},"eu-north-1":{"value":"nodejs20.x"},"eu-south-1":{"value":"nodejs20.x"},"eu-south-2":{"value":"nodejs20.x"},"eu-west-1":{"value":"nodejs20.x"},"eu-west-2":{"value":"nodejs20.x"},"eu-west-3":{"value":"nodejs20.x"},"il-central-1":{"value":"nodejs20.x"},"me-central-1":{"value":"nodejs20.x"},"me-south-1":{"value":"nodejs20.x"},"sa-east-1":{"value":"nodejs20.x"},"us-east-1":{"value":"nodejs20.x"},"us-east-2":{"value":"nodejs20.x"},"us-west-1":{"value":"nodejs20.x"},"us-west-2":{"value":"nodejs20.x"}}
Conditions
[+] Condition CDKMetadata/Condition CDKMetadataAvailable: {"Fn::Or":[{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"af-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ca-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-northwest-1"]}]},{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-3"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"il-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"me-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"me-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"sa-east-1"]}]},{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-2"]}]}]}
Resources
[+] AWS::CodeStarConnections::Connection RepositoryConnection/MyCfnConnectionnextjs-dashboard RepositoryConnectionMyCfnConnectionnextjsdashboard5BBC15B1
[+] AWS::SSM::Parameter RepositoryConnection/SSMConnectionArn RepositoryConnectionSSMConnectionArn1E74102B
[+] AWS::SSM::Parameter RepositoryConnection/SSMConnectionName RepositoryConnectionSSMConnectionNameAB648C27
[+] AWS::SSM::Parameter RepositoryConnection/SSMConnectionRegion RepositoryConnectionSSMConnectionRegion4029CE52
[+] AWS::CloudFront::KeyValueStore Hosting/UriStore HostingUriStoreC5D8CAEE
[+] AWS::CloudFront::Function Hosting/ChangeUri HostingChangeUri1A688977
[+] AWS::S3::Bucket Hosting/HostingInfrastructure/HostingBucket HostingHostingInfrastructureHostingBucketBB444313
[+] AWS::S3::BucketPolicy Hosting/HostingInfrastructure/HostingBucket/Policy HostingHostingInfrastructureHostingBucketPolicy905DD8E2
[+] AWS::CloudFront::ResponseHeadersPolicy Hosting/HostingInfrastructure/ResponseHeadersPolicy HostingHostingInfrastructureResponseHeadersPolicy630F57EC
[+] AWS::CloudFront::CachePolicy Hosting/HostingInfrastructure/DefaultCachePolicy HostingHostingInfrastructureDefaultCachePolicy266661D2
[+] AWS::CloudFront::CachePolicy Hosting/HostingInfrastructure/ImagesCachePolicy HostingHostingInfrastructureImagesCachePolicyFF009C20
[+] AWS::CloudFront::CachePolicy Hosting/HostingInfrastructure/staticAssetsCachePolicy HostingHostingInfrastructurestaticAssetsCachePolicy6B67ED12
[+] AWS::CloudFront::OriginAccessControl Hosting/HostingInfrastructure/OAC HostingHostingInfrastructureOACF290641F
[+] AWS::CloudFront::Distribution Hosting/HostingInfrastructure/Distribution HostingHostingInfrastructureDistribution2FE26CAE
[+] AWS::SSM::Parameter Hosting/HostingInfrastructure/SSMConnectionRegion HostingHostingInfrastructureSSMConnectionRegionA72A0577
[+] AWS::IAM::Role Hosting/PipelineInfrastructure/UpdateCFF/BasicLambdaRole HostingPipelineInfrastructureUpdateCFFBasicLambdaRole6E5EC6AE
[+] AWS::IAM::Policy Hosting/PipelineInfrastructure/UpdateCFF/BasicLambdaRole/DefaultPolicy HostingPipelineInfrastructureUpdateCFFBasicLambdaRoleDefaultPolicy58B6752A
[+] AWS::Lambda::LayerVersion Hosting/PipelineInfrastructure/UpdateCFF/AwsSdkLayer HostingPipelineInfrastructureUpdateCFFAwsSdkLayerEA1E386E
[+] AWS::Lambda::Function Hosting/PipelineInfrastructure/UpdateCFF/UpdateKvsFunction HostingPipelineInfrastructureUpdateCFFUpdateKvsFunction85F93F9D
[+] Custom::LogRetention Hosting/PipelineInfrastructure/UpdateCFF/UpdateKvsFunction/LogRetention HostingPipelineInfrastructureUpdateCFFUpdateKvsFunctionLogRetentionCCFB0174
[+] AWS::Lambda::Function Hosting/PipelineInfrastructure/UpdateCFF/DeleteOldDeployments HostingPipelineInfrastructureUpdateCFFDeleteOldDeployments1E3F3EDE
[+] Custom::LogRetention Hosting/PipelineInfrastructure/UpdateCFF/DeleteOldDeployments/LogRetention HostingPipelineInfrastructureUpdateCFFDeleteOldDeploymentsLogRetentionA1B2D84E
[+] AWS::Logs::LogGroup Hosting/PipelineInfrastructure/UpdateCFF/sfnLog HostingPipelineInfrastructureUpdateCFFsfnLog3A570214
[+] AWS::IAM::Role Hosting/PipelineInfrastructure/UpdateCFF/StaticHostingSF/Role HostingPipelineInfrastructureUpdateCFFStaticHostingSFRoleC8BF2D67
[+] AWS::IAM::Policy Hosting/PipelineInfrastructure/UpdateCFF/StaticHostingSF/Role/DefaultPolicy HostingPipelineInfrastructureUpdateCFFStaticHostingSFRoleDefaultPolicyE080F905
[+] AWS::StepFunctions::StateMachine Hosting/PipelineInfrastructure/UpdateCFF/StaticHostingSF HostingPipelineInfrastructureUpdateCFFStaticHostingSF1C3F6163
[+] AWS::Lambda::LayerVersion Hosting/PipelineInfrastructure/InitialPage/AwsCliLayer HostingPipelineInfrastructureInitialPageAwsCliLayer9A51F97C
[+] Custom::CDKBucketDeployment Hosting/PipelineInfrastructure/InitialPage/CustomResource HostingPipelineInfrastructureInitialPageCustomResource34A55526
[+] AWS::S3::Bucket Hosting/PipelineInfrastructure/ArtifactBucket HostingPipelineInfrastructureArtifactBucket000BDB60
[+] AWS::S3::BucketPolicy Hosting/PipelineInfrastructure/ArtifactBucket/Policy HostingPipelineInfrastructureArtifactBucketPolicy55FE49F0
[+] Custom::S3AutoDeleteObjects Hosting/PipelineInfrastructure/ArtifactBucket/AutoDeleteObjectsCustomResource HostingPipelineInfrastructureArtifactBucketAutoDeleteObjectsCustomResource8CA26117
[+] AWS::IAM::Role Hosting/PipelineInfrastructure/MyPipeline/Role HostingPipelineInfrastructureMyPipelineRoleB18081B5
[+] AWS::IAM::Policy Hosting/PipelineInfrastructure/MyPipeline/Role/DefaultPolicy HostingPipelineInfrastructureMyPipelineRoleDefaultPolicy804931F0
[+] AWS::CodePipeline::Pipeline Hosting/PipelineInfrastructure/MyPipeline HostingPipelineInfrastructureMyPipelineFC5FDDD0
[+] AWS::IAM::Role Hosting/PipelineInfrastructure/MyPipeline/Sources/GitHub-Source-nextjs-dashboard/CodePipelineActionRole HostingPipelineInfrastructureMyPipelineSourcesGitHubSourcenextjsdashboardCodePipelineActionRoleC4013751
[+] AWS::IAM::Policy Hosting/PipelineInfrastructure/MyPipeline/Sources/GitHub-Source-nextjs-dashboard/CodePipelineActionRole/DefaultPolicy HostingPipelineInfrastructureMyPipelineSourcesGitHubSourcenextjsdashboardCodePipelineActionRoleDefaultPolicy17547051
[+] AWS::IAM::Role Hosting/PipelineInfrastructure/MyPipeline/Build/Build-And-Copy-to-S3-nextjs-dashboard/CodePipelineActionRole HostingPipelineInfrastructureMyPipelineBuildBuildAndCopytoS3nextjsdashboardCodePipelineActionRole07513F10
[+] AWS::IAM::Policy Hosting/PipelineInfrastructure/MyPipeline/Build/Build-And-Copy-to-S3-nextjs-dashboard/CodePipelineActionRole/DefaultPolicy HostingPipelineInfrastructureMyPipelineBuildBuildAndCopytoS3nextjsdashboardCodePipelineActionRoleDefaultPolicy34DDAFB2
[+] AWS::IAM::Role Hosting/PipelineInfrastructure/MyPipeline/ChangeUri/Invoke/CodePipelineActionRole HostingPipelineInfrastructureMyPipelineChangeUriInvokeCodePipelineActionRole87BB8ADC
[+] AWS::IAM::Policy Hosting/PipelineInfrastructure/MyPipeline/ChangeUri/Invoke/CodePipelineActionRole/DefaultPolicy HostingPipelineInfrastructureMyPipelineChangeUriInvokeCodePipelineActionRoleDefaultPolicyE3E98FC9
[+] AWS::Logs::LogGroup Hosting/PipelineInfrastructure/MyLogGroup HostingPipelineInfrastructureMyLogGroupF5049128
[+] AWS::IAM::Role Hosting/PipelineInfrastructure/Project/Role HostingPipelineInfrastructureProjectRole0A86D4CE
[+] AWS::IAM::Policy Hosting/PipelineInfrastructure/Project/Role/DefaultPolicy HostingPipelineInfrastructureProjectRoleDefaultPolicyC3411297
[+] AWS::CodeBuild::Project Hosting/PipelineInfrastructure/Project HostingPipelineInfrastructureProject873982A1
[+] AWS::SSM::Parameter Hosting/PipelineInfrastructure/SSMPipelineName HostingPipelineInfrastructureSSMPipelineName8B6DD00A
[+] AWS::IAM::Role LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB
[+] AWS::IAM::Policy LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole/DefaultPolicy LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB
[+] AWS::Lambda::Function LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A
[+] AWS::IAM::Role Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265
[+] AWS::IAM::Policy Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF
[+] AWS::Lambda::Function Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536
[+] AWS::IAM::Role Custom::S3AutoDeleteObjectsCustomResourceProvider/Role CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092
[+] AWS::Lambda::Function Custom::S3AutoDeleteObjectsCustomResourceProvider/Handler CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F
Outputs
[+] Output RepositoryConnection/ConnectionArn RepositoryConnectionConnectionArn087CB8E2: {"Value":{"Fn::GetAtt":["RepositoryConnectionMyCfnConnectionnextjsdashboard5BBC15B1","ConnectionArn"]}}
[+] Output RepositoryConnection/ConnectionName RepositoryConnectionConnectionNameFD2EABB9: {"Value":"nextjs-dashboard-main-non-97"}
[+] Output RepositoryConnection/HostingRegion RepositoryConnectionHostingRegion1B4A18AD: {"Value":{"Ref":"AWS::Region"}}
[+] Output Hosting/HostingInfrastructure/DomainName HostingHostingInfrastructureDomainNameF9962E64: {"Value":{"Fn::Join":["",["https://",{"Fn::GetAtt":["HostingHostingInfrastructureDistribution2FE26CAE","DomainName"]}]]}}
[+] Output Hosting/PipelineInfrastructure/PipelineName HostingPipelineInfrastructurePipelineNameC86AA64C: {"Value":{"Ref":"HostingPipelineInfrastructureMyPipelineFC5FDDD0"}}
Other Changes
[+] Unknown Rules: {"CheckBootstrapVersion":{"Assertions":[{"Assert":{"Fn::Not":[{"Fn::Contains":[["1","2","3","4","5"],{"Ref":"BootstrapVersion"}]}]},"AssertDescription":"CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."}]}}
✨ Number of stacks with differences: 1
大量ですね。CLIの場合は、スタックがいくつかに分かれていますが、L3 Constructの場合は一つのスタックにまとめて定義することが可能です。
npx cdk deploy
でデプロイします。
$ npx cdk deploy
✨ Synthesis time: 16.69s
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:
IAM Statement Changes
┌───┬──────────────────────────────────────────────────────┬────────┬──────────────────────────────────────────────────────┬──────────────────────────────────────────────────────┬─────────────────────────────────────────────────────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼──────────────────────────────────────────────────────┼────────┼──────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ + │ ${Custom::CDKBucketDeployment8693BB64968944B69AAFB0C │ Allow │ sts:AssumeRole │ Service:lambda.amazonaws.com │ │
.
.
(中略)
.
.
Do you wish to deploy these changes (y/n)? y
WebsiteStack: deploying... [1/1]
WebsiteStack: creating CloudFormation changeset...
✅ WebsiteStack
✨ Deployment time: 291.2s
Outputs:
WebsiteStack.HostingHostingInfrastructureDomainNameF9962E64 = https://d2uctbvmq5rfn4.cloudfront.net
WebsiteStack.HostingPipelineInfrastructurePipelineNameC86AA64C = non-97-nextjs-dashboard
WebsiteStack.RepositoryConnectionConnectionArn087CB8E2 = arn:aws:codestar-connections:us-east-1:<AWSアカウントID>:connection/77713fc5-903b-4df7-8f50-9e87bda64f20
WebsiteStack.RepositoryConnectionConnectionNameFD2EABB9 = nextjs-dashboard-main-non-97
WebsiteStack.RepositoryConnectionHostingRegion1B4A18AD = us-east-1
Stack ARN:
arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/WebsiteStack/8b7347c0-384d-11ef-a7d6-0affcdd81315
✨ Total time: 307.89s
5分ほどで完了しました。
CodePipelineを確認すると、Sources
ステージで失敗していました。
ChangeUri
まで完了していないため、CloudFront FunctionsのKeyValueStoresは空になっています。
Sources
ステージで失敗したのはGitHubリポジトリとの連携が完了していないためです。
連携状態を確認すると、ステータスが保留中になっています。
前回の記事を参考にGitHubと連携をします。(GitHub側の操作は割愛)
GitHubリポジトリとの連携が完了したのち、先ほど失敗したパイプラインで変更をリリースします。
次はChangeUri
で失敗していました。
StepFunctionsのステートマシンを確認すると、前回の記事同様AWS CRTのバイナリがないというエラーが出力されていました。
これは修正が必要そうです。
なお、Build
ステージは正常に終了しており、S3バケットにビルドした結果が保存されていました。
ただし、ChangeUri
ステージが完了していないため、アクセスをしてもデフォルトのコンテンツを向いたままです。
$ curl https://d2uctbvmq5rfn4.cloudfront.net
<!DOCTYPE html>
<html>
<head>
<link rel="shortcut icon" href="#" />
<meta http-equiv="refresh" content="10">
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<title>Your website is currently being deployed</title>
<style type="text/css">
body { text-align: center; padding: 150px; }
h1 { font-size: 40px; }
body { font: 20px Helvetica, sans-serif; color: #333; }
#article { display: block; text-align: center; width: 1000px; margin: 0 auto; }
a { color: #dc8100; text-decoration: none; }
a:hover { color: #333; text-decoration: none; }
</style>
</head>
<body>
<div id="article">
<h1>Deployment status</h1>
<div>
<p>Please note that your are currently seeing this screen because this is the first deployment of the website. So, just take it easy and unwind, the page will automatically refresh on its own.</p>
</div>
</div>
</html>⏎
Lambda Layerでバンドルするパッケージを手動でインストールして再トライ
Lambda Layerでバンドルするパッケージを手動でインストールして再トライします。ちなみに、CloudFront Hosting Toolkitをグローバルインストールしても特に結果は変わりませんでした。
./node_modules/@aws/cloudfront-hosting-toolkit/lambda/layers/aws_sdk/nodejs/
を確認すると、エラーメッセージに記載されている./aws-crt/dist/bin
は存在しません。
$ cd node_modules/@aws/cloudfront-hosting-toolkit/lambda/layers/aws_sdk/nodejs/
$ ls -l
total 192
drwxr-xr-x@ 74 <OSユーザー名> staff 2368 7 3 07:20 node_modules/
-rw-r--r--@ 1 <OSユーザー名> staff 92858 7 3 07:20 package-lock.json
-rw-r--r--@ 1 <OSユーザー名> staff 540 7 3 07:20 package.json
$ cat package.json
{
"name": "AWS_SDK_Layer",
"version": "1.0.0",
"description": "Latest AWS SDK libs",
"main": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"@aws-sdk/client-cloudfront-keyvaluestore": "^3.511.0",
"@aws-sdk/signature-v4-crt": "^3.511.0",
"@aws-sdk/signature-v4-multi-region": "^3.511.0"
},
"author": {
"name": "Amazon Web Services",
"url": "https://aws.amazon.com/solutions"
},
"license": "Apache-2.0"
}
$ ls -l node_modules/aws-crt/dist/
total 8
drwxr-xr-x@ 18 <OSユーザー名> staff 576 7 3 07:20 common/
-rw-r--r--@ 1 <OSユーザー名> staff 2948 7 3 07:20 index.js
drwxr-xr-x@ 19 <OSユーザー名> staff 608 7 3 07:20 native/
手動でインストールします。
$ npm ci
added 159 packages, and audited 160 packages in 5s
10 packages are looking for funding
run `npm fund` for details
1 high severity vulnerability
To address all issues, run:
npm audit fix
Run `npm audit` for details.
$ ls -l node_modules/aws-crt/dist/
total 24
drwxr-xr-x@ 8 <OSユーザー名> staff 256 7 3 07:43 bin/
drwxr-xr-x@ 50 <OSユーザー名> staff 1600 7 3 07:43 common/
-rw-r--r--@ 1 <OSユーザー名> staff 809 7 3 07:43 index.d.ts
-rw-r--r--@ 1 <OSユーザー名> staff 2948 7 3 07:43 index.js
-rw-r--r--@ 1 <OSユーザー名> staff 563 7 3 07:43 index.js.map
drwxr-xr-x@ 53 <OSユーザー名> staff 1696 7 3 07:43 native/
$ ls -l node_modules/aws-crt/dist/bin/
total 0
drwxr-xr-x@ 3 <OSユーザー名> staff 96 7 3 07:43 darwin-arm64-cruntime/
drwxr-xr-x@ 3 <OSユーザー名> staff 96 7 3 07:43 darwin-x64-cruntime/
drwxr-xr-x@ 3 <OSユーザー名> staff 96 7 3 07:43 linux-arm64-glibc/
drwxr-xr-x@ 3 <OSユーザー名> staff 96 7 3 07:43 linux-x64-glibc/
drwxr-xr-x@ 3 <OSユーザー名> staff 96 7 3 07:43 linux-x64-musl/
drwxr-xr-x@ 3 <OSユーザー名> staff 96 7 3 07:43 win32-x64-cruntime/
お目当てのbin
ディレクトリが生成されました。
再度デプロイしましょう。
$ npx cdk diff
Stack WebsiteStack
Hold on while we create a read-only change set to get a diff with accurate replacement information (use --no-change-set to use a less accurate but faster template-only diff)
Resources
[~] AWS::Lambda::LayerVersion Hosting/PipelineInfrastructure/UpdateCFF/AwsSdkLayer HostingPipelineInfrastructureUpdateCFFAwsSdkLayerEA1E386E replace
└─ [~] Content (requires replacement)
└─ [~] .S3Key:
├─ [-] 8573a0a228ac578823a4d559179cafa3c8b436a4cd8b5fbe88e0c77ef9b6bc49.zip
└─ [+] 9bf43224bde174dac822dcdf2c16fc5861466e13a99cca32ad38c82ce9f21f9b.zip
✨ Number of stacks with differences: 1
$ npx cdk deploy
✨ Synthesis time: 7.75s
WebsiteStack: start: Building 9bf43224bde174dac822dcdf2c16fc5861466e13a99cca32ad38c82ce9f21f9b:current_account-current_region
WebsiteStack: success: Built 9bf43224bde174dac822dcdf2c16fc5861466e13a99cca32ad38c82ce9f21f9b:current_account-current_region
WebsiteStack: start: Building f90fcc9cd945165d7dab242b9e1f577cfae1ff02b15c294f9a09da086ace5f82:current_account-current_region
WebsiteStack: success: Built f90fcc9cd945165d7dab242b9e1f577cfae1ff02b15c294f9a09da086ace5f82:current_account-current_region
WebsiteStack: start: Publishing 9bf43224bde174dac822dcdf2c16fc5861466e13a99cca32ad38c82ce9f21f9b:current_account-current_region
WebsiteStack: start: Publishing f90fcc9cd945165d7dab242b9e1f577cfae1ff02b15c294f9a09da086ace5f82:current_account-current_region
WebsiteStack: success: Published f90fcc9cd945165d7dab242b9e1f577cfae1ff02b15c294f9a09da086ace5f82:current_account-current_region
WebsiteStack: success: Published 9bf43224bde174dac822dcdf2c16fc5861466e13a99cca32ad38c82ce9f21f9b:current_account-current_region
WebsiteStack: deploying... [1/1]
WebsiteStack: creating CloudFormation changeset...
✅ WebsiteStack
✨ Deployment time: 72.17s
Outputs:
WebsiteStack.HostingHostingInfrastructureDomainNameF9962E64 = https://d2uctbvmq5rfn4.cloudfront.net
WebsiteStack.HostingPipelineInfrastructurePipelineNameC86AA64C = non-97-nextjs-dashboard
WebsiteStack.RepositoryConnectionConnectionArn087CB8E2 = arn:aws:codestar-connections:us-east-1:<AWSアカウントID>:connection/77713fc5-903b-4df7-8f50-9e87bda64f20
WebsiteStack.RepositoryConnectionConnectionNameFD2EABB9 = nextjs-dashboard-main-non-97
WebsiteStack.RepositoryConnectionHostingRegion1B4A18AD = us-east-1
Stack ARN:
arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/WebsiteStack/8b7347c0-384d-11ef-a7d6-0affcdd81315
✨ Total time: 79.92s
Lambda Layerが更新されたようですね。
ChangeUri
ステージを再試行すると、正常に実行が完了しました。
ステートマシンも以下のように成功しています。
実際にアクセスしてみましょう。
トップページにアクセスすると画像が表示されていません。
画像を含んでいないページについては特に問題ありません
next/image を next-export-optimize-images/image に置換して再トライ
原因はNext Export Optimize Imagesを使っているにも関わらず、app/page.tsx
でnext/image
を使用していたためです。
next/image
をnext-export-optimize-images/image
に置換して再トライします。
以下のように置換します。
- import Image from 'next/image';
+ import Image from 'next-export-optimize-images/image';
置換後、ローカルでビルドをして、正常に画像が表示されることを確認します。
以下のようにビルドをして、ローカルのWebサーバーを起動させます。
$ pnpm export
> @ export /<作業ディレクトリ>
> NEXT_EXPORT=true next build && next-export-optimize-images
info - [next-export-optimize-images]: Configuration loaded from `/<作業ディレクトリ>/export-images.config.js`.
▲ Next.js 15.0.0-rc.0
Creating an optimized production build ...
✓ Compiled successfully
✓ Linting and checking validity of types
✓ Collecting page data
✓ Generating static pages (7/7)
✓ Collecting build traces
✓ Finalizing page optimization
Route (app) Size First Load JS
┌ ○ / 6.53 kB 104 kB
├ ○ /_not-found 896 B 89.9 kB
├ ○ /dashboard 142 B 89.1 kB
├ ○ /dashboard/customers 142 B 89.1 kB
└ ○ /dashboard/invoices 142 B 89.1 kB
+ First Load JS shared by all 89 kB
├ chunks/539-816c8e2f3535b557.js 35.6 kB
├ chunks/8e36bf0b-94faa33a74e31594.js 51.5 kB
└ other shared chunks (total) 1.9 kB
○ (Static) prerendered as static content
next-export-optimize-images: Optimize images.
info - [next-export-optimize-images]: Configuration loaded from `/<作業ディレクトリ>/export-images.config.js`.
- Collect images in public directory -
- Image Optimization -
Optimization progress |████████████████████████████████████████| 100% || 288/288 Chunks
Cache assets: 288, NonCache assets: 0, Error assets: 0
Successful optimization!
$ npx http-server out
Starting up http-server, serving out
http-server version: 14.1.1
http-server settings:
CORS: disabled
Cache: 3600 seconds
Connection Timeout: 120 seconds
Directory Listings: visible
AutoIndex: visible
Serve GZIP Files: false
Serve Brotli Files: false
Default File Extension: none
Available on:
http://127.0.0.1:8080
http://192.168.3.19:8080
Hit CTRL-C to stop the server
アクセスすると、正常に画像が表示されることを確認できました。
修正したコードをGitHubリポジトリにPushします。
すると、CodePipelineが動き始め、正常に実行が完了しました。
アクセスをすると、今度は正常に画像が表示されました。
ウィンドウの幅を変更をして、表示される画像が切り替わることも確認できました。
カスタムドメインの設定
ここで皆さんお気づきではないでしょうか。
カスタムドメインが設定されていないことに。
CLIの場合はカスタムドメインとRoute 53 Public Hosted ZoneのIDを渡してあげると、ACMで証明書を発行し、CloudFrontディストリビューションでカスタムドメインの設定をしてくれました。
L3 Constructの場合は、そうではないようです。ACMで証明書の発行はされておらず、CloudFrontディストリビューションの代替ドメイン名も設定されていませんでした。
自身でACMの証明書を発行し、L3 Constructのパラメーターとして渡してあげます。
ACMでパブリック証明書をリクエストします。
ここでZone Apexだけでなく、サブドメインwww
も追加したのは以下のエラーを防ぐためです。
13:18:32 | UPDATE_FAILED | AWS::CloudFront::Distribution | HostingHostingInfr...stribution2FE26CAE
Resource handler returned message: "Invalid request provided: The certificate that is attached to your distribution doesn't cover the alternate domain name (CNAME) that you're trying to add. For more details, see: https://docs.aws.amazon.co
m/AmazonCloudFront/latest/DeveloperGuide/CNAMEs.html#alternate-domain-names-requirements (Service: CloudFront, Status Code: 400, Request ID: 3f6f6e0c-e0ee-4dad-b79d-d2161f9560b4)" (RequestToken: b381ec3d-3b3d-100a-f979-a61071ff7cad, Handler
ErrorCode: InvalidRequest)
Route 53に検証用のCNAMEレコードを追加します。
1分ほど待つと証明書の検証が完了します。
証明書のARNをHosting()
のcertificateArn
に指定します。
const hosting = new Hosting(this, "Hosting", {
hostingConfiguration: config,
buildFilePath: path.join(
__dirname,
"../cloudfront-hosting-toolkit/cloudfront-hosting-toolkit-build.yml"
),
connectionArn: repositoryConnection.connectionArn,
cffSourceFilePath: path.join(
__dirname,
"../cloudfront-hosting-toolkit/cloudfront-hosting-toolkit-cff.js"
),
certificateArn:
"arn:aws:acm:us-east-1:<AWSアカウントID>:certificate/0722854d-637f-4f66-a3b0-3a0236a13255",
});
npx cdk diff
を叩いて、差分を確認します。
$ npx cdk diff
Stack WebsiteStack
Hold on while we create a read-only change set to get a diff with accurate replacement information (use --no-change-set to use a less accurate but faster template-only diff)
Resources
[~] AWS::CloudFront::Distribution Hosting/HostingInfrastructure/Distribution HostingHostingInfrastructureDistribution2FE26CAE
└─ [~] DistributionConfig
├─ [+] Added: .Aliases
└─ [+] Added: .ViewerCertificate
✨ Number of stacks with differences: 1
CloudFrontディストリビューションに代替ドメインと証明書の設定が行われるようです。
Route 53 Public HostedにCloudFrontディストリビューションのALIASレコードを追加するまでは行ってくれなさそうなので注意ですね。
npx cdk deploy
でデプロイ後、CloudFrontディストリビューションを確認すると、代替ドメイン名およびカスタム SSL 証明書が設定されたことを確認できました。
一方で、まだALIASレコードは作成されていないため、名前解決はできません。
$ dig cf-hosting-toolkit.non-97.net +short
手動でALIASレコードを登録します。
登録後、名前解決および、カスタムドメインでアクセスできることを確認します。
$ dig cf-hosting-toolkit.non-97.net +short
18.65.216.62
18.65.216.60
18.65.216.92
18.65.216.125
$ curl https://cf-hosting-toolkit.non-97.net -I
HTTP/2 200
date: Wed, 03 Jul 2024 04:58:57 GMT
content-type: text/html
content-length: 10932
last-modified: Wed, 03 Jul 2024 00:34:44 GMT
etag: "60de3a39bfa79ada05241daa142400bf"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
vary: Accept-Encoding
x-cache: Miss from cloudfront
via: 1.1 2944fb08ed200b542920ceadbff2083e.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-P4
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: DTtOvKlk98NxDeAuUotEHC_Sjz4NT8bEIIqUpdur5Q7jO03tHG17qA==
x-xss-protection: 1; mode=block
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000; includeSubDomains
念の為ブラウザからも確認しましたが、問題なくアクセスできました。
5分ほどでフロントエンドのホスティングとCI/CDパイプラインを用意することができる
CloudFront Hosting ToolkitをAWS CDKのL3 Constructとして使ってみました。
多少のケアは必要ですが、5分ほどでフロントエンドのホスティングとCI/CDパイプラインを用意することができるのは非常にありがたいですね。
CloudFrontディストリビューションの細かいカスタマイズはできないので、そこは手動で行うことになるのは注意しましょう。
この記事が誰かの助けになれば幸いです。
以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!