AWS CDKで構築！Next.js SSGで作った静的サイトをホスティングする

# はじめに
私は最近AWS CDKコントリビューションに入門しいくつかプルリクエストを提出しています。しかし、肝心のCDK自体をがっつり触ったことがなく、エアプの状態でコントリビュートしているような状態でした。せっかくなので何かCDKで作ってみるか〜〜と思い、簡単な静的サイトをホスティングしてみました。

# できたもの
私のプロフィールサイトを作りました
https://about-tttol.link/


![about-me.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159675/613c4c97-8b5b-4e34-8799-a1c44810785b.jpeg)



フロントエンドはNext.js SSGを使っています。Vercelでホスティングしても良かったのですが、CDKの勉強をしたかったのであえてAWSリソースで構築しました。

インフラはS3, CloudFront, Route 53, ACMを使っています。

# まずはプロフィールサイトをつくる
公開したいサイトを構築しましょう。ここは好きな技術スタックで作ればOKです。

# Route 53でドメイン取得
次にRoute 53でドメインを取得しましょう。

Route 53のドメインはTLD(TopLevelDomain)によって値段が大きく異なります。.com, .jpなどの有名ドメインはお高いですが、ドメイン名に特にこだわりがなければ安価なドメインを探すことも可能です。自分が探した限りでは.linkが5ドルと一番安価だったので、こちらのTLDを使って`about-tttol.link`というドメインを取得しました。

※記載しているドメイン価格は2025/05/09時点のものです

# ACMで証明書作成
続いて、Route 53で取得したドメインを用いてSSL証明書を発行していきます。CloudFrontが用意したデフォルトのURLのままでもホスティングはできますが、やはりカスタムドメインで配信したいですよね。

以下の記事を参考にして登録しました。

https://dev.classmethod.jp/articles/issuing-certificate-with-acm/



# インフラリソースをCDKで構築・デプロイ
Route 53とACMの証明書ができたら、他のリソースはCDKで構築していきます。
※Route 53とACMもCDKで構築しようと思いましたが、実装方法がいまいち分からず諦めました。
```ts
import * as cdk from 'aws-cdk-lib';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as targets from 'aws-cdk-lib/aws-route53-targets';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

export interface AboutMeInfraStackProps extends cdk.StackProps {
  domainName: string | undefined;
  hostedZoneId: string | undefined;
}

export class AboutMeInfraStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: AboutMeInfraStackProps) {
    super(scope, id, props);

    // Validate required parameters
    if (!props.domainName) {
      throw new Error('domainName is required');
    }
    if (!props.hostedZoneId) {
      throw new Error('hostedZoneId is required');
    }

    const hostedZone = route53.HostedZone.fromHostedZoneAttributes(this, 'HostedZone', {
      hostedZoneId: props.hostedZoneId,
      zoneName: props.domainName,
    });

    // S3 bucket for static assets
    const s3Bucket = new s3.Bucket(this, 'AboutMeInfraBucket', {
      bucketName: `about-me-infra-${this.account}`,
      publicReadAccess: false,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
      encryption: s3.BucketEncryption.S3_MANAGED,
    });

    const certificateArn = process.env.ACM_CERTIFICATE_ARN || '';
    const certificate = acm.Certificate.fromCertificateArn(this, 'Certificate', certificateArn);

    // CloudFront distribution with custom domain
    const distribution = new cloudfront.Distribution(this, 'AboutMeDistribution', {
      defaultBehavior: {
        origin: origins.S3BucketOrigin.withOriginAccessControl(s3Bucket),
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
        compress: true,
      },
      domainNames: [props.domainName],
      certificate: certificate,
      defaultRootObject: 'index.html',
      errorResponses: [
        {
          httpStatus: 404,
          responseHttpStatus: 200,
          responsePagePath: '/index.html',
        },
        {
          httpStatus: 403,
          responseHttpStatus: 200,
          responsePagePath: '/index.html',
        },
      ],
    });

    // Create A record for the domain
    new route53.ARecord(this, 'AliasRecord', {
      zone: hostedZone,
      target: route53.RecordTarget.fromAlias(
        new targets.CloudFrontTarget(distribution)
      ),
    });

    // Output the CloudFront URL
    new cdk.CfnOutput(this, 'CloudFrontURL', {
      value: `https://${props.domainName}`,
      description: 'Custom Domain URL',
    });
  }
}
```

ソースコードの全文はこちら。

https://github.com/tttol/about-me-infra

# S3バケットにアプリをアップロード
CDKリソースのデプロイが成功すると静的アセット用のS3バケットが作成されます。このバケットにHTMLなどの成果物をアップロードしましょう。
私はNext.jsでサイトを構築したため、`npm run build`コマンドで`out`ディレクトリに成果物を生成し、配下のディレクトリ・ファイルを全てS3バケットにアップロードしました。この部分をCodeBuildやCodePipelineを使って自動化することも考えましたが、デプロイ頻度と構築コストを考えるとコスパが悪かったため自動化はしませんでした。

# さいごに
CDKを使ったAWSリソース構築は初めてでしたが、思ったより簡単でした。