はじめに
最近ある案件で Serverless Framework (以下、SF)を触る機会があり、いろいろ試行錯誤したので、せっかくなのでブログにしてみました。
今回はAWSにおけるSPAとして割とメジャーな構成 (CloudFront + S3 + API Gateway) をSFでつくりつつ、その過程でちょっとハマったところについても最後に記載しています。
なお、今回はSFに焦点を当てており、Lambda関数や静的コンテンツ自体の実装については言及していません。
また、SFを含む各種ツールやプロダクトの紹介およびインストール方法などは割愛しています。
注意点など
IAM
今回、 IAM は administration 権限で sls コマンドなど実行していますので、
例えば本番環境で継続的にデプロイする場合など適切なポリシーを設定してください。
参考: https://serverless.com/framework/docs/providers/aws/guide/iam/
SFバージョン
Framework Core: 1.57.0
Plugin: 3.2.3
SDK: 2.2.1
Components Core: 1.1.2
Components CLI: 1.4.0
構成
ClodFrontでリクエストを受け取り、パスベースで S3 または API GW にリクエストを振り分けます。
ということで今回作成する構成は以下の通りです。
- S3 (SF デプロイ用バケット。us-east-1)
- S3 (SF デプロイ用バケット。ap-northeast-1)
- S3 (コンテンツ配信。CloudFront のオリジンの一つ。ap-northeast-1)
- API Gateway + Lambda (CloudFront のオリジンの一つ)
- CloudFront (Path ベースで API Gateway と S3 に振り分ける)
- Lambda@Edge (レスポンスにヘッダを付与する)
ここで、3つのS3バケットはあらかじめ作成しています。
デプロイ用のバケットは使い回しが可能なので、実質的に新規作成するのはコンテンツ配信用のバケットのみです。
また、コンテンツ配信用のS3は Static website hosting としてすでに外部公開の設定が完了しているものとします。
API Gateway + Lambda と S3 アップロード
API Gateway + Lambda の作成
まずは、API Gateway と Lambda を作成します。
service: sample-api-lambda
provider:
name: aws
runtime: nodejs12.x
stage: ${opt:stage, 'develop'}
region: ap-northeast-1
deploymentBucket:
name: "deploymentBucket-ap-northeast-1"
functions:
sample:
name: sample-lambda
runtime: nodejs12.x
handler: handler.hello
role: "arn:aws:iam::xxxxx:role/service-role/xxxxx"
events:
- http: GET api
resources:
Outputs:
ApiGwDomain:
Description: "API Gateway Domain"
Value:
Fn::Join:
- "."
- - Ref: ApiGatewayRestApi
- "execute-api"
- ${self:provider.region}
- "amazonaws.com"
Export:
Name: ApiGwDomain
ここで、handler.js
は サンプル をそのまま利用することにします。
また、 API Gateway のドメインを CloudFront 側で参照したいので Outputs として定義しています。
S3 アップロード
CloudFront で配信するために S3へのコンテンツアップロードも前述の serverless.yml に組み込んでおきます。
(ここは必須の設定ではないので、手動で実行しても大丈夫です。)
今回は serverless-s3-sync
plugin を利用します。以下を serverless.yml に追記します。
plugins:
- serverless-s3-sync
custom:
s3Sync:
- bucketName: sample-s3-orin
localDir: assets/
local の assets ディレクトリには index.html
を配置しておきます。
(内容は S3 bucket origin
としておきます。)
CloudFront + Lambda@Edge と Invalidation
CloudFront + Lambda@Edge の作成
次に CloudFront 側の設定です。
前述の API Gateway + Lambda の serverless.yml とは分割して作成します。
service: sample-cloudfront
provider:
name: aws
runtime: nodejs10.x
stage: ${opt:stage, 'develop'}
region: us-east-1
deploymentBucket:
name: "deploymentBucket-us-east-1"
functions:
sampleLambdaEdge:
handler: sample.handler
events:
- cloudFront:
eventType: viewer-response
origin:
DomainName: sample-s3-orin.s3-website-ap-northeast-1.amazonaws.com
CustomOriginConfig:
OriginProtocolPolicy: match-viewer
- cloudFront:
eventType: viewer-response
pathPattern: /api
origin:
DomainName: ${cf.ap-northeast-1:sample-api-lambda-${self:provider.stage}.ApiGwDomain}
OriginPath: /${self:provider.stage}
CustomOriginConfig:
OriginProtocolPolicy: match-viewer
ここでは、
- Region を
us-east-1
にしています。 - API Gateway の DomainName を
cf.REGION:stackName.outputKey
で動的に取得しています。 - sample.js は SF の サンプル を利用しています。
余談ですが、CloudFormation のシンタックス も利用可能なので、CloudFront を単体で作成する(Lambda@Edge を利用しない)ことも可能です。(が、この場合は SF 使わなくてもいいかもしれませんね。)
Invalidation の設定
CloudFront のデプロイ後、Invalidation が自動的に実行されるように Invalidation の設定も組み込んでおきます。
(ここは必須の設定ではないので、手動で実行しても大丈夫です。)
今回は serverless-cloudfront-invalidate
plugin を利用します。
以下を CloudFront の serverless.yml に追記します。
plugins:
- serverless-cloudfront-invalidate
custom:
cloudfrontInvalidate:
distributionIdKey: 'CDNDistributionId'
items:
- '/index.html'
デプロイ
それでは、上記で作成したものをデプロイしていきます。
ここでディレクトリ構成は以下のようにしています。
events/
├── api-gateway
│ ├── assets
│ │ └── index.html
│ ├── handler.js
│ └── serverless.yml
└── cloudfront
├── sample.js
└── serverless.yml
まずは、API Gateway+Lambda をデプロイします。
$ cd events/api-gateway/
$ sls deploy -v
ApiGwDomain: xxxxx.execute-api.ap-northeast-1.amazonaws.com
S3 Sync: Synced.
が出力されていれば成功です。次に、CloudFront をデプロイしていきます。
$ cd events/cloudfront/
$ sls deploy -v
CloudFrontDistributionDomainName: xxx.cloudfront.net
CloudfrontInvalidate: Invalidation started
が出力されていれば成功です。
動作確認
デプロイが無事完了したら、CloudFrontDistributionDomainName
で出力された CloudFront のURLにアクセスして確認します。
# S3 オリジン
$ curl https://xxx.cloudfront.net/
S3 bucket origin
# API GW オリジン
$ curl https://xxx.cloudfront.net/api
{"message":"Hello World!"}
よさそうですね。(もちろん、 Sampleでは x-serverless-time
ヘッダを Lambda@Edge で付与するようになっているので、これも確認ができました。)
ハマったところ
いくつかあるのですが、ここでは2つほど挙げておきます。
リージョンの指定
当初、CloudFront の serverless.yml を記述する際に、 region を ap-northeast-1
としていました。
これによって、デプロイ時に 2 つのエラーに遭遇しました。
- Could not locate deployment bucket. Error: Deployment bucket is not in the same region as the lambda function
- CloudFront associated functions have to be deployed to the us-east-1 region.
まー言われてみれば当然なのですが、デプロイ用の S3 バケットは同一リージョンに存在する必要があるのと、
Lambda@Edge は 2019年12月現在 us-east-1
でしか利用できないことが原因です。
従って、API Gateway + Lambda
と Cloudfront + Lambda@Edge
の serverless.yml を分割し、
各リージョンでデプロイ用バケット作成することで対応しました。
CloudFormation のリージョンをまたがるクロススタックの参照
CloudFormation はリージョンをまたがったクロススタック参照ができません。従って、SFも同様に ap-northeast-1
で作成したスタックは us-east-1
では参照できないとばかり思い込んでいました。今回だと API Gateway のドメインなのでそうそう変更になるようなこともないですが、例えば、検証目的で作ったり壊したりをやっていると、都度書き出すのはちょっと不便だな...とも思っていました。
が、ドキュメントを読み返してたら、ありましたありました。(ちゃんと読みましょう、自分。。。)
別リージョンの outputKey 参照方法(cf.REGION:stackName.outputKey
の部分)
これで API Gateway + Lambda 側のスタックが変更になっても、CloudFront 側で追従してくれるようになります。複数リージョンで作成する場合、とても便利ですね。
さいごに
ということで、Serverless Framework を使って、CloudFront, S3, API Gateway を作成してみました。
そこは間違っているとか、もっとこうした方がいいよなど、ご指摘やご意見お待ちしております。