静的サイトについてローカルに開発環境を持たない&リリースについてSFTPで接続してのアップロードを行っていたという状況を改善する目的で、S3+CloudFrontのサーバーレスな環境にServerless Frameworkを利用してデプロイする構成を考えました。
90%くらい以下の2記事を参考にさせていただきました。
- CloudFront + S3 で静的ページをホストするための Serverless Framework 設定
- Serverless Framework + S3 + CloudFront – Qiita
今回もServerless Frameworkに関する話題ですが、これまでのLambda関数をデプロイする内容とは少し毛色の違う使い方に関する内容となります。
この構成のメリット、デメリット
- メリット
- ローカルからコマンド1つで静的サイトを公開できる
- Webサーバーを動かす場所が不要(EC2、Lightsail、その他VPSサービス等)
- 低コスト(のはず)
- 乗っかるサービスの仕組み上、アクセス集中に関する心配が不要
- デメリット
- CloudFrontへのEDoSによって多額の請求が来る可能性がある
など
環境構築
今回作ったGitHubリポジトリは自分用のコメントが多数残っている状態なので非公開となります。そのため各リソースについて要点を記事の中で記載します。
FROM node:18-slim SHELL ["/bin/bash", "-oeux", "pipefail", "-c"] RUN npm install -g http-server@latest USER node WORKDIR /workspace CMD [ "npx", "http-server", "-p", "80"]
始めにDockerfileですが、Serverless Frameworkを導入するのにnpmが必要なので、ベースとなるDockerイメージはnodeとなります。今回はnode:18-slimを使用。
また、静的サイトについてローカルで動作確認するのに、npmでインストール可能なhttp-serverというパッケージをグローバルインストールしています。http-serverのWebサーバーをCMD命令で実行しておくことで、開発者は意識せずにhttp://localhostにアクセスしての動作確認が可能となります。
最初はNode.js標準のhttpモジュールを使用した簡易なビルトインサーバーを作ったりもしましたが、npmが使える以上ちゃんとしたパッケージを使用した方が良いなと思い直して方針転換しました。
services: app: build: context: ./ dockerfile: ./.docker/app/Dockerfile volumes: - .:/workspace:cached ports: - "80:80" environment: - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - AWS_DEFAULT_REGION=ap-northeast-1 - AWS_S3_BUCKET_PREFIX=${AWS_S3_BUCKET_PREFIX}
docker-compose.ymlの中では.envよりAWSのアクセスキーとシークレットキーの他、後述するServerless Frameworkの中で使用するS3のバケット名のプレフィックスをコンテナに渡す設定を記述しています。
また、コンテナ内で動かすhttp-serverにホストPCから接続するためのポートフォワーディングの設定も行います。
ディレクトリ構成
添付の画像のようになりました。
publicディレクトリが静的サイトを含むディレクトリになります。
.serverlessディレクトリはデプロイ時に生成されるディレクトリです。
.gitignoreは以下の通り
.env .serverless node_modules
MakefileはServerless Frameworkやコンテナ操作についてコマンドをまとめてるだけなので省略。
デプロイに必要となるIAMポリシー
.envで設定中のIAMユーザーに以下のポリシーを持たせる必要があります
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "s3:*", "cloudformation:DescribeStackResource", "cloudformation:CreateChangeSet", "s3-object-lambda:*", "cloudformation:DeleteChangeSet", "cloudformation:DescribeStacks", "cloudfront:*", "cloudformation:DescribeStackEvents", "cloudformation:GetTemplate", "cloudformation:DeleteStack", "cloudformation:DescribeChangeSet", "cloudformation:ExecuteChangeSet", "cloudformation:ValidateTemplate", "cloudformation:ListStackResources" ], "Resource": "*" } ] }
s3、s3-object-lambda、cloudfrontについてすべてのアクションを有効としている点は改善の余地がある状態。
ただ、デプロイに失敗したときにエラーログから不足しているアクションが分からないことがある&ロールバックが正しく行えず、再デプロイするのに手動でスタックに含まれるリソースを削除する必要が出てしまうという状況に陥ってしまうことがあったため調査を断念しているという経緯があります。。
package.json
{ "private": true, "devDependencies": { "serverless": "^3.31.0", "serverless-cloudfront-invalidate": "^1.12.2", "serverless-s3-sync": "^3.1.0" } }
Serverless Framework本体の他に、プラグインを2つ使用します(次項で解説)。
Serverless Frameworkについて
serverless.ymlの内容は記事の先頭に書いた参考ページ2つの情報を元に以下のようになりました。
service: s3-static-site frameworkVersion: "3" plugins: - serverless-s3-sync - serverless-cloudfront-invalidate provider: name: aws region: ${env:AWS_DEFAULT_REGION} custom: bucketName: ${env:AWS_S3_BUCKET_PREFIX}-${aws:accountId} s3Sync: - bucketName: ${self:custom.bucketName} localDir: public/ cloudfrontInvalidate: - distributionIdKey: DistributionId items: - "/*" resources: Resources: Bucket: Type: AWS::S3::Bucket Properties: BucketName: ${self:custom.bucketName} AccessControl: Private WebsiteConfiguration: IndexDocument: index.html BucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref Bucket PolicyDocument: Version: "2012-10-17" Statement: Sid: AllowCloudFrontServicePrincipalReadOnly Effect: Allow Principal: Service: cloudfront.amazonaws.com Action: s3:GetObject Resource: Fn::Sub: arn:aws:s3:::${Bucket}/* Condition: StringEquals: AWS:SourceArn: Fn::Sub: arn:aws:cloudfront::${AWS::AccountId}:distribution/${Distribution} OriginAccessControl: Type: AWS::CloudFront::OriginAccessControl Properties: OriginAccessControlConfig: Name: Fn::Sub: ${self:custom.bucketName}-oac OriginAccessControlOriginType: s3 SigningBehavior: always SigningProtocol: sigv4 Distribution: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: Enabled: true HttpVersion: http2and3 Origins: - Id: ${self:custom.bucketName} DomainName: Fn::Join: - '.' - - Ref: Bucket - s3 - Ref: AWS::Region - Ref: AWS::URLSuffix OriginAccessControlId: Ref: OriginAccessControl S3OriginConfig: OriginAccessIdentity: '' DefaultCacheBehavior: TargetOriginId: ${self:custom.bucketName} ViewerProtocolPolicy: https-only ForwardedValues: QueryString: false DefaultRootObject: index.html Outputs: URL: Value: Fn::Sub: https://${Distribution.DomainName} DistributionId: Value: !Ref Distribution
いつもの内容と違う点としてはLambda関数が不要のためfunctions:が存在しません。今回のServerless Frameworkはresources:の内容によってS3+CloudFrontのスタックを作成→S3にコンテンツをアップロードするアップローダとしての役割を担っています。
デプロイ時に作成するリソースは以下の4つ
- S3のバケット
- S3のバケットポリシー
- CloudFrontのディストリビューション
- CloudFrontのオリジンアクセスコントロール
オリジンアクセスコントロールを利用することで非公開なS3バケットからバケットポリシーの設定を介してコンテンツにアクセスできるようにするという仕組み。
Distribution → Bucket → BucketPolicy → OriginAccessControl という順番でリソースが構築されます。
以下、上記serverless.ymlの抜粋した解説
plugins: - serverless-s3-sync custom: s3Sync: - bucketName: ${self:custom.bucketName} localDir: public/
こちらの設定によってpublicフォルダの中のコンテンツをS3に同期しています。
serverless-s3-syncプラグインのその他のオプションについてはServerless Framework公式のプラグインのページかGitHubリポジトリで確認すると良いと思います。
Serverless Framework: Plugins
k1LoW/serverless-s3-sync: A plugin to sync local directories and S3 prefixes for Serverless Framework
plugins: - serverless-cloudfront-invalidate custom: cloudfrontInvalidate: - distributionIdKey: DistributionId items: - "/*" resources: Resources: Distribution: Type: AWS::CloudFront::Distribution Outputs: DistributionId: Value: !Ref Distribution
こちらの設定によってS3の同期の後にCloudFrontのキャッシュ削除を生成しています。
CloudFrontによってHTMLやCSSがキャッシュされることで更新後の内容が反映されなかったり、削除したページがキャッシュとして残り続けてしまうことを防ぐことができます。
resources: Resources: Distribution: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: HttpVersion: http2and3
その他、CloudFrontディストリビューションについてデフォルトで有効となるHTTP/1.0、1.1プロトコルの他にHTTP/2とHTTP/3に対応させたい場合はhttp2and3という設定を行う必要があるみたいです。
詳しくは以下のDistributionConfigに関するドキュメント参照
AWS::CloudFront::Distribution DistributionConfig – AWS CloudFormation
デプロイ
Serverlessデプロイ $ docker-compose exec app npx serverless deploy --verbose Serverlessで作成したスタックを含む削除 $ docker-compose exec app npx serverless remove --verbose Serverlessの設定確認 $ docker-compose exec app npx serverless print
初回のスタック構築も行うデプロイは7~8分程度掛かりました。
S3バケット名についてcustom:の中でAWSアカウントIDをサフィックスとして付与しようとした際、${AWS::AccountId}の疑似パラメータ参照が解決できずに文字列としてデプロイされてしまいスタックの作成に失敗するといったことがありました(custom:の中ではServerless Frameworkで提供されている${aws:accountId}という疑似パラメータの参照を行う必要がある)。
擬似パラメータ参照 – AWS CloudFormation
Serverless Framework Variables
事前にserverless printしておけばおかしな設定となっていることに気づけた問題でした。。
まとめ、運用する場合の備考
S3+CloudFrontの構成の静的サイトについてServerless Frameworkを使用してローカルからデプロイすることができました。
なお、実際に運用する場合はCloudFrontディストリビューションに対しての標準ログ記録の有効化や、AWS WAFを使用した保護が必要かと思います。
サーバーレスによってWebサーバーを稼働させるインスタンスの料金が節約できてもAWS WAFの使用料がかかることになるので、元々t4g.nanoとかのインスタンスで十分な運用が行えている静的サイトの場合は積極的に移行する必要は無いかなとも思います。
以下は標準ログ記録の有効化をCloudFormationのテンプレートに含める場合の追記になります。
resources: Resources: + # 標準ログ記録用のバケットを作成 + AccessLogBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: ${self:custom.bucketName}-accesslog + AccessControl: LogDeliveryWrite + OwnershipControls: + Rules: + - ObjectOwnership: BucketOwnerPreferred + LifecycleConfiguration: + Rules: + - Status: Enabled + ExpirationInDays: 3 Distribution: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: + # 標準ログ記録を有効化 + Logging: + Bucket: !GetAtt AccessLogBucket.DomainName
ポイントとしてCloudFrontの標準ログ記録で使用するS3バケットはCloudFrontからの書き込みについてACLを有効化した状態で許可する必要があるみたいです。
最初、ログを保管するバケットについて静的サイト用のものと同じ要領でACL無効化&バケットポリシーによる許可を行おうとしたところデプロイ時に「Invalid request provided: The S3 bucket that you specified for CloudFront logs does not enable ACL access…」というエラーが出てしまいました。
この件については以下の記事が参考になりました。
S3 のオブジェクト ACL を有効化にするケースまとめ | DevelopersIO
その他、日本国内の人に向けたページであればCloudFrontの配信について地理的制限を掛けることでEDoSのリスクを多少下げることが出来るかなと思います(実際のEDoSの手法を理解してないので間違ってるかも)。
そういった点について、今回紹介したserverless.ymlにはまだ改善できるところがありそうな状態だと思います。
参考リンク
記事の中に貼ったリンクなどについても再度ここに記載
- CloudFront + S3 で静的ページをホストするための Serverless Framework 設定
- 今回のS3+CloudFrontの導入の参考ページ1
- Serverless Framework + S3 + CloudFront – Qiita
- 今回のS3+CloudFrontの導入の参考ページ2
- 要注意!?本当に怖いCloudFront – Qiita
- CloudFrontを使うデメリットとしてのEDoSについての解説をされているページ
- S3 のオブジェクト ACL を有効化にするケースまとめ | DevelopersIO
- 擬似パラメータ参照 – AWS CloudFormation
- CloudFormationにおける疑似パラメータ参照の書き方について解説をされているページ
- Serverless Framework Variables
- Serverless Frameworkにおける疑似パラメータ参照の書き方について解説をされているページ
- Amazon Athena で CloudFront のログを集計する – ForgeVision Engineer Blog
- CloudFrontのログを解析する方法についての参考ページ
- 無料ホームページ作成用素材 フリー素材屋Hoshino
- 静的サイトのサンプルについてこちらのサイトのフリーテンプレートを使用させていただきました