😎

aws-solutions-constructsを使って楽してAWS CDK構築する(静的サイト構築編)

に公開

はじめに

こんにちは、株式会社ゼンビットのゆうとです。

私個人のAWS開発環境について、コンソールからAWS構築している状態です。
本記事ではaws-solutions-constructsを使って、
静的サイト構築をベストプラクティスに沿いつつ少ないコードで構築していきます😎

aws-solutions-constructsとは
https://aws.amazon.com/jp/blogs/news/aws-solutions-constructs-a-library-of-architecture-patterns-for-the-aws-cdk/

ありがちな構成のAWS CDK構成を2つ以上組み合わせてくれるライブラリ集合体という理解です。

対象読者

  • 静的サイトのAWS CDK導入を考えている人

記事で説明する内容

  • AWS CDKでの以下環境構築
    • ACM証明書を含めたクロスリージョン構築
    • aws-solutions-constructs

説明しない内容

  • 静的サイトの構築など
  • AWS CDKの基本的な内容

背景

  • 個人開発の環境について、コンソールで開発していました。
  • 最近IaCに関心がある且つ、「どこで何設定したのか覚えていない。。。」となってきたのでAWS CDK構築して綺麗にしたいと思っていました。
  • べスプラが知りたくなり、Construct HubからいいAWS CDKのテンプレートないかと調べていたところ、aws-solutions-constructsの存在を知りました。

構成

AWS CDKで環境構築する環境は大まかに以下の通りです。
静的Webコンテンツ+APIGateWayでLambdaを実行して、他サービス使うような構成です。
改修前の構成

利用するライブラリ調査

Construct Hubから所望のaws-solutions-constructsを検索します。

残念ながら、aws-solutions-constructsで私の作りたい環境の全てをまるまる構築するようなものは見つけられませんでした。

バックエンドとフロントエンド切り離したイメージで調べてみると良さそうなものがヒットしました。

フロントエンド(CloudFront、S3)
https://constructs.dev/packages/@aws-solutions-constructs/aws-cloudfront-s3/v/2.93.0?lang=typescript

バックエンド(APIGateway、Lambda、CloudFront)
https://constructs.dev/packages/@aws-solutions-constructs/aws-cloudfront-apigateway-lambda/v/2.93.0?lang=typescript

ACM証明書周りの作成をしてくれるものは見つからなかったです。見つからないものは素直にL2コンストラクタで作成していきます。

コーディング

コーディングを進めていきます。
長くなりそうなので本記事ではフロント側を構築していきます。
フロント系の環境は大まかに以下の通りです。

  • Node.js 18 以上
  • pnpm v8 系
  • Astro(static output mode) フロントエンド

初期構築

空のフォルダから以下コマンドでAWS CDKのプロジェクトを立ち上げます

cdk init app --language typescript

cdk init後に追加するライブラリとしては「@aws-solutions-constructs/aws-cloudfront-s3」の一点です。
peer dependencyの警告が出たため、aws-cdk-libのバージョンも上げます。

package.json
  "dependencies": {
+    "@aws-solutions-constructs/aws-cloudfront-s3": "2.93.0",
-    "aws-cdk-lib": "2.205.0",
+    "aws-cdk-lib": "^2.219.0",
    "constructs": "^10.0.0"
  }

バイナリファイル作成(クロスリージョン設定)

ACM証明書だけバージニア北部リージョンで作成して、他は東京リージョンで作成したいです。
スタックを2つに分けて構築します。
crossRegionReferencesオプション+それぞれのスタックにリージョンを明記します。
さらに、CertificateStack完了後にInfrastructureStackを実行したいので依存関係を作成しています。

bin/infrastructure.ts
#!/usr/bin/env node
import * as cdk from "aws-cdk-lib";
import { CertificateStack } from "../lib/certificate-stack";
import { InfrastructureStack } from "../lib/infrastructure-stack";

const APP_NAME = "XXXXXX";
const DOMAIN_NAME = "www.XXXXXX.XXXXX";
const CDK_ACCOUNT = "123456789012";

const app = new cdk.App();

const certificateStack = new CertificateStack(
  app,
  `CertificateStack-${APP_NAME}`,
  {
    domainName: DOMAIN_NAME,
    appName: APP_NAME,
    crossRegionReferences: true, //クロスリージョン設定
    env: {
      region: "us-east-1",
      account: CDK_ACCOUNT
    },
  }
);
const infrastructureStack = new InfrastructureStack(
  app,
  `InfrastructureStack-${APP_NAME}`,
  {
    domainName: DOMAIN_NAME,
    appName: APP_NAME,
    crossRegionReferences: true, //クロスリージョン設定
    certificate: certificateStack.certificate,
    env: {
      region: "ap-northeast-1",
      account: CDK_ACCOUNT
    },
  }
);
infrastructureStack.addDependency(certificateStack);  //依存関係設定

app.synth();

ACM証明書のスタック作成

次にスタック作成です。
まずはACM証明書専用のCertificateStackを作成します。
InfrastructureStackに渡すため、ACM証明書の戻り値はパブリックで取得します。

lib/certificate-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as acm from "aws-cdk-lib/aws-certificatemanager";

interface CertificateStackProps extends cdk.StackProps {
  domainName: string;
  appName: string;
}

export class CertificateStack extends cdk.Stack {
  public readonly certificate: acm.ICertificate; //Public指定
  constructor(scope: Construct, id: string, props: CertificateStackProps) {
    super(scope, id, props);

    this.certificate = new acm.Certificate(
      this,
      `SiteCertificate-${props.appName}`,
      {
        domainName: props.domainName,
        validation: acm.CertificateValidation.fromDns(),
      }
    );
  }
}

静的サイト(aws-solutions-constructs利用)のスタック作成

次にACM証明書以外の部分、InfrastructureStackを構築します。
「CloudFrontToS3」が今回主役の「aws-solutions-constructs」です。

lib/infrastructure-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { CloudFrontToS3 } from "@aws-solutions-constructs/aws-cloudfront-s3";
import * as s3deploy from "aws-cdk-lib/aws-s3-deployment";
import * as acm from "aws-cdk-lib/aws-certificatemanager";

interface InfrastructureStackProps extends cdk.StackProps {
  certificate: acm.ICertificate;
  domainName: string;
  appName: string;
}

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

    const cloudFrontToS3 = new CloudFrontToS3(
      this,
      `CloudFrontToS3-${props.appName}`,
      {
        cloudFrontDistributionProps: {
          domainNames: [props.domainName],
          certificate: props.certificate,
        },
      }
    );

    // ここでデプロイを行う(事前にビルドしておくこと)
    new s3deploy.BucketDeployment(
      this,
      `DeployStaticWebsite-${props.appName}`,
      {
        sources: [s3deploy.Source.asset("../apps/web/dist")],
        destinationBucket: cloudFrontToS3.s3Bucket!,
      }
    );
  }
}

実行

bootstrapしてデプロイ開始します。
deploy中に固まりました。これはACM証明書の設定を待ってくれてるみたいです。想定通りです。
cdk_stop

cloudflareのCNAMEレコード作成します。
CLIで作成がスマートだと思いますが今回は手入力します。
cloudflare_dns

証明書認証できたみたいです🤗。
AWS CDK再開してCertificateStackが終わりました。
cdk_restart

続けてデプロイできたので動作確認してみると
なんだかjavascriptがなくなってます。。。
ブラウザの開発者ツールみてみるとエラーが😿
errorimage

どうやらディストリビューションのcloudfrontfunctionに以下関数がされているみたいです。
securityfunction

ガチガチのCSP設定なので緩和しましょう。
修正します。

lib/infrastructure-stack.ts
    const cloudFrontToS3 = new CloudFrontToS3(
      this,
      `CloudFrontToS3-${props.appName}`,
      {
+       insertHttpSecurityHeaders: false,
        cloudFrontDistributionProps: {
          domainNames: [props.domainName],
          certificate: props.certificate,
        },
+        responseHeadersPolicyProps: {
+          securityHeadersBehavior: {
+            contentSecurityPolicy: {
+              override: true,
+              contentSecurityPolicy:
+                "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';",
+            },
+          },
+        },
      }
    );

すっかり忘れていましたが、私の環境だとインデックスドキュメント機能の追加対応が必要でした。
「CloudFrontToS3」にCloudFrontFunctionをオーバーライドしています。

lib/infrastructure-stack.ts

+    const functionForDirIndex = new cloudfront.Function(
+      this,
+      `DirIndexFunction--${props.appName}`,
+      {
+        code: cloudfront.FunctionCode.fromInline(`
+          function handler(event) {
+          var request = event.request;
+          var uri = request.uri;

+          if (uri.endsWith("/")) {
+            request.uri += "index.html";
+          }
+          else if (!uri.includes(".")) {
+            request.uri += "/index.html";
+          }

+          return request;
+        }
+      `),
+      }
+    );

    const cloudFrontToS3 = new CloudFrontToS3(
      this,
      `CloudFrontToS3-${props.appName}`,
      {
        insertHttpSecurityHeaders: false,
        cloudFrontDistributionProps: {
          domainNames: [props.domainName],
          certificate: props.certificate,
+          defaultBehavior: {
+            functionAssociations: [
+              {
+                eventType: cloudfront.FunctionEventType.VIEWER_REQUEST,
+                function: functionForDirIndex,
+              },
+            ],
+          },
+        },
         responseHeadersPolicyProps: {
           securityHeadersBehavior: {
             contentSecurityPolicy: {
               override: true,
               contentSecurityPolicy:
                 "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';",
             },
           },
         },
      }
    );

デプロイすると期待通りのcloudfrontFunctionが設定できました🎇
cloudfrontfunction

エラーもなくなりました🎇
noerrorimage

結論・まとめ

  • 期待していた静的ウェブサイトの構築が、シンプルで綺麗にAWS CDKで実現できました。
  • CSPガチガチなところが気になりました。こういうものなのでしょうか。
  • インデックスドキュメントの対応が必要なことを忘れていました。AWS CDKでコード管理するのは大事だと感じました。

今後の発展

  • 次回はlambda部分の構築を進めていきます。
  • lambda部分終わればcdk-nagの導入をしたいです。

Discussion