AWS CDK で構築する S3 + CloudFront 静的サイトの CI/CD パイプライン(TypeScript)

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

s1mlogs1mlog
25 April, 2026
AWS CDK で構築する S3 + CloudFront 静的サイトの CI/CD パイプライン(TypeScript)

注意: この記事は 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-all

S3 ・ 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.js
  • lambda/BasicAuth/stg-auth.js
  • lambda/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.jsonnpm-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 などの構成が現実的ですので、あくまで考え方の参考として利用してください。