AWS CDK で構築する S3 + CloudFront 静的サイトの CI/CD パイプライン(TypeScript)
AWS CDK (TypeScript) を使って、S3 + CloudFront と Code シリーズによる dev / stg / prd の 3 環境 CI/CD パイプラインを構築した際の実装例をまとめます。CloudFront Function によるベーシック認証の設定もあわせて紹介します。

注意: この記事は 2021 年時点の情報をもとに書かれており、AWS CDK v1 系 (
@aws-cdk/aws-*形式のモジュール) の記法を利用しています。現行の CDK v2 ではaws-cdk-libに統合されているため、最新の実装では CDK v2 公式ドキュメントを参照してください。また、AWS CodeCommit は 2024 年 7 月以降、新規利用が停止されています。新規に構築する場合は GitHub 等を利用する想定で読み替えてください。
はじめに
静的サイトを S3 + CloudFront でホスティングし、Code シリーズ (CodeCommit / CodeBuild / CodePipeline / CodeDeploy) で CI/CD を回す構成を、AWS CDK (TypeScript) で一括構築した際の実装メモです。
dev / stg / prd の 3 環境をまとめて一つのコードで定義し、環境ごとにベーシック認証をかけられるようにしています。
今回構築した構成
以下のサービスを組み合わせた 3 環境 (dev / stg / prd) の構成を CDK で定義します。
- S3: 静的サイトホスティング
- CloudFront: CDN 配信
- CloudFront Function: ベーシック認証
- CodeCommit: リポジトリ管理
- CodeBuild: ビルド処理
- CodePipeline: CI/CD パイプライン
- CodeDeploy (S3DeployAction): デプロイ処理
動作フロー
各環境のブランチへの push / マージを契機に CodePipeline が起動し、CodeBuild でビルドを実行、成果物を S3 にデプロイします。CloudFront 経由で配信する際に CloudFront Function でベーシック認証を強制する流れです。
実装手順
AWS CDK のインストール
npm install -g aws-cdkプロジェクトの作成
cdk init app --language=typescript初回利用時はブートストラップコマンドを実行します。
cdk bootstrap必要なモジュールのインストール
CDK v1 を前提とした依存モジュールを追加します。
npm install @aws-cdk/aws-codecommit
npm install @aws-cdk/aws-codebuild
npm install @aws-cdk/aws-codepipeline
npm install @aws-cdk/aws-codepipeline-actions
npm install @aws-cdk/aws-iam
npm install @aws-cdk/aws-s3
npm install @aws-cdk/aws-cloudfront
npm install @aws-cdk/aws-ssm
npm install --save-dev npm-run-allS3 ・ CloudFront 定義 (lib/s3-stack.ts)
バケットを 3 環境分作り、OAI (Origin Access Identity) 経由で CloudFront から参照するようにします。ベーシック認証は環境ごとに別ファイルの JS を CloudFront Function として紐づけます。
import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';
import * as iam from '@aws-cdk/aws-iam';
import { StringParameter } from '@aws-cdk/aws-ssm';
import * as cloudfront from '@aws-cdk/aws-cloudfront';
export class s3Stack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const projectName = this.node.tryGetContext('projectName');
['prd', 'stg', 'dev'].forEach((stage) => {
const bucketName = stage + '-' + projectName;
const s3Bucket = new s3.Bucket(this, stage + '-pjBucket', {
bucketName: bucketName,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
const oai = new cloudfront.OriginAccessIdentity(this, bucketName);
const bucketPolicy = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:GetObject'],
principals: [
new iam.CanonicalUserPrincipal(
oai.cloudFrontOriginAccessIdentityS3CanonicalUserId,
),
],
resources: [s3Bucket.bucketArn + '/*'],
});
s3Bucket.addToResourcePolicy(bucketPolicy);
// CloudFront Function でベーシック認証を掛ける
const basicAuthFunction = new cloudfront.Function(
this,
stage + '-BasicAuthFunction',
{
functionName: bucketName + '-BasicAuth',
code: cloudfront.FunctionCode.fromFile({
filePath: `lambda/BasicAuth/${stage}-auth.js`,
}),
},
);
this.createCloudFront(stage, s3Bucket, oai, basicAuthFunction);
// CI/CD スタックから参照できるようにバケット ARN を SSM Parameter Store に保存
new StringParameter(this, stage + '-bucketArn', {
parameterName: bucketName + '-bucketArn',
stringValue: s3Bucket.bucketArn,
});
});
}
private createCloudFront(
stage: string,
s3Bucket: s3.Bucket,
oai: cloudfront.OriginAccessIdentity,
basicAuthFunction: cloudfront.Function,
) {
new cloudfront.CloudFrontWebDistribution(this, stage + '-Distribution', {
viewerCertificate: {
aliases: [],
props: {
cloudFrontDefaultCertificate: true,
},
},
priceClass: cloudfront.PriceClass.PRICE_CLASS_200,
originConfigs: [
{
s3OriginSource: {
s3BucketSource: s3Bucket,
originAccessIdentity: oai,
},
behaviors: [
{
isDefaultBehavior: true,
functionAssociations: [
{
eventType: cloudfront.FunctionEventType.VIEWER_REQUEST,
function: basicAuthFunction,
},
],
minTtl: cdk.Duration.seconds(0),
maxTtl: cdk.Duration.days(365),
defaultTtl: cdk.Duration.days(1),
pathPattern: '*',
},
],
},
],
errorConfigurations: [
{
errorCode: 403,
responsePagePath: '/error_403.html',
responseCode: 200,
errorCachingMinTtl: 0,
},
{
errorCode: 404,
responsePagePath: '/error_404.html',
responseCode: 200,
errorCachingMinTtl: 0,
},
],
});
}
}CI/CD 定義 (lib/app-stack.ts)
S3 スタックで出力したバケット ARN を SSM から取得し、環境ごとのブランチに対応したパイプラインを作成します。
import * as cdk from '@aws-cdk/core';
import * as codecommit from '@aws-cdk/aws-codecommit';
import * as codebuild from '@aws-cdk/aws-codebuild';
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as codePipeline_actions from '@aws-cdk/aws-codepipeline-actions';
import { StringParameter } from '@aws-cdk/aws-ssm';
import { Bucket } from '@aws-cdk/aws-s3';
export class CiCdStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const projectName = this.node.tryGetContext('projectName');
const tag: string = projectName;
const repoName = projectName;
const repo = new codecommit.Repository(this, 'pjRepo', {
repositoryName: repoName,
description: 'repository',
});
['prd', 'stg', 'dev'].forEach((stage) => {
const bucketArn = StringParameter.valueForStringParameter(
this,
stage + '-' + projectName + '-bucketArn',
);
const targetBucket = Bucket.fromBucketArn(
this,
stage + 'BucketByArn',
bucketArn,
);
const project = this.createProject(stage, targetBucket, tag);
const sourceOutput = new codepipeline.Artifact();
let branch;
if (stage == 'dev') {
branch = 'develop';
} else if (stage == 'stg') {
branch = 'staging';
} else {
branch = 'main';
}
new codepipeline.Pipeline(this, this.createId('Pipline', stage, tag), {
pipelineName: this.createName(stage, tag),
stages: [
{
stageName: 'Source',
actions: [this.createSourceAction(repo, branch, sourceOutput)],
},
{
stageName: 'Build',
actions: [this.createBuildAction(project, sourceOutput)],
},
{
stageName: 'Deploy',
actions: [this.createDeployAction(targetBucket, sourceOutput)],
},
],
});
});
}
private createId(name: string, stage: string, tag: string): string {
return tag + '-' + name + '-' + stage;
}
private createName(stage: string, tag: string): string {
return stage + '-' + tag;
}
private createProject(
stage: string,
s3BucketName: any,
tag: string,
): codebuild.PipelineProject {
const project = new codebuild.PipelineProject(
this,
this.createId('Project', stage, tag),
{
projectName: this.createName(stage, tag),
buildSpec: codebuild.BuildSpec.fromObject({
version: '0.2',
phases: {
build: {
commands: ['echo "*******Start Build*******"'],
},
},
}),
},
);
return project;
}
private createSourceAction(
repo: codecommit.Repository,
branch: string,
sourceOutput: codepipeline.Artifact,
): codePipeline_actions.CodeCommitSourceAction {
return new codePipeline_actions.CodeCommitSourceAction({
actionName: 'CodeCommit',
repository: repo,
branch: branch,
output: sourceOutput,
});
}
private createBuildAction(
project: codebuild.IProject,
sourceOutput: codepipeline.Artifact,
) {
return new codePipeline_actions.CodeBuildAction({
actionName: 'CodeBuild',
project: project,
input: sourceOutput,
outputs: [new codepipeline.Artifact()],
});
}
private createDeployAction(
targetBucket: any,
sourceOutput: codepipeline.Artifact,
) {
return new codePipeline_actions.S3DeployAction({
actionName: 'CodeDeploy',
bucket: targetBucket,
input: sourceOutput,
});
}
}実行ファイル (bin/app.ts)
S3 スタックと CI/CD スタックを 1 つのアプリから呼び出します。S3 側で ARN を SSM に保存しているため、先に S3 スタック、その後 CI/CD スタックという順でデプロイする必要があります。
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { CiCdStack } from '../lib/app-stack';
import { s3Stack } from '../lib/s3-stack';
const app = new cdk.App();
new CiCdStack(app, 'CiCdStack');
new s3Stack(app, 's3Stack');ベーシック認証コードの実装
CloudFront Function として配置する JS を環境ごとに分けて作成します。
lambda/BasicAuth/dev-auth.jslambda/BasicAuth/stg-auth.jslambda/BasicAuth/prd-auth.js
各ファイルの中身は下記のような形です。user:pass を Base64 エンコードした値を比較します (実運用では環境ごとにパスワードを分ける・シークレットで管理するなどを検討してください)。
function handler(event) {
var request = event.request;
var headers = request.headers;
// Base64 エンコード: user:pass = dXNlcjpwYXNz
var authString = 'Basic dXNlcjpwYXNz';
if (
typeof headers.authorization === 'undefined' ||
headers.authorization.value !== authString
) {
return {
statusCode: 401,
statusDescription: 'Unauthorized',
headers: { 'www-authenticate': { value: 'Basic' } },
};
}
return request;
}デプロイスクリプトの設定
複数スタックを順序付きでデプロイするため、package.json に npm-run-all を使ったスクリプトを追記しておきます。
"deploy:ci/cd": "run-s build \"cdk deploy -- {1} --context projectName={2} --profile {3}\" --",
"deploy:s3": "run-s build \"cdk deploy -- {1} --context projectName={2} --profile {3}\" --",
"deploy:all": "run-s \"deploy:s3 {3} {1} {2}\" \"deploy:ci/cd {4} {1} {2}\" --"実行例は次の通りです。
npm run deploy:s3 {S3_StackName} {PJ_Name} {PROFILE}
npm run deploy:ci/cd {CICD_StackName} {PJ_Name} {PROFILE}
npm run deploy:all {PJ_Name} {PROFILE} {S3_StackName} {CICD_StackName}おわりに
今回の構成で、S3 と CloudFront を使ったフロント側 CI/CD 環境を 3 環境分まとめて構築できました。
SSM Parameter Store を介してスタック間のデータ連携を行うことで、S3 とパイプラインのスタックを疎結合に保てています。環境ごとに異なるベーシック認証を適用できるのも便利なポイントです。
今後の改善として、
・CI/CD の結果を Slack 等へ通知する仕組みの追加
・CDK のユニットテスト / スナップショットテストの整備
あたりに取り組んでいきたいところです。
冒頭にも書いた通り、本記事の実装は CDK v1 / CodeCommit を前提としたものです。2026 年時点では CDK v2 + GitHub Actions や CodePipeline V2 などの構成が現実的ですので、あくまで考え方の参考として利用してください。