AWS SAMで静的Webホスティング自動アップロードを試みた記録

記事タイトルとURLをコピーする

はじめに

こんにちは、山本です。

先日、AWS SAM(Serverless Application Model)を使用してフロントエンド用の静的WebサイトをS3でホスティングしようとしたとき、 「index.htmlなどの静的ファイルをSAMテンプレートに含めてデプロイ時に自動配置できれば運用が楽になるのではないか」と考えたので実装してみました。

その中で、AWS SAMの仕様によりスタック作成時にオブジェクトをS3へアップロードする方法が用意されていない(SAMテンプレート内に直接的に記述できない)ことがわかりました。
そこで、CloudFormationのカスタムリソースとLambda関数を組み合わせて、SAM内で完結する形で静的ファイルを自動アップロードできるかどうかを試してみたので、実装方法と所感についてブログに起こしたいと思います。

SAM では S3 オブジェクト配置ができない?

AWS SAM では AWS::S3::Bucket リソースを定義することは可能です。

Resources:
  MyBucket:
    Type: AWS::S3::Bucket

しかし、以下のようにindex.html を同時に配置する記述はできません

Type: AWS::S3::Object

つまり、S3バケットは作れますが、ファイルの中身まではCloudFormation(SAM)で定義できないという制約があります。

解決方法:カスタムリソースの使用

まず、前提としてAWS SAM テンプレートはCloudFormation テンプレートを拡張したものです。
ビルド作業を行うと CloudFormation ネイティブリソースに変換されます。
そのため、SAM テンプレート内には CloudFormation ネイティブリソースも直接記述でき、SAM リソースと混在させることが可能です。

CloudFormation には Custom:: リソースという任意の処理を差し込める仕組みがあります。
この方法を使えば、 S3 バケット作成後に Lambda を使ってファイルを配置するというアプローチが取れます。

具体的な構成と流れとしては以下のようになります。

SAMテンプレート
├─ S3バケット(静的Webホスティング有効)
├─ Lambda関数(静的ファイルアップロード)
└─ Custom Resource → Lambda起動 → S3にファイル配置

また、今回は Lambda 内に直接 site/index.html を含んでデプロイをしています。 これは構成がシンプルで実装もわかりやすいのですが、実際にはサイズの制限がありますので、静的ファイルの容量が大きくなるにつれて Lambda レイヤーを使用する方法に置き換えるなど対策が必要となります。

元々 AWS SAM テンプレートで定義していた template.yaml に以下の部分を追加します。
カスタムリソースを定義し、ServiceTokenに指定した Lambda 関数が実行され、 S3 に静的ファイルをアップロードしています。

TriggerUpload:
  Type: Custom::UploadStaticFiles
  Properties:
    ServiceToken: !GetAtt UploadFunction.Arn

以下、実行する Lambda 関数の全体コードです。 /var/task/site に配置された静的ファイルを全て読み込み、S3バケットに配置しています。 また、CloudFormationに対しては sendResponse() 関数を通じて正常終了を通知しています。

const AWS = require('aws-sdk');
const fs = require('fs');
const path = require('path');

// アップロード先のS3バケット名
const s3 = new AWS.S3();
const bucketName = process.env.BUCKET_NAME;

// Layerに格納された静的ファイルのディレクトリ
const localDir = '/var/task/site';

exports.handler = async function (event, context) {
  console.log('Event:', JSON.stringify(event));

  // CloudFormationスタック削除時(Deleteイベント)は処理スキップ
  if (event.RequestType === 'Delete') {
    return sendResponse(event, context, 'SUCCESS');
  }

  // siteディレクトリ内のすべてのファイルをS3にアップロード
  const files = fs.readdirSync(localDir);
  for (const file of files) {
    const filePath = path.join(localDir, file);
    const fileContent = fs.readFileSync(filePath);
    
    // Content-Typeを簡易判定
    const contentType = file.endsWith('.html') ? 'text/html' : 'text/plain';

    await s3.putObject({
      Bucket: bucketName,
      Key: file,
      Body: fileContent,
      ContentType: contentType,
    }).promise();

    console.log(`Uploaded: ${file}`);
  }

  // CloudFormationへ正常終了の通知
  return sendResponse(event, context, 'SUCCESS');
};

// CloudFormationにレスポンスを返すための処理
function sendResponse(event, context, status) {
  const https = require('https');
  const responseBody = JSON.stringify({
    Status: status,
    Reason: 'See the details in CloudWatch Log Stream: ' + context.logStreamName,
    PhysicalResourceId: context.logStreamName,
    StackId: event.StackId,
    RequestId: event.RequestId,
    LogicalResourceId: event.LogicalResourceId,
  });

  const parsedUrl = new URL(event.ResponseURL);
  const options = {
    hostname: parsedUrl.hostname,
    port: 443,
    path: parsedUrl.pathname + parsedUrl.search,
    method: 'PUT',
    headers: {
      'Content-Type': '',
      'Content-Length': responseBody.length,
    },
  };

  return new Promise((resolve, reject) => {
    const req = https.request(options, (res) => {
      console.log(`Status: ${res.statusCode}`);
      resolve();
    });

    req.on('error', (err) => {
      console.error('sendResponse error:', err);
      reject(err);
    });

    req.write(responseBody);
    req.end();
  });
}

動作確認と結果

今回は EC2 へ VSCode を使用した ssh 接続の元実行しているので、上述したファイルの追加・編集を終えた後
以下のコマンドを使用してビルド&デプロイを行います。

sam build
sam deploy 

デプロイ後、コンソールの OutPuts 欄に Web サイトの URL が表示されますのでブラウザにコピー&ペーストして確認します。

OutPutsでのWebサイトURL

表示されたWebページ

超簡易的ですがブラウザでの確認が取れましたのでアップロードが Lambda により完了したことがわかります。

所感・おわりに

今回の検証では、AWS SAMを使って静的WebサイトをS3に自動アップロードする仕組みを構築してみました。

AWS SAMでは本来、S3オブジェクトの配置を直接テンプレートに記述することはできません。 そのため、CloudFormationのカスタムリソース + Lambda関数を使って、 スタックデプロイ時にHTMLファイルを自動アップロードする構成を組みました。

今回のような構成を組むことで 一度のデプロイで静的コンテンツの更新まで行うことができ、私が試したかった AWS SAM 内でデプロイを完結する構成とその仕組みを理解することができました。

しかし、実際に実装する中で気づきましたがこのように一度の AWS SAM のデプロイで全て実装することも運用としては間違いではありませんが、
実運用では aws s3 sync などの CLI で柔軟にアップロードできる構成の方が、特に開発段階などで「柔軟性」の面では効率的なのではないかと感じました。

AWS SAM でのデプロイを行うと作成済みのリソースに関しても差分を確かめたりするのでその分時間がかかってしまい、少しのWebサイト内のマイナーチェンジの反映だけなのに多くの時間がかかってしまうケースなどもあるので AWS SAM で完結させることが最適解でもなく、場合によって処理の分散を考えなければならないということを学びました。

AWS CDK などでは コード内で S3 オブジェクト配置も可能になってはいますが、上記観点のもと適宜調整する必要はあると思います。
AWS SAM はテンプレートとしてとても扱いやすく、IaC におけるサーバーレス構成について学びやすいのでぜひ皆さんも活用してみてはいかがでしょうか。

山本 竜也 (記事一覧)

2025年度新入社員です!AWSについてはほぼ未経験なのでたくさんアウトプットできるよう頑張ります✨