- 追加された行はこの色です。
- 削除された行はこの色です。
#author("2019-09-01T12:50:46+00:00","","")
#mynavi(AWSメモ)
#setlinebreak(on);
* 目次 [#q9218622]
#contents
- 参考
-- CloudFront Lambda@Edge での AWS Lambda の使用
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-edge.html
-- Lambda@Edge を使用したエッジでのコンテンツのカスタマイズ
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-at-the-edge.html
-- Lambda@Edge を使用したエッジでのコンテンツのカスタマイズ » リクエストとレスポンスを使用する
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-generating-http-responses.html
- 参考(AWSドキュメント)
-- [[CloudFront Lambda@Edge での AWS Lambda の使用:https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-edge.html]]
-- [[Lambda@Edge を使用したエッジでのコンテンツのカスタマイズ:https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-at-the-edge.html]]
-- [[Lambda@Edge を使用したエッジでのコンテンツのカスタマイズ » リクエストとレスポンスを使用する:https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-generating-http-responses.html]]
-- [[リクエストトリガーでの HTTP レスポンスの生成:https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-generating-http-responses-in-requests.html]]
-- [[Lambda@Edge 用の IAM アクセス権限とロールの設定:https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-permissions.html]]
-- [[CloudFront ディストリビューションのオリジンとして S3 ウェブサイトのエンドポイントを使用しています。403 Access Denied (アクセス拒否) エラーが発生するのはなぜですか?:https://aws.amazon.com/jp/premiumsupport/knowledge-center/s3-website-cloudfront-error-403/]]
-- [[オリジンアクセスアイデンティティを使用して Amazon S3 コンテンツへのアクセスを制限する:https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html]]
-- https://github.com/awslabs/serverless-application-model/tree/master/examples/2016-10-31/lambda_edge
- 関連
-- [[AWSメモ]]
-- [[AWS CloudFrontメモ]]
-- [[1つのAWS Lambda関数でSPAフレームワーク作成]]
-- [[1つのAWS Lambda関数でSPA(その2)]]
-- [[API Gateway&Lambda で画像データを返却する]]
* 概要 [#mfdc32cf]
#html(<div style="padding-left: 10px;">)
以前に [[1つのAWS Lambda関数でSPAフレームワーク作成]] という記事を書いたが、当記事はこれを API Gateway を利用せず CloudFront を利用した場合の一例として記載する。
以前に [[1つのAWS Lambda関数でSPAフレームワーク作成]] や [[1つのAWS Lambda関数でSPA(その2)]] という記事を書いたが、当記事では API Gateway ではなく CloudFront を利用して似たような事をやってみる。
- 静的コンテンツ(HTML、CSS、JS等) はS3に配置する。
- 動的コンテンツは Lambda で提供する。
- 動的コンテンツへのリクエストの場合は、Lambda@Edge を使用してレスポンスを返却する
- 作成する Lambda は1つのみとし、リクエスト情報から処理を判定する。(Lambda内部でルーティング)
- フロントエンドには Vue.js を利用する
- S3バケット内のフォルダ構成
#html(<div style="border:1px solid #333; padding:10px; display: inline-block; margin-left: 20px;">)
#html(<div style="display:inline-block;">)
/css
/html
/js
#html(</div>)
#html(<div style="display:inline-block; padding: 0 30px; vertical-align: top;">)
・・・ CSS格納用
・・・ HTML格納用
・・・ JS格納用
#html(</div>)
#html(</div>)
-URI設計
#html(<div style="border:1px solid #333; padding:10px; display: inline-block; margin-left: 20px;">)
#html(<div style="display:inline-block;">)
/api
/css
/html
/js
#html(</div>)
#html(<div style="display:inline-block; padding: 0 30px; vertical-align: top;">)
・・・ Lambdaで処理
・・・ オリジン(S3)にリクエストさせる
・・・ 〃
・・・ 〃
#html(</div>)
#html(</div>)
** イメージ [#w2df3fd7]
#html(<div style="padding-left: 10px;">)
#TODO
#html(</div>)
// イメージ END
** この構成のメリット/デメリット [#s7004326]
#html(<div style="padding-left: 10px;">)
#TODO
- メリット
-- 静的コンテンツは CloudFront のキャッシュにより高速配信が期待できる
-- 1つのアプリケーションとしてデプロイする事ができる
-- 静的コンテンツは CloudFront のキャッシュにより高速配信が期待できる
- デメリット
-- CloudFrontのデプロイに時間がかかる(10〜15分)
※恐らく更新内容をエッジロケーションに反映させるのに時間がかかっている。
※Lambdaも各リージョンにレプリカを作るらしい。
-- グローバルに配信するコンテンツでない場合、高速配信のメリットは殆どない。
-- 1 Lambdaの為、個別にメモリ調整等ができない
ここまで書いて気づく。
&color(red){''グローバル配信しない場合、高速配信のメリットはあまり無いうえにデプロイに時間がかかり過ぎる(10分〜15分程かかる)''};
&color(red){''今の所、グローバル配信の予定もないので、当記事の記載はいったん中断する。(覚書として調査/確認した所を記載する程度に留める)''};
#html(</div>)
// メリット/デメリット END
#html(</div>)
// 概要 END
* 料金試算 [#p28e9660]
#html(<div style="padding-left: 10px;">)
#TODO
#html(</div>)
// 料金試算 END
* 静的コンテンツの準備 [#z3a8db84]
#html(<div style="padding-left: 10px;">)
#TODO
src/html/index.html
#myhtml2(){{
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Sample</title>
</head>
<body>
<div id="app">
<button v-on:click="callApi('sample1')">Sample API1</button>
<div>
<p>Message : {{ message }}</p>
<p>Datetime: {{ datetime }}</p>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
message: "init message",
datetime: ""
},
methods: {
callApi: function(apiName){
var self = this;
var uri = "/api/" + apiName;
axios.get(uri)
.then(function (res) {
self.message = res.data.message;
if (res.data.datetime) {
self.datetime = res.data.datetime;
}
});
}
},
});
</script>
</body>
</html>
}}
#html(</div>)
// 静的コンテンツの準備 END
* Lambdaの作成 [#jfe1e94e]
#html(<div style="padding-left: 10px;">)
#TODO
src/index.js
#mycode2(){{
'use strict';
var myapp = require("myapp");
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
console.log("event:\n" + JSON.stringify(event, null, 2));
// /api 配下へのリクエストの場合
if (request["uri"] && request["uri"].match("/api/.+")) {
// ルーティング( /api/xxxx.js を実行 )
// TODO: ヘッダの検証による認証/認可など
// TODO: コンテキストに依存しないIFにしておく方が良い
let path = request["uri"].replace(/^\//,"") + ".js"; // TODO: ../ への対応など
if (myapp.fileExists(path)){
var module = require(path);
module.handler(event, context, function(error, response){
callback(error, response);
});
return;
} else if (コンテンツアップロード用のURIの場合) { // 例) /api/76ee3de97a1b8b903319b7c013d8c877
// TODO: src/html、src/css、src/js を対象のS3バケットにアップロード
}
// 対象の処理がない場合
let d = new Date();
let result = {
message: "target API not found!",
datetime: d.toLocaleDateString() + " " + d.toLocaleTimeString()
}
const response = {
status: 200,
statusDescription: "",
headers: {
"content-type": [
{key: "Content-Type", value: "application/json"}
]
},
body: JSON.stringify(result),
bodyEncoding: "text"
};
callback(null, response);
} else {
// API以外へのリクエストはそのままオリジンに流す
callback(null, request);
}
};
}}
src/api/sample1.js
#mycode2(){{
'use strict';
/**
* サンプルAPI1
*/
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
console.log("event:\n" + JSON.stringify(event, null, 2));
let d = new Date();
let result = {
message: "Sample1 API!!",
datetime: d.toLocaleDateString() + " " + d.toLocaleTimeString()
}
const response = {
status: 200,
headers: {
"content-type": [
{key: "Content-Type", value: "application/json"}
],
"cache-control": [
{key: "Cache-Control", value: "max-age=0"}
]
},
body: JSON.stringify(result),
bodyEncoding: "text"
};
callback(null, response);
};
}}
src/node_modules/myapp.js
#mycode2(){{
'use strict';
var fs = require('fs');
/**
* 共通関数などの定義.
*/
const myapp = {
"fileExists" : function(path){
try {
return fs.statSync(path);
} catch (e) {
console.error("not found : " + path);
return false;
}
}
}
module.exports = myapp;
}}
#html(</div>)
// Lambdaの作成 END
* CloudFormationテンプレート [#abbf7329]
#html(<div style="padding-left: 10px;">)
#TODO
参考)
https://github.com/awslabs/serverless-application-model/tree/master/examples/2016-10-31/lambda_edge
注意)
edgelambda.amazonaws.com を assumable role しないと、
「The function execution role must be assumable with edgelambda.amazonaws.com as well as lambda.amazonaws.com principals.」と怒られる。
#mycode2(){{
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Resources:
# アクセスログ用バケット
CloudFrontSpaLogBucket:
Type: AWS::S3::Bucket
#DeletionPolicy: Retain
Properties:
BucketName: !Sub 'cloudfront-spa-lambda-log-${AWS::Region}-${AWS::AccountId}'
# 静的コンテンツ用バケット
CloudFrontSpaBucket:
Type: AWS::S3::Bucket
#DeletionPolicy: Retain
Properties:
BucketName: !Sub 'cloudfront-spa-lambda-${AWS::Region}-${AWS::AccountId}'
AccessControl: PublicRead
# 静的コンテンツ用バケットポリシー
# (OAIを使用してCloudFront経由のアクセスのみ許可)
CloudFrontSpaBucketPolicy:
Type: AWS::S3::BucketPolicy
DependsOn:
- CloudFrontSpaBucket
- CloudFrontSpaOai
Properties:
Bucket: !Ref CloudFrontSpaBucket
PolicyDocument:
Version: 2008-10-17
Statement:
- Action:
- s3:GetObject
Effect: Allow
Resource: !Sub "${CloudFrontSpaBucket.Arn}/*"
Principal:
CanonicalUser: !GetAtt CloudFrontSpaOai.S3CanonicalUserId
# オリジンリクエストトリガーから起動されるLambda関数
CloudFrontSpaFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: CloudFrontSpaFunction
CodeUri: ./src
Handler: index.handler
Role: !GetAtt CloudFrontSpaFunctionRole.Arn
#Runtime: python3.6 # Lambda@Edgeでは python はサポート対象外
Runtime: nodejs8.10
AutoPublishAlias: live # 追加プロパティである Version を使用可能にする
# Lambda関数用のロール
CloudFrontSpaFunctionRole:
Type: AWS::IAM::Role
Properties:
RoleName: CloudFrontSpaFunctionRole
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- edgelambda.amazonaws.com
- lambda.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: "CloudFrontSpaFunctionRolePolicy"
PolicyDocument:
# 許可する操作/リソースなど
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
#- logs:CreateLogGroup
#- logs:CreateLogStream
#- logs:PutLogEvents
- s3:*
Resource: "*"
# CloudFrontディストリビューション
SpaCloudfrontDistribution:
Type: AWS::CloudFront::Distribution
DependsOn:
- CloudFrontSpaBucket
- CloudFrontSpaOai
Properties:
DistributionConfig:
Enabled: true
# デフォルトのキャッシュ動作
DefaultCacheBehavior:
# 許可するメソッド
AllowedMethods:
- GET
- HEAD
CachedMethods:
- GET
- HEAD
# ターゲットのオリジンのID ( Origins 配下に定義したオリジンと紐付ける )
TargetOriginId: CloudFrontSpaBucket-Origin
# オリジンに転送する情報の設定
ForwardedValues:
# オリジンに転送するCookie
Cookies:
# Cookie転送するならLambda側で値だけ取得して、S3に転送する際には値を空に書き換える等?(キャッシュ効率を考慮)
Forward: all
#WhitelistedNames:
# - String
# オリジンに転送するヘッダー ( キャッシュに影響するので定義しない方が良さそう )
Headers:
#- "User-Agent" # キャッシュ効率が悪くなる為、User-Agent は定義しない方が良さそう
- "CloudFront-Is-Desktop-Viewer" # 代わりに CloudFront-Is-XXXX-Viewer を使用する事が可能
- "CloudFront-Is-Mobile-Viewer"
- "CloudFront-Is-SmartTV-Viewer"
- "CloudFront-Is-Tablet-Viewer"
# オリジンにクエリ文字列を転送するか
QueryString: true
# キャッシュに使用するクエリ文字列
# (転送はするがキャッシュキーにしたくない場合はダミーを1つ定義しておく)
QueryStringCacheKeys:
- "dummy"
# オリジンリクエストトリガーを使用してLambdaを起動する設定
LambdaFunctionAssociations:
- EventType: origin-request
#LambdaFunctionARN: !Sub "${CloudFrontSpaFunction.Arn}:バージョン"
LambdaFunctionARN: !Ref CloudFrontSpaFunction.Version
# HTTPSのみ許可
ViewerProtocolPolicy:
https-only
#MaxTTL:
#MinTTL:
#DefaultTTL:
# IPv6アドレス使用するか
IPV6Enabled: true
# ビューワーで使用する最大のHTTPバージョン
HttpVersion: http2
# ルートURL指定時に公開するオブジェクト名
DefaultRootObject: index.html
ViewerCertificate:
CloudFrontDefaultCertificate: true # CloudFrontドメイン名を使用する場合
#AcmCertificateArn: String # ACMで作成した証明書を使用する場合
# オリジンに関する情報
Origins:
- DomainName: !GetAtt CloudFrontSpaBucket.DomainName
Id: CloudFrontSpaBucket-Origin
S3OriginConfig:
OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${CloudFrontSpaOai}
#OriginPath: String
#CustomOriginConfig:
# OriginKeepaliveTimeout: integer-value
# OriginReadTimeout: integer-value
# カスタムエラーページ
CustomErrorResponses:
- ErrorCachingMinTTL: 5
ErrorCode: 403
ResponseCode: 403
ResponsePagePath: /html/error.html
- ErrorCachingMinTTL: 5
ErrorCode: 404
ResponseCode: 404
ResponsePagePath: /html/error.html
# ログの出力先
Logging:
Bucket: !GetAtt CloudFrontSpaLogBucket.DomainName
IncludeCookies: true
# S3保護用のオリジンアクセスアイデンティティ(OAI)
CloudFrontSpaOai:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: !Ref AWS::StackName
Outputs:
SpaCloudfrontDistributionId:
Value: !Ref SpaCloudfrontDistribution
Export:
Name: SpaCloudfrontDistributionId
SpaCloudfrontDistributionDomainName:
Value: !GetAtt SpaCloudfrontDistribution.DomainName
Export:
Name: SpaCloudfrontDistributionDomainName
CloudFrontSpaFunctionVersion:
Value: !Ref CloudFrontSpaFunction.Version
Export:
Name: CloudFrontSpaFunctionVersion
}}
** AutoPublishAlias: live について [#ybae1540]
#html(<div style="padding-left:10px;">)
AutoPublishAlias: live を指定すると Version という追加のプロパティが使用できるようになるらしい。
AWS::CloudFront::Distribution 側の LambdaFunctionAssociations 配下の LambdaFunctionARN では、これを使用する事ができるようになる。
https://github.com/awslabs/serverless-application-model/tree/master/examples/2016-10-31/lambda_edge
例)
#mycode2(){{
LambdaEdgeFunctionSample:
Type: AWS::Serverless::Function
Properties:
:
CodeUri: ./src
Handler: index.handler
AutoPublishAlias: live
SampleDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
:
LambdaFunctionAssociations:
-
EventType: origin-request
LambdaFunctionARN: !Ref LambdaEdgeFunctionSample.Version
}}
#html(</div>)
#html(</div>)
// CloudFormationテンプレート END
* 補足(Lambda@Edge) [#r1e51760]
#html(<div style="padding-left: 10px;">)
Lambda@Edge では、Node.js Lambda 関数により返却するコンテンツをカスタマイズする事ができる。
Lambda 関数が実行出来るタイミングは以下の通り。
- CloudFront がビューワーからリクエストを受信したとき (ビューワーリクエスト)
- CloudFront がリクエストをオリジンに転送する前 (オリジンリクエスト)
- CloudFront がオリジンからレスポンスを受信したとき (オリジンレスポンス)
- CloudFront がビューワーにレスポンスを返す前 (ビューワーレスポンス)
当記事では、上記の「オリジンリクエスト」イベントによってLambdaをトリガーし、動的コンテンツか静的コンテンツかを判定する。
動的コンテンツへのリクエストだった場合はLambd 内でレスポンスデータを作成/返却する。
静的コンテンツへのリクエストだった場合は何もしない。(そのままオリジンに対してリクエストさせる)
#html(</div>)
// 説明 END