CloudFront Hosting Toolkitを使って静的Webサイト環境を作ってみた

サクッと静的Webサイトを用意したい場合に
2024.06.26

手間をかけずにCloudFrontを使った静的Webサイトを作りたい

こんにちは、のんピ(@non____97)です。

皆さんは手間をかけずにCloudFrontを使った静的Webサイトを作りたいなと思ったことはありますか? 私はあります。

過去にAWS CDKを使ってこの思いを実現したことがありますが、一からAWS CDKを作り込むのはなかなか大変でした。

そんな苦労はCloudFront Hosting Toolkitを使用すると少し解消されるかもしれません。

CloudFront Hosting ToolkitはCLIまたはAWS CDKでフロントエンドのホスティングとCI/CDパイプラインを用意するツールです。

今だとAmplifyを使えば良いのではないか? という声も聞こえてきますが、細かいカスタマイズを行いたい場合にCloudFront周りを直接操作したい場合があります。そういったときにCloudFront Hosting Toolkitを使用すれば、素早く環境を用意することが可能です。

デプロイはGitHubリポジトリを用意し、コマンドを数回実行する程度です。これは便利です。

CloudFront Hosting Toolkitを紹介しているAWS Blogsは以下をご覧ください。

具体的な使用方法についてはGitHubをご覧ください。

実際に使ってみたので紹介します。

いきなりまとめ

  • CloudFront Hosting ToolkitはCLIまたはAWS CDKでフロントエンドのホスティングとCI/CDパイプラインを用意するツール
    • AWS CDKのL3 Constructとして使用することも可能
  • サポートされているフロントエンドフレームワークは以下
    • AngularJS
    • Next.js
    • React
    • Vue.js
  • フロントエンドフレームワークを使用しないことも可能
  • カスタムドメインを割り当てることも可能
  • CloudFront Hosting Toolkit v1.9時点では以下に注意
    • 連携可能なGitリポジトリはGitHubのみ
    • AWS CLIのプロファイルはサポートされていない
    • CLI操作の場合、グローバルインストールをしなければLambda Layerに必要なパッケージがバンドルされない

CloudFront Hosting Toolkitとは

CloudFront Hosting ToolkitとはCLIまたはAWS CDKでフロントエンドのホスティングとCI/CDパイプラインを用意するツールです。

環境をデプロイした後はGitにPushするだけで自動でコンテンツをデプロイすることが可能です。

サポートされているフロントエンドフレームワークは以下のとおりです。

  • AngularJS
  • Next.js
  • React
  • Vue.js

フロントエンドフレームワークを使わないという選択肢も可能です。

現時点ではSSRはサポートされていません。ロードマップには含まれているので期待して待ちましょう。

また、連携できるGitリポジトリはGitHub上のもののみです。

カスタムドメインについては使用可能です。本番環境でも使えそうですね。

全体概要は以下のとおりです。

cloudfront-hosting-toolkit_architecture

引用 : Figure 1: Architecture of CloudFront Hosting Toolkit | Introducing CloudFront Hosting Toolkit | Networking & Content Delivery

大半のリソースのデプロイはAWS CDKで行われています。L3 Constructとして使用することも可能です。一部分だけ組み込みたいという場面に役立ちそうですね。

Webアクセスおよびデプロイのフローについては以下の図が非常に参考になります。

cloudfront-hosting-toolkit_diag-hosting-1

引用 : Figure 2: CloudFront Hosting Toolkit – Event flow | Introducing CloudFront Hosting Toolkit | Networking & Content Delivery

CloudFront KeyValueStoreとCloudFront Functionsを用いることによって、コンテンツ更新時に新しいコンテンツにユーザーが素早くアクセスできるようにしています。

やってみた

CloudFront Hosting Toolkitのインストール

実際にやってみましょう。今回はAWS CDKではなく、CLIでデプロイします。

まず、CloudFront Hosting Toolkitのインストールをします。

GitHub上にはグローバルインストールをするように記載がありましたが、ローカルインストールで試します。

$ npm i @aws/cloudfront-hosting-toolkit
npm WARN deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm WARN deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm WARN deprecated @aws-sdk/node-config-provider@3.374.0: This package has moved to @smithy/node-config-provider
npm WARN deprecated @aws-sdk/config-resolver@3.374.0: This package has moved to @smithy/config-resolver

added 310 packages in 17s

26 packages are looking for funding
  run `npm fund` for details

310パッケージと、それなりの数のパッケージがインストールされました。直接の依存パッケージはcloudfront-hosting-toolkit/package.jsonをご覧ください。

GitHubのプライベートリポジトリの用意

GitHubのプライベートリポジトリの用意をします。

CloudFront Hosting Toolkitのデプロイメントの初期化を行う際にGitHubのリポジトリのURLが必要です。

今回はcloudfront-hosting-toolkit-testというプライベートリポジトリを用意しました。

1.検証用のプライベートリポジトリの作成

手元の端末にクローンして、cdしておきます。

$ git clone https://github.com/non-97/cloudfront-hosting-toolkit-test.git
Cloning into 'cloudfront-hosting-toolkit-test'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Receiving objects: 100% (3/3), done.

$ cd cloudfront-hosting-toolkit-test/
$ ls -la
total 8
drwxr-xr-x@  4 <ユーザー名>  staff  128  6 20 13:39 ./
drwxr-xr-x@  9 <ユーザー名>  staff  288  6 20 13:39 ../
drwxr-xr-x@ 11 <ユーザー名>  staff  352  6 20 13:40 .git/
-rw-r--r--@  1 <ユーザー名>  staff   34  6 20 13:39 README.md

CloudFront Hosting Toolkitのデプロイメントの初期化

CloudFront Hosting Toolkitのデプロイメントの初期化を行います。

$ 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 /<作業ディレクトリパス>/cloudfront-hosting-toolkit-test

? Please provide your GitHub repository URL ›

GitHubのリポジトリURLの入力を促されました。入力してあげましょう。

? Please provide your GitHub repository URL › https://github.com/non-97/cloudfront-hosting-toolkit-test.git
✔ Please provide your GitHub repository URL … https://github.com/non-97/cloudfront-hosting-toolkit-test.git

? What is the name of the branch you would like to use? Hit Enter to confirm or change the selection. › main

次にブランチ名を使用するのかを選択するようです。mainブランチを選択します。

? What is the name of the branch you would like to use? Hit Enter to confirm or change the selection. › main
✔ What is the name of the branch you would like to use? Hit Enter to confirm or change the selection. … main

? Which framework did you use for website construction? Press Enter to confirm or change the selection. › - Use arrow-keys. Return to submit.
    AngularJS Framework
❯   No FrontEnd framework used; Basic implementation (no build required)
    Next.js Framework
    React Framework
    Vue.js Framework
    None from the list, exit and add my own

次にWebサイトのフロントエンドのフレームワークを選択するようです。今回は特に使用しないのでNo FrontEnd framework usedを選択します。

? Which framework did you use for website construction? Press Enter to confirm or change the selection. › - Use arrow-keys. Return to submit.
    AngularJS Framework
❯   No FrontEnd framework used; Basic implementation (no build required)
    Next.js Framework
    React Framework
    Vue.js Framework
    None from the list, exit and add my own
✔ Which framework did you use for website construction? Press Enter to confirm or change the selection. › No FrontEnd framework used; Basic implementation (no build required)

? Do you own a domain name that you would like to use? › - Use arrow-keys. Return to submit.
    Yes
❯   No

次にカスタムドメインを使用するかどうかの選択です。カスタムドメインを使用したいので、Yesを選択します。

? Do you own a domain name that you would like to use? › - Use arrow-keys. Return to submit.
❯   Yes
    No
✔ Do you own a domain name that you would like to use? › Yes

Please provide your domain name in the following formats:

ドメインの選択です。今回はcf-test.non-97.netにします。

? Please provide your domain name in the following formats: www.mydomainname.com or mydomainname.com ? > cf-test.non-97.net
✔ Please provide your domain name in the following formats: www.mydomainname.com or mydomainname.com ? … cf-test.non-97.net

? Where is the authoritative DNS server of this domain? › - Use arrow-keys. Return to submit.
    Elsewhere
❯   Route 53 in this AWS Account

そのドメインをどこのDNSサーバーで管理しているか選択します。デプロイ先と同じAWSアカウント上のRoute 53 Public Hosted Zoneを使用したいのでRoute 53 in this AWS Accountを選択します。併せてcf-test.non-97.netを管理しているRoute 53 Public Hosted ZoneのIDを入力します。

? Where is the authoritative DNS server of this domain? › - Use arrow-keys. Return to submit.
    Elsewhere
❯   Route 53 in this AWS Account
✔ Where is the authoritative DNS server of this domain? › Route 53 in this AWS Account
✔ Please type the hosted zone ID … Z00551472Z2MB7670X9ER

----------------------------------------------------
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/cloudfront-hosting-toolkit-test.git/main
>       Framework: basic
>       Domain name: cf-test.non-97.net
>       Hosted zone ID: Z00551472Z2MB7670X9ER

--
>       Configuration file generated /<作業ディレクトリパス>/cloudfront-hosting-toolkit-test/cloudfront-hosting-toolkit/cloudfront-hosting-toolkit-config.json
>       Build configuration generated /<作業ディレクトリパス>/cloudfront-hosting-toolkit-test/cloudfront-hosting-toolkit/cloudfront-hosting-toolkit-build.yml
>       CloudFront Function source code generated /<作業ディレクトリパス>/cloudfront-hosting-toolkit-test/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.

初期化が設定が完了し、設定ファイルが出力されました。

出力されたファイルは以下のとおりです。BuildspecのYAMLファイルやCloudFront Functionsと思われるJavaScriptのコードが生成されていますね。

./cloudfront-hosting-toolkit-test/cloudfront-hosting-toolkit/cloudfront-hosting-toolkit-config.json

{
  "repoUrl": "https://github.com/non-97/cloudfront-hosting-toolkit-test.git",
  "branchName": "main",
  "framework": "basic",
  "domainName": "cf-test.non-97.net",
  "hostedZoneId": "Z00551472Z2MB7670X9ER"
}

./cloudfront-hosting-toolkit-test/cloudfront-hosting-toolkit/cloudfront-hosting-toolkit-build.yml

version: 0.2

phases:
  build:
    commands:
      - 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

./cloudfront-hosting-toolkit-test/cloudfront-hosting-toolkit/cloudfront-hosting-toolkit-cff.js

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;
}

CloudFront Hosting Toolkitを使ったデプロイ

CloudFront Hosting Toolkitを使ってデプロイします。

$ aws sts get-caller-identity
Enter MFA code for arn:aws:iam::<Assume Role元AWSアカウントID>:mfa/<MFAデバイス名>:
{
    "UserId": "<IAMユーザーID>:botocore-session-1718859240",
    "Account": "<Assume Role先AWSアカウントID>",
    "Arn": "arn:aws:sts::<Assume Role先AWSアカウントID>:assumed-role/<IAMロール名>/botocore-session-1718859240"
}

$ npx cloudfront-hosting-toolkit deploy

 --> 1. Setting up a SSL/TLS certificate with AWS Certificate Manager (ACM)

Error checking ACM Certificate AccessDeniedException: User: arn:aws:iam::<Assume Role元AWSアカウントID>:user/<IAMユーザー名> is not authorized to perform: acm:ListCertificates with an explicit deny in an identity-based policy
    at de_AccessDeniedExceptionRes (/<作業ディレクトリパス>/node_modules/@aws-sdk/client-acm/dist-cjs/index.js:986:21)
    at de_CommandError (/<作業ディレクトリパス>/node_modules/@aws-sdk/client-acm/dist-cjs/index.js:949:19)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async /<作業ディレクトリパス>/node_modules/@smithy/middleware-serde/dist-cjs/index.js:35:20
    at async /<作業ディレクトリパス>/node_modules/@smithy/core/dist-cjs/index.js:165:18
    at async /<作業ディレクトリパス>/node_modules/@smithy/middleware-retry/dist-cjs/index.js:320:38
    at async /<作業ディレクトリパス>/node_modules/@aws-sdk/middleware-logger/dist-cjs/index.js:34:22
    at async checkCertificateExists (/<作業ディレクトリパス>/node_modules/@aws/cloudfront-hosting-toolkit/bin/cli/utils/awsSDKUtil.js:98:26)
    at async handleDeployCommand (/<作業ディレクトリパス>/node_modules/@aws/cloudfront-hosting-toolkit/bin/cli/actions/deploy.js:67:42)
    at async handleCommand (/<作業ディレクトリパス>/node_modules/@aws/cloudfront-hosting-toolkit/bin/cli/index.js:54:13) {
  '$fault': 'client',
  '$metadata': {
    httpStatusCode: 400,
    requestId: '386f3574-35b6-4ff2-bda4-6f54bba13849',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  __type: 'AccessDeniedException'
}
AccessDeniedException: User: arn:aws:iam::<Assume Role元AWSアカウントID>:user/<IAMユーザー名> is not authorized to perform: acm:ListCertificates with an explicit deny in an identity-based policy
    at de_AccessDeniedExceptionRes (/<作業ディレクトリパス>/node_modules/@aws-sdk/client-acm/dist-cjs/index.js:986:21)
    at de_CommandError (/<作業ディレクトリパス>/node_modules/@aws-sdk/client-acm/dist-cjs/index.js:949:19)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async /<作業ディレクトリパス>/node_modules/@smithy/middleware-serde/dist-cjs/index.js:35:20
    at async /<作業ディレクトリパス>/node_modules/@smithy/core/dist-cjs/index.js:165:18
    at async /<作業ディレクトリパス>/node_modules/@smithy/middleware-retry/dist-cjs/index.js:320:38
    at async /<作業ディレクトリパス>/node_modules/@aws-sdk/middleware-logger/dist-cjs/index.js:34:22
    at async checkCertificateExists (/<作業ディレクトリパス>/node_modules/@aws/cloudfront-hosting-toolkit/bin/cli/utils/awsSDKUtil.js:98:26)
    at async handleDeployCommand (/<作業ディレクトリパス>/node_modules/@aws/cloudfront-hosting-toolkit/bin/cli/actions/deploy.js:67:42)
    at async handleCommand (/<作業ディレクトリパス>/node_modules/@aws/cloudfront-hosting-toolkit/bin/cli/index.js:54:13) {
  '$fault': 'client',
  '$metadata': {
    httpStatusCode: 400,
    requestId: '386f3574-35b6-4ff2-bda4-6f54bba13849',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  __type: 'AccessDeniedException'
}

権限が不足しており、デプロイできないようです。

私の端末では以下記事のようにAWS CLIからAssume Roleをして操作をするように設定しています。

プロファイル名を指定しても結果は変わりありませんでした。

$ npx cloudfront-hosting-toolkit status --profile <プロファイル名>
Error retrieving parameter PipelineName AccessDeniedException: User: arn:aws:iam::<Assume Role元AWSアカウントID>:user/<IAMユーザー名> is not authorized to perform: ssm:GetParameter on resource: arn:aws:ssm:us-east-1:<Assume Role元AWSアカウントID>:parameter/hosting-main-cloudfront-hosting-toolkit-test-main/PipelineName with an explicit deny in an identity-based policy
    at throwDefaultError (/<作業ディレクトリパス>/node_modules/@smithy/smithy-client/dist-cjs/index.js:839:20)
    at /<作業ディレクトリパス>/node_modules/@smithy/smithy-client/dist-cjs/index.js:848:5
    at de_CommandError (/<作業ディレクトリパス>/node_modules/@aws-sdk/client-ssm/dist-cjs/index.js:7003:14)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async /<作業ディレクトリパス>/node_modules/@smithy/middleware-serde/dist-cjs/index.js:35:20
    at async /<作業ディレクトリパス>/node_modules/@smithy/core/dist-cjs/index.js:165:18
    at async /<作業ディレクトリパス>/node_modules/@smithy/middleware-retry/dist-cjs/index.js:320:38
    at async /<作業ディレクトリパス>/node_modules/@aws-sdk/middleware-logger/dist-cjs/index.js:34:22
    at async getSSMParameter (/<作業ディレクトリパス>/node_modules/@aws/cloudfront-hosting-toolkit/bin/cli/utils/awsSDKUtil.js:499:26)
    at async getPipelineStatus (/<作業ディレクトリパス>/node_modules/@aws/cloudfront-hosting-toolkit/bin/cli/utils/awsSDKUtil.js:534:30) {
  '$fault': 'client',
  '$metadata': {
    httpStatusCode: 400,
    requestId: 'aaa09af6-7166-4b3d-9050-7f432ad57b05',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  __type: 'AccessDeniedException'
}
Error getting Pipeline Status AccessDeniedException: User: arn:aws:iam::<Assume Role元AWSアカウントID>:user/<IAMユーザー名> is not authorized to perform: ssm:GetParameter on resource: arn:aws:ssm:us-east-1:<Assume Role元AWSアカウントID>:parameter/hosting-main-cloudfront-hosting-toolkit-test-main/PipelineName with an explicit deny in an identity-based policy
    at throwDefaultError (/<作業ディレクトリパス>/node_modules/@smithy/smithy-client/dist-cjs/index.js:839:20)
    at /<作業ディレクトリパス>/node_modules/@smithy/smithy-client/dist-cjs/index.js:848:5
    at de_CommandError (/<作業ディレクトリパス>/node_modules/@aws-sdk/client-ssm/dist-cjs/index.js:7003:14)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async /<作業ディレクトリパス>/node_modules/@smithy/middleware-serde/dist-cjs/index.js:35:20
    at async /<作業ディレクトリパス>/node_modules/@smithy/core/dist-cjs/index.js:165:18
    at async /<作業ディレクトリパス>/node_modules/@smithy/middleware-retry/dist-cjs/index.js:320:38
    at async /<作業ディレクトリパス>/node_modules/@aws-sdk/middleware-logger/dist-cjs/index.js:34:22
    at async getSSMParameter (/<作業ディレクトリパス>/node_modules/@aws/cloudfront-hosting-toolkit/bin/cli/utils/awsSDKUtil.js:499:26)
    at async getPipelineStatus (/<作業ディレクトリパス>/node_modules/@aws/cloudfront-hosting-toolkit/bin/cli/utils/awsSDKUtil.js:534:30) {
  '$fault': 'client',
  '$metadata': {
    httpStatusCode: 400,
    requestId: 'aaa09af6-7166-4b3d-9050-7f432ad57b05',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  __type: 'AccessDeniedException'
}
AccessDeniedException: User: arn:aws:iam::<Assume Role元AWSアカウントID>:user/<IAMユーザー名> is not authorized to perform: ssm:GetParameter on resource: arn:aws:ssm:us-east-1:<Assume Role元AWSアカウントID>:parameter/hosting-main-cloudfront-hosting-toolkit-test-main/PipelineName with an explicit deny in an identity-based policy
    at throwDefaultError (/<作業ディレクトリパス>/node_modules/@smithy/smithy-client/dist-cjs/index.js:839:20)
    at /<作業ディレクトリパス>/node_modules/@smithy/smithy-client/dist-cjs/index.js:848:5
    at de_CommandError (/<作業ディレクトリパス>/node_modules/@aws-sdk/client-ssm/dist-cjs/index.js:7003:14)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async /<作業ディレクトリパス>/node_modules/@smithy/middleware-serde/dist-cjs/index.js:35:20
    at async /<作業ディレクトリパス>/node_modules/@smithy/core/dist-cjs/index.js:165:18
    at async /<作業ディレクトリパス>/node_modules/@smithy/middleware-retry/dist-cjs/index.js:320:38
    at async /<作業ディレクトリパス>/node_modules/@aws-sdk/middleware-logger/dist-cjs/index.js:34:22
    at async getSSMParameter (/<作業ディレクトリパス>/node_modules/@aws/cloudfront-hosting-toolkit/bin/cli/utils/awsSDKUtil.js:499:26)
    at async getPipelineStatus (/<作業ディレクトリパス>/node_modules/@aws/cloudfront-hosting-toolkit/bin/cli/utils/awsSDKUtil.js:534:30) {
  '$fault': 'client',
  '$metadata': {
    httpStatusCode: 400,
    requestId: 'aaa09af6-7166-4b3d-9050-7f432ad57b05',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  __type: 'AccessDeniedException'
}

ソースコードを確認したところ、プロファイルを引数として渡すことはできないようです。

/bin/cli/index.ts

#!/usr/bin/env node
/*
 *  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License").
 *  You may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

import { handleDeleteCommand } from "./actions/delete";
import handleDeployCommand from "./actions/deploy";
import { handleShowCommand } from "./actions/show";
import handleInitCommand from "./actions/init";
import { ERROR_PREFIX } from "./shared/constants";
import { handleStatusCommand } from "./actions/status";

const yargs = require("yargs");

async function main() {
  const args = yargs
    .usage("Usage: $0 <command> [options]")
    .command(
      "init",
      "Step by step guide for configuring a GitHub source code repository and generate a configuration file",
      (yargs: any) => {
        yargs
          .option("s3", {
            describe:
              "Step by step guide for configuring an S3 source code repository and generate a configuration file",
            type: "boolean",
          })          
      }
    )
    .command(
      "deploy",
      'Initiate a deployment of the infrastructure, utilizing the configuration file generated during the execution of the "init" command'
    )
    .command(
      "show",
      "Show the domain name connected to the deployed source code repository for a website that has been deployed"
    )
    .command(
      "delete",
      "Completely remove the hosting infrastructure from your AWS account"
    )
    .command("status", "Display the current status of the pipeline deployment")
    .help()
    .parse();

  if (args._.length > 1) {
    console.error(`${ERROR_PREFIX} Only one command at a time`);
    process.exit(1);
  }

  await handleCommand(args);
}

async function handleCommand({
  _: [command],
  s3,
}: {
  _: string[];
  s3?: boolean;
}) {
  switch (command) {
    case "deploy":
      await handleDeployCommand();
      break;

    case "show":
      await handleShowCommand();
      break;
    case "init":
      await handleInitCommand(s3 || false);
      break;
    case "delete":
      await handleDeleteCommand();
      break;
    case "status":
      await handleStatusCommand();
      break;
    default:
      yargs.showHelp()
  }
}

if (require.main === module) {
  main().catch((err) => {
    console.error(err);
    process.exit(1);
  });
}

Assume Role用スクリプトの実行

今までの挙動から判断するに、CloudFront Hosting ToolkitはAWS CLIのプロファイルを認識しないようです。

専用にアクセスキーを発行することはしたくはないので、Assume Roleするスクリプトを用意します。以下記事を参考にしました。

作成したAssume Role用のシェルスクリプトは以下のとおりです。

./assume-role.sh

#!/usr/bin/env bash

set -euo pipefail

readonly AWS_CONFIG_FILE="${HOME}/.aws/config"
readonly DEFAULT_REGION="us-east-1"
readonly SESSION_DURATION=3600

# 使用方法の表示
function print_usage() {
  cat <<EOF
Usage: $0 [-p PROFILE] [-t TOKEN_CODE]
  -p PROFILE     AWS profile name in ${AWS_CONFIG_FILE}
  -t TOKEN_CODE  MFA token code
If no arguments are provided, the script will run in interactive mode.
EOF
}

# プロファイル一覧の表示
function print_profile_list() {
  echo -e "\nProfile List:"
  grep "\[profile .*\]" "${AWS_CONFIG_FILE}" || echo "No profiles found"
}

# 指定された変数に標準入力結果を代入
function read_user_input() {
  local prompt="$1"
  local var_name="$2"
  read -p "${prompt}: " "${var_name}"
}

# プロファイル内の指定したキーの値を取得
function get_aws_config_value() {
  local key="$1"
  aws configure get "${key}" --profile "${PROFILE}"
}

# Assume Roleの実行
function assume_role() {
  local role_arn="$1"
  local serial_number="$2"
  local source_profile="$3"
  local token_code="$4"
  local session_name
  session_name="session-$(date +%s)"

  aws sts assume-role \
    --role-arn "${role_arn}" \
    --serial-number "${serial_number}" \
    --role-session-name "${session_name}" \
    --profile "${source_profile}" \
    --duration-seconds "${SESSION_DURATION}" \
    --token-code "${token_code}"
}

# Assume Role結果を環境変数に代入
function set_aws_environment_variables() {
  local assume_role_output="$1"
  export AWS_ACCESS_KEY_ID=$(echo "${assume_role_output}" | jq -r .Credentials.AccessKeyId)
  export AWS_SECRET_ACCESS_KEY=$(echo "${assume_role_output}" | jq -r .Credentials.SecretAccessKey)
  export AWS_SESSION_TOKEN=$(echo "${assume_role_output}" | jq -r .Credentials.SessionToken)
}

# メインで実行する関数
function main() {
  local PROFILE=""
  local TOKEN_CODE=""

  # 引数のパース
  while getopts ":p:t:h" opt; do
    case ${opt} in
    p)
      PROFILE=$OPTARG
      ;;
    t)
      TOKEN_CODE=$OPTARG
      ;;
    h)
      print_usage
      exit 0
      ;;
    \?)
      echo "Invalid option: $OPTARG" 1>&2
      print_usage
      exit 1
      ;;
    :)
      echo "Invalid option: $OPTARG requires an argument" 1>&2
      print_usage
      exit 1
      ;;
    esac
  done
  shift $((OPTIND - 1))

  # 引数でプロファイル名もしくはMFAトークンコードが入力されなかった場合は、対話形式で入力を受け付ける
  if [[ -z "$PROFILE" || -z "$TOKEN_CODE" ]]; then
    print_profile_list

    [ -z "$PROFILE" ] && read_user_input "Profile" PROFILE
    [ -z "$TOKEN_CODE" ] && read_user_input "MFA Code" TOKEN_CODE
  fi

  # 指定されたプロファイル名の情報を取得
  local role_arn
  local serial_number
  local source_profile
  local region

  role_arn=$(get_aws_config_value "role_arn")
  serial_number=$(get_aws_config_value "mfa_serial")
  source_profile=$(get_aws_config_value "source_profile")
  region=$(get_aws_config_value "region")

  # Assume Roleの実行
  local assume_role_output
  assume_role_output=$(
    assume_role \
      "${role_arn}" \
      "${serial_number}" \
      "${source_profile}" \
      "${TOKEN_CODE}"
  )

  # Assume Roleの結果を環境変数に代入
  set_aws_environment_variables "${assume_role_output}"

  # デフォルトリージョンの指定
  export AWS_DEFAULT_REGION="${region:-${DEFAULT_REGION}}"
  echo "AWS_DEFAULT_REGION : ${AWS_DEFAULT_REGION}"

  # Assume Roleが正常に行われたか確認
  aws sts get-caller-identity
}

main "$@"

参考にしたスクリプトは対話式でプロファイルやMFAトークンコードを入力していましたが、これらの情報をコマンドライン引数で渡せられるように機能追加をしました。

試してみましょう。

なお、私はfish shellを使用しており、source ../assume-role.shを実行しても環境変数がセットされません。対応としてbassを使ってシェルスクリプトを呼び出します。

$ bash ../assume-role.sh -h
Usage: ../assume-role.sh [-p PROFILE] [-t TOKEN_CODE]
  -p PROFILE     AWS profile name in /<ホームディレクトリ>/.aws/config
  -t TOKEN_CODE  MFA token code
If no arguments are provided, the script will run in interactive mode.

$ bass source ../assume-role.sh -p <プロファイル名> -t <MFAトークンコード>
AWS_DEFAULT_REGION : us-east-1
{
    "UserId": "<IAMユーザーID>:session-1719125569",
    "Account": "<Assume Role先AWSアカウントID>",
    "Arn": "arn:aws:sts::<Assume Role先AWSアカウントID>:assumed-role/<IAMロール名>/session-1719125569"
}

$ echo $AWS_SESSION_TOKEN
IQoJb3JpZ2luX2VjEMr..(以下略)..

正常にAssume Roleでき、環境変数がセットされたようです。

CloudFront Hosting Toolkitを使ったデプロイ(2回目)

それではデプロイです。

$ npx cloudfront-hosting-toolkit deploy

 --> 1. Setting up a SSL/TLS certificate with AWS Certificate Manager (ACM)

status=undefined
A CNAME record has been added to your hosted zone. It may take a few minutes for the ACM service to validate your domain. Please wait for the validation process to complete.
A CNAME record has been added to your hosted zone. It may take a few minutes for the ACM service to validate your domain. Please wait for the validation process to complete.

Certificate is not ready to be used. Waiting ...

.
.
(中略)
.
.

Certificate is not ready to be used. Waiting ...

Certificate is not ready to be used. Waiting ...

The certificate is ready to be used.

 --> 2. Bootstrapping your AWS account

Please wait ... | ████████████████████████████████████████ | 100%

 --> 3. Create resources for connecting your AWS account to your GitHub repository.

Please wait ... | ████████████████████████████████████████ | 100%

 --> 4. Configure github connection

You need to complete Github authentication using the AWS Console.
To do so, follow these steps:

 1. Open the following page:
    https://us-east-1.console.aws.amazon.com/codesuite/settings/connections
 2. In the connection list, look out for connection name: cloudfront-hosting-toolkit-test-
 3. For this connection with Status=Pending, complete the connection by following these instructions:
     - Select the pending connection  cloudfront-hosting-toolkit-test-.
     - Click on Update a pending connection.
     - In the new popup window, under Github apps, choose an app installation, or create a new app by selecting Install a new app.
     - If you have already installed an app, select the app, click on Connect, and refresh the page if needed. The status of the connection should be Available.
     - If you choose to Install a new app, follow the on-screen instructions to authenticate your Github account, click on Connect, and refresh the page if needed. The status of the connection should be Available.




? Please complete the operation and type 'ok' to continue  ›

GitHub連携をするように催促されました。

ちなみに、現時点で以下のようなログファイルが出力されていました。AWS CDKを内部的に実行していることがわかりますね。

./cloudfront-hosting-toolkit-test/cloudfront-hosting-toolkit/2024-06-22_18-46-53.log

══════════════════════
npx cdk bootstrap --context config-path=/<作業ディレクトリパス>/cloudfront-hosting-toolkit-test/cloudfront-hosting-toolkit --context certificate-arn=arn:aws:acm:us-east-1:<Assume Role先AWSアカウントID>:certificate/1c7238d4-0620-4327-b8ea-389f72b68bd9


══════════════════════

 ⏳  Bootstrapping environment aws://<Assume Role先AWSアカウントID>/us-east-1...
Trusted accounts for deployment: (none)
Trusted accounts for lookup: (none)
Using default execution policy of 'arn:aws:iam::aws:policy/AdministratorAccess'. Pass '--cloudformation-execution-policies' to customize.
CDKToolkit: creating CloudFormation changeset...
CDKToolkit | 0/4 | 18:47:23 | UPDATE_IN_PROGRESS   | AWS::IAM::Role          | DeploymentActionRole 
CDKToolkit | 0/4 | 18:47:17 | UPDATE_IN_PROGRESS   | AWS::CloudFormation::Stack | CDKToolkit User Initiated
CDKToolkit | 0/4 | 18:47:21 | UPDATE_IN_PROGRESS   | AWS::SSM::Parameter     | CdkBootstrapVersion 
CDKToolkit | 1/4 | 18:47:21 | UPDATE_COMPLETE      | AWS::SSM::Parameter     | CdkBootstrapVersion 
CDKToolkit | 2/4 | 18:47:39 | UPDATE_COMPLETE      | AWS::IAM::Role          | DeploymentActionRole 
CDKToolkit | 3/4 | 18:47:41 | UPDATE_COMPLETE_CLEA | AWS::CloudFormation::Stack | CDKToolkit 
CDKToolkit | 4/4 | 18:47:42 | UPDATE_COMPLETE      | AWS::CloudFormation::Stack | CDKToolkit 
 ✅  Environment aws://<Assume Role先AWSアカウントID>/us-east-1 bootstrapped.

./cloudfront-hosting-toolkit-test/cloudfront-hosting-toolkit/2024-06-22_18-47-52.log

══════════════════════
npx cdk deploy  hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 --context config-path=/<作業ディレクトリパス>/cloudfront-hosting-toolkit-test/cloudfront-hosting-toolkit  --context certificate-arn=arn:aws:acm:us-east-1:<Assume Role先AWSアカウントID>:certificate/1c7238d4-0620-4327-b8ea-389f72b68bd9 


══════════════════════


✨  Synthesis time: 1.8s

hosting-connection-cloudfront-hosting-toolkit-test-main-non-97:  start: Building ebe9a8787caa48eb49bbe1c33dc994d46c6414a6a127b16df154acd2dbfa645a:<Assume Role先AWSアカウントID>-us-east-1
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97:  success: Built ebe9a8787caa48eb49bbe1c33dc994d46c6414a6a127b16df154acd2dbfa645a:<Assume Role先AWSアカウントID>-us-east-1
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97:  start: Publishing ebe9a8787caa48eb49bbe1c33dc994d46c6414a6a127b16df154acd2dbfa645a:<Assume Role先AWSアカウントID>-us-east-1
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97:  success: Published ebe9a8787caa48eb49bbe1c33dc994d46c6414a6a127b16df154acd2dbfa645a:<Assume Role先AWSアカウントID>-us-east-1
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97: deploying... [1/1]
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97: creating CloudFormation changeset...
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 1/6 | 18:48:19 | CREATE_COMPLETE      | AWS::CloudFormation::Stack           | hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 1/6 | 18:48:05 | REVIEW_IN_PROGRESS   | AWS::CloudFormation::Stack           | hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 User Initiated
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 1/6 | 18:48:13 | CREATE_IN_PROGRESS   | AWS::CloudFormation::Stack           | hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 User Initiated
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 1/6 | 18:48:15 | CREATE_IN_PROGRESS   | AWS::SSM::Parameter                  | RepositoryConnection/SSMConnectionRegion (RepositoryConnectionSSMConnectionRegion4029CE52) 
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 1/6 | 18:48:15 | CREATE_IN_PROGRESS   | AWS::SSM::Parameter                  | RepositoryConnection/SSMConnectionName (RepositoryConnectionSSMConnectionNameAB648C27) 
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 1/6 | 18:48:15 | CREATE_IN_PROGRESS   | AWS::CodeStarConnections::Connection | RepositoryConnection/MyCfnConnectioncloudfront-hosting-toolkit-test (RepositoryConnectionMyCfnConnectioncloudfronthostingtoolkittest6BB0B6C0) 
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 1/6 | 18:48:15 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata                   | CDKMetadata/Default (CDKMetadata) 
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 1/6 | 18:48:16 | CREATE_IN_PROGRESS   | AWS::CodeStarConnections::Connection | RepositoryConnection/MyCfnConnectioncloudfront-hosting-toolkit-test (RepositoryConnectionMyCfnConnectioncloudfronthostingtoolkittest6BB0B6C0) Resource creation Initiated
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 1/6 | 18:48:16 | CREATE_IN_PROGRESS   | AWS::SSM::Parameter                  | RepositoryConnection/SSMConnectionName (RepositoryConnectionSSMConnectionNameAB648C27) Resource creation Initiated
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 1/6 | 18:48:16 | CREATE_IN_PROGRESS   | AWS::SSM::Parameter                  | RepositoryConnection/SSMConnectionRegion (RepositoryConnectionSSMConnectionRegion4029CE52) Resource creation Initiated
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 2/6 | 18:48:16 | CREATE_COMPLETE      | AWS::CodeStarConnections::Connection | RepositoryConnection/MyCfnConnectioncloudfront-hosting-toolkit-test (RepositoryConnectionMyCfnConnectioncloudfronthostingtoolkittest6BB0B6C0) 
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 2/6 | 18:48:16 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata                   | CDKMetadata/Default (CDKMetadata) Resource creation Initiated
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 3/6 | 18:48:16 | CREATE_COMPLETE      | AWS::SSM::Parameter                  | RepositoryConnection/SSMConnectionName (RepositoryConnectionSSMConnectionNameAB648C27) 
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 4/6 | 18:48:16 | CREATE_COMPLETE      | AWS::SSM::Parameter                  | RepositoryConnection/SSMConnectionRegion (RepositoryConnectionSSMConnectionRegion4029CE52) 
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 5/6 | 18:48:16 | CREATE_COMPLETE      | AWS::CDK::Metadata                   | CDKMetadata/Default (CDKMetadata) 
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 5/6 | 18:48:17 | CREATE_IN_PROGRESS   | AWS::SSM::Parameter                  | RepositoryConnection/SSMConnectionArn (RepositoryConnectionSSMConnectionArn1E74102B) 
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 5/6 | 18:48:18 | CREATE_IN_PROGRESS   | AWS::SSM::Parameter                  | RepositoryConnection/SSMConnectionArn (RepositoryConnectionSSMConnectionArn1E74102B) Resource creation Initiated
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97 | 6/6 | 18:48:18 | CREATE_COMPLETE      | AWS::SSM::Parameter                  | RepositoryConnection/SSMConnectionArn (RepositoryConnectionSSMConnectionArn1E74102B) 

 ✅  hosting-connection-cloudfront-hosting-toolkit-test-main-non-97

✨  Deployment time: 18.76s

Outputs:
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97.ExportsOutputFnGetAttRepositoryConnectionMyCfnConnectioncloudfronthostingtoolkittest6BB0B6C0ConnectionArnC6089F79 = arn:aws:codestar-connections:us-east-1:<Assume Role先AWSアカウントID>:connection/aeca0b6c-87a0-4f45-b2b2-6ad39e0648fa
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97.RepositoryConnectionConnectionArn087CB8E2 = arn:aws:codestar-connections:us-east-1:<Assume Role先AWSアカウントID>:connection/aeca0b6c-87a0-4f45-b2b2-6ad39e0648fa
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97.RepositoryConnectionConnectionNameFD2EABB9 = cloudfront-hosting-toolkit-test-
hosting-connection-cloudfront-hosting-toolkit-test-main-non-97.RepositoryConnectionHostingRegion1B4A18AD = us-east-1
Stack ARN:

✨  Total time: 20.56s

arn:aws:cloudformation:us-east-1:<Assume Role先AWSアカウントID>:stack/hosting-connection-cloudfront-hosting-toolkit-test-main-non-97/85e351c0-307c-11ef-b1e6-0ecb10ce0993

また、この時点で証明書が作成されていました。CloudFormationのスタックを作成する際のログにはACMが含まれていないので、こちらはCloudFormationで管理されていないようです。

4.証明書が作成されたことを確認

GitHubへの接続はステータスが保留中になっていました。

5.cloudfront-hosting-toolkit-test-

以下記事を参考にGitHub連携をします。

保留中の接続を更新をクリックすると、権限確認を促されるのでAuthorize AWS Connector for GitHubをクリックします。

6.AWS Connector for GitHub by Amazon Web Services would like permission to-

新しいアプリをインストールするをクリックします。

7.新しいアプリをインストールする

インストール先を選択します。

8.Install AWS Connector

インストールが完了したら、連携するリポジトリを選択してSaveをクリックします。

9.Only select repositories

AWSマネジメントコンソールに戻ってGitHubアプリのIDを選択し、接続をクリックします。

10.接続

正常に連携が完了しました。

11.接続 cloudfront-hosting-toolkit-test- が正常に作成されました。これで、ポップアップウィンドウを閉じることができます

デプロイを実行します。

✔ Please complete the operation and type 'ok' to continue  … ok

 --> 5. Deploy the hosting infrastructure within your AWS account

Please wait while deploying the infrastructure, it may take a few minutes.
Please wait ... | ████████████████████████████████████████ | 100%

_____________________________________________________________________


The Origin paired with its associated CloudFront domain name:

>       Code Repository: https://github.com/non-97/cloudfront-hosting-toolkit-test.git
>       Hosting: https://d3kt1ccx65w6km.cloudfront.net



? Would you like to associate the domain name to the CloudFront distribution automatically now, or would you prefer to do it later? › - Use arrow-keys. Return to submit.
❯   Associate automatically now.
    Do it later.

CloudFrontの作成が完了したようです。OACでオリジンも設定されており、ビヘイビアでパスパターンごとにキャッシュポリシーやレスポンスヘッダーポリシーを細かく設定しているようです。

16.CloudFront distributionが作成されていることを確認

17.OACが設定されていた

18.ビヘイビア

作成されたリソースの全容は以下のとおりです。

12_hosting-main-cloudfront-hosting-toolkit-test-main_1

13_hosting-main-cloudfront-hosting-toolkit-test-main_2

14.hosting-main-cloudfront-hosting-toolkit-test-main_2

15.hosting-main-cloudfront-hosting-toolkit-test-main_4

ただし、現時点ではCloudFrontディストリビューションへのAliasレコードは登録されていないようです。

20.ホストゾーンにCFのレコードはまだ登録されていない

Associate automatically now.を選択して、Aliasレコードを登録させます。

✔ Would you like to associate the domain name to the CloudFront distribution automatically now, or would you prefer to do it later? › Associate automatically now.

A new A Record has been added/updated to your DNS records that points to your CloudFront distribution:

>       cf-test.non-97.net -> d3kt1ccx65w6km.cloudfront.net

It may take a few minutes to reflect the change.

In the future, whenever you push changes to your Github repository, an automatic pipeline will be triggered to deploy the new version.

_____________________________________________________________________


The hosting infrastructure for your project has been successfully deployed on your AWS account


The deployment consists of the following AWS CloudFormation stack(s):
>           hosting-connection-cloudfront-hosting-toolkit-test-main-non-97
>           hosting-main-cloudfront-hosting-toolkit-test-main

The following AWS CodePipeline has been created for automatic deployment upon Git push execution:
>           non-97-cloudfront-hosting-toolkit-test

You can review the resources deployed by logging into the AWS Management Console at https://aws.amazon.com/console

The pipeline has been initiated following the recent deployment to apply any changes made.
The pipeline is in progress, waiting for the pipeline to finish...

Please wait ... | ████████████████████████████████████████ | 100%


 *** ERROR ***


The pipeline execution encountered an error on the stage 'ChangeUri'.
To investigate and resolve the issue, please follow these steps:

A. Explore our troubleshooting section https://github.com/awslabs/cloudfront-hosting-toolkit/blob/main/docs/troubleshooting.md


B. Inspect the pipeline execution details

   1. Visit the AWS Management Console.
   2. Navigate to AWS CodePipeline.
   3. Select the "non-97-cloudfront-hosting-toolkit-test" pipeline.
   4. Inspect the pipeline execution details and logs for error messages.
   5. Take the necessary actions to address the error.
   6. Once resolved, you can trigger a new pipeline execution by choosing 'Release change' on the AWS Console.

どうやらコンテンツのパイプラインでChangeUriが失敗しているようです。

CodePipelineを確認すると、確かに失敗していました。

19.CodePipeline

こちらの影響で、アクセスをしても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.と表示されてしまいます。

23.Deployment status

CodePipelineのChangeUri呼び出し先のStep Functionsを確認すると、Update KeyValueStoreが失敗しています。

21.Execution__c0369ae2-e660-4d47-a108-046661c10d95

ログは以下のとおりです。呼び出し先のLambda関数上でAWS CRTライブラリが見つからないようです。

{
  "errorType": "Error",
  "errorMessage": "AWS CRT binary not present in any of the following locations:\n\t/opt/nodejs/node_modules/aws-crt/dist/bin/linux-x64-glibc/aws-crt-nodejs.node",
  "trace": [
    "Error: AWS CRT binary not present in any of the following locations:",
    "\t/opt/nodejs/node_modules/aws-crt/dist/bin/linux-x64-glibc/aws-crt-nodejs.node",
    "    at Object.<anonymous> (/opt/nodejs/node_modules/aws-crt/dist/native/binding.js:109:11)",
    "    at Module._compile (node:internal/modules/cjs/loader:1358:14)",
    "    at Module._extensions..js (node:internal/modules/cjs/loader:1416:10)",
    "    at Module.load (node:internal/modules/cjs/loader:1208:32)",
    "    at Module._load (node:internal/modules/cjs/loader:1024:12)",
    "    at Module.require (node:internal/modules/cjs/loader:1233:19)",
    "    at require (node:internal/modules/helpers:179:18)",
    "    at Object.<anonymous> (/opt/nodejs/node_modules/aws-crt/dist/native/auth.js:20:35)",
    "    at Module._compile (node:internal/modules/cjs/loader:1358:14)",
    "    at Module._extensions..js (node:internal/modules/cjs/loader:1416:10)"
  ]
}

こちらのLambda関数にはHostingPipelineInfrastructureUpdateCFFAwsSdkLayerEA1E386Eがアタッチされていました。

こちらでのLambda Layerは../lambda/layers/aws_sdk`をバンドルしているようです。

/lib/deployment_workflow_sf.ts

    const awsSdkLayer = new lambda.LayerVersion(this, "AwsSdkLayer", {
      compatibleRuntimes: [lambda.Runtime.NODEJS_20_X],
      code: lambda.Code.fromAsset(path.join(__dirname, "../lambda/layers/aws_sdk")),
      description: "AWS SDK lib including client-cloudfront-keyvaluestore",
    });

package.jsonにはAWS CRTは含まれていそうです。

/lambda/layers/aws_sdk/nodejs/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"
}

Lambda Layerをダウンロードしてきて解凍したディレクトリを確認します。

$ ls -l ../../HostingPipelineInfrastructureUpdateCFFAwsSdkLayerEA1E386E-081b2836-9d7c-46e9-9a03-247c1db1bf74/nodejs/node_modules/aws-crt/
total 8
drwxr-xr-x@ 6 <ユーザー名>  staff   192  6 22 19:47 dist/
drwxr-xr-x@ 6 <ユーザー名>  staff   192  6 22 19:47 dist.browser/
drwxr-xr-x@ 3 <ユーザー名>  staff    96  6 22 19:45 lib/
-rw-r--r--@ 1 <ユーザー名>  staff  1999  1  1  1980 package.json
drwxr-xr-x@ 7 <ユーザー名>  staff   224  6 22 19:45 scripts/

$ ls -l ../../HostingPipelineInfrastructureUpdateCFFAwsSdkLayerEA1E386E-081b2836-9d7c-46e9-9a03-247c1db1bf74/nodejs/node_modules/aws-crt/dist/
total 8
drwxr-xr-x@ 18 <ユーザー名>  staff   576  6 22 19:45 common/
-rw-r--r--@  1 <ユーザー名>  staff  2948  1  1  1980 index.js
drwxr-xr-x@ 19 <ユーザー名>  staff   608  6 22 19:45 native/

確かにエラーログで出力されていたnodejs/node_modules/aws-crt/dist/bin/linux-x64-glibc/aws-crt-nodejs.nodeは見つかりません。

CloudFront Hosting Toolkitをグローバルインストールをして再デプロイ

もしかして、CloudFront Hosting Toolkitをグローバルインストールではなく、ローカルインストールで使用しているためでしょうか。

CloudFront Hosting Toolkitをグローバルインストールをして再デプロイします。

グローバルインストールします。

$ npm install -g @aws/cloudfront-hosting-toolkit
npm WARN deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm WARN deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm WARN deprecated @aws-sdk/config-resolver@3.374.0: This package has moved to @smithy/config-resolver
npm WARN deprecated @aws-sdk/node-config-provider@3.374.0: This package has moved to @smithy/node-config-provider

added 310 packages in 25s

26 packages are looking for funding
  run `npm fund` for details

デプロイします。

cloudfront-hosting-toolkit deploy

 --> 1. Setting up a SSL/TLS certificate with AWS Certificate Manager (ACM)

status=ISSUED
The certificate for this domain has already been created.

The certificate is ready to be used.

 --> 2. Bootstrapping your AWS account

Please wait ... | ████████████████████████████████████████ | 100%

 --> 3. Create resources for connecting your AWS account to your GitHub repository.

Please wait ... | ████████████████████████████████████████ | 100%

 --> 4. Deploy the hosting infrastructure within your AWS account

Please wait while deploying the infrastructure, it may take a few minutes.
Please wait ... | ████████████████████████████████████████ | 100%

_____________________________________________________________________


The Origin paired with its associated CloudFront domain name:

>       Code Repository: https://github.com/non-97/cloudfront-hosting-toolkit-test.git
>       Hosting: https://d3kt1ccx65w6km.cloudfront.net

>       Domain name 'cf-test.non-97.net' is already associated with the CloudFront distribution.


In the future, whenever you push changes to your Github repository, an automatic pipeline will be triggered to deploy the new version.

_____________________________________________________________________


The hosting infrastructure for your project has been successfully deployed on your AWS account


The deployment consists of the following AWS CloudFormation stack(s):
>           hosting-connection-cloudfront-hosting-toolkit-test-main-non-97
>           hosting-main-cloudfront-hosting-toolkit-test-main

The following AWS CodePipeline has been created for automatic deployment upon Git push execution:
>           non-97-cloudfront-hosting-toolkit-test

You can review the resources deployed by logging into the AWS Management Console at https://aws.amazon.com/console

The pipeline has been initiated following the recent deployment to apply any changes made.
The pipeline is in progress, waiting for the pipeline to finish...

Please wait ... | ████████████████████████████████████████ | 100%
Current pipeline status: Succeeded

今度は最後までデプロイが完了しました。グローバルインストールしたものを使っても、参照するのはカレントディレクトリの./cloudfront-hosting-toolkit配下の設定ファイルを参照するためか別スタックでのデプロイではないようです。

デプロイ時のイベントを確認すると、Requested update requires the creation of a new physical resource; hence creating one.とLambda Layerが新しく作られるようです。

24.cloudfront-hosting-toolkitをグローバルインストールしてdeploy

前回失敗していたCodePipelineを確認すると、最後まで正常に実行が完了しています。

25_CodePipelineが成功したことを確認

先ほど失敗していたLambda関数も正常に実行完了しています。

/aws/lambda/hosting-main-cloudfront-h-HostingPipelineInfrastru-tfbTogV4wSp7

INIT_START Runtime Version: nodejs:20.v23	Runtime Version ARN: arn:aws:lambda:us-east-1::runtime:92c5bcb1529200756eb64a0d90d4ab606fdaf21421321da6c202187b88833f52
START RequestId: 71567e55-f1e1-49b3-bf9c-c213298e06c9 Version: $LATEST
2024-06-23T07:21:47.751Z	71567e55-f1e1-49b3-bf9c-c213298e06c9	INFO	event=
{
    "commitId": "a680b14f8cefc6aacd2c6a4b07e8a4edf108bae6"
}

2024-06-23T07:21:47.754Z	71567e55-f1e1-49b3-bf9c-c213298e06c9	INFO	Get ETAG for KVS arn:aws:cloudfront::<Assume Role先AWSアカウントID>:key-value-store/a8423ea3-4665-4cb4-bf70-a0bc3f85fa47
2024-06-23T07:21:48.038Z	71567e55-f1e1-49b3-bf9c-c213298e06c9	INFO	Update KVS using ETAG KVTVPDKIKX0DER
2024-06-23T07:21:48.193Z	71567e55-f1e1-49b3-bf9c-c213298e06c9	INFO	KVS updated
END RequestId: 71567e55-f1e1-49b3-bf9c-c213298e06c9
REPORT RequestId: 71567e55-f1e1-49b3-bf9c-c213298e06c9	Duration: 445.30 ms	Billed Duration: 446 ms	Memory Size: 512 MB	Max Memory Used: 93 MB	Init Duration: 400.97 ms

試しにアクセスします。

$ curl https://cf-test.non-97.net/README.md
# cloudfront-hosting-toolkit-test

正常にアクセスできました。

その他、cloudfront-hosting-toolkitコマンドで確認できる情報は以下のとおりです。

$ cloudfront-hosting-toolkit show

The Origin paired with its associated CloudFront domain name:

Code Repository: https://github.com/non-97/cloudfront-hosting-toolkit-test.git/main -->  Hosting: https://d3kt1ccx65w6km.cloudfront.net

cf-test.non-97.net -->  Hosting: d3kt1ccx65w6km.cloudfront.net

$ cloudfront-hosting-toolkit status
Current pipeline status: Succeeded

GitにコンテンツをPush

GitにコンテンツをPushしてコンテンツがデプロイされるかどうか確認します。

適当にファイルを用意してPushします。

$ git add .
$ git ls-files
.gitignore
README.md
dir/index.html
index.html
test.html

$ git commit -m "cloudfront-hosting-toolkitのCI/CDパイプラインの動作確認のため適当にファイルを追加"
[main 3d54ae9] cloudfront-hosting-toolkitのCI/CDパイプラインの動作確認のため適当にファイルを追加
 4 files changed, 200 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 dir/index.html
 create mode 100644 index.html
 create mode 100644 test.html

$ git push
Enumerating objects: 8, done.
Counting objects: 100% (8/8), done.
Delta compression using up to 10 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (7/7), 1.99 KiB | 1.99 MiB/s, done.
Total 7 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/non-97/cloudfront-hosting-toolkit-test.git
   a680b14..3d54ae9  main -> main

すると、パイプラインが起動して正常に完了していました。

27.GitHubにpushしたらCodePipelineが動作した

コンテンツを保存しているS3バケットを確認します。コミットIDのディレクトリが作成されており、中にはコンテンツが保存されていました。

29.hosting-main-cloudfront-h-hostinghostinginfrastruc-xetcoemvzsox

30.3d54ae90e02c21fc236e7790f779226c79d6ed18

初回デプロイ時に存在していたコンテンツは、最新のデプロイのタイミングで削除されるようです。

/aws/lambda/hosting-main-cloudfront-h-HostingPipelineInfrastru-kEMrqVpuIwO4

INIT_START Runtime Version: nodejs:18.v29	Runtime Version ARN: arn:aws:lambda:us-east-1::runtime:5c2d7f0b914a9dbb8b6a6e3117c7950fa2b7434331c349799226fadd052f19a9
START RequestId: bd15acf8-a7f7-46ae-b0dc-f8624b6fdcc4 Version: $LATEST
2024-06-23T07:40:38.594Z	bd15acf8-a7f7-46ae-b0dc-f8624b6fdcc4	INFO	event=
{
    "commitId": "3d54ae90e02c21fc236e7790f779226c79d6ed18"
}

2024-06-23T07:40:39.858Z	bd15acf8-a7f7-46ae-b0dc-f8624b6fdcc4	INFO	Deleted file: a680b14f8cefc6aacd2c6a4b07e8a4edf108bae6/README.md
2024-06-23T07:40:39.858Z	bd15acf8-a7f7-46ae-b0dc-f8624b6fdcc4	INFO	Deleted 1 objects successfully.
END RequestId: bd15acf8-a7f7-46ae-b0dc-f8624b6fdcc4
REPORT RequestId: bd15acf8-a7f7-46ae-b0dc-f8624b6fdcc4	Duration: 1285.37 ms	Billed Duration: 1286 ms	Memory Size: 128 MB	Max Memory Used: 90 MB	Init Duration: 381.72 ms

実際にアクセスしましょう。

$ curl https://cf-test.non-97.net/README.md
# cloudfront-hosting-toolkit-test

$ curl https://cf-test.non-97.net/
index.html

$ curl https://cf-test.non-97.net/test.html
test.html

$ curl https://cf-test.non-97.net/dir/
dir/index.html

$ curl https://cf-test.non-97.net/dir/ -I
HTTP/2 200
date: Sun, 23 Jun 2024 07:51:39 GMT
content-type: text/html
content-length: 15
last-modified: Sun, 23 Jun 2024 07:40:27 GMT
etag: "bdf7745a2f17049b4a445c5a088e1fda"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 a4ab9ca675174fa667c8399f24cb4440.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-P2
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: FRCGNlVMuFh0GsIiquipydye50cEmsWmyYgNuFPziw48dpiWbJyIAA==
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

$ curl https://cf-test.non-97.net/dir/ -I
HTTP/2 200
date: Sun, 23 Jun 2024 07:51:43 GMT
content-type: text/html
content-length: 15
last-modified: Sun, 23 Jun 2024 07:40:27 GMT
etag: "bdf7745a2f17049b4a445c5a088e1fda"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 3c3704d1d972509b35eb599b7ec5b18e.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-P2
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: uI2poobRk_X_dufm0vUYm46uYjNHBfoHnj9RSRYuJwTdIBwen4MWNg==
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

$ curl https://cf-test.non-97.net/.gitignore
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>MAY2Z458MFMACCCF</RequestId><HostId>q0kpOR+we6uBy7fv7+ZvNrDt69++HGmIQfajnRknb9xmtldZq7cLijzqiuWym4BEZp6v/k54lv4YStJ+AEK8DYhXt1eG1cxaQK6uSxnv/+I=</HostId></Error>

$ curl https://cf-test.non-97.net/.gitignore -I
HTTP/2 403
date: Wed, 26 Jun 2024 04:49:00 GMT
content-type: application/xml
server: AmazonS3
x-cache: Error from cloudfront
via: 1.1 5519434325290aca21702ef9e3fa5194.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-P2
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: Ud9kYAqDyWm4hwtPS57yQtRxr84ECexh0uqdS1jFkLWZwh0wU5P2iA==
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

コンテンツが反映されていますね。CloudFront Functionsを使ったディレクトリインデックスも正常に動作しています。

なお、.gitignoreについてはS3バケット上に展開されていないため、アクセスはできませんでした。

サクッと静的Webサイトを用意したい場合に

CloudFront Hosting Toolkitを使って静的Webサイト環境を作ってみました。

CodePipeline周りやコンテンツを切り替える処理を自作しようとすると結構大変です。サクッと静的Webサイトを用意したい場合に非常に使いやすそうですね。

なお、デプロイされるCodePipelineはV1です。モノレポでありファイルパスのトリガーを使いたいという場合にはCodePipeline V2に変更してあげましょう。

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!