目次 †
概要 †以前に 1つのAWS Lambda関数でSPAフレームワーク作成 や 1つのAWS Lambda関数でSPA(その2) という記事を書いたが、当記事では API Gateway ではなく CloudFront を利用して似たような事をやってみる。
/css ・・・ CSS格納用
/api ・・・ Lambdaで処理 イメージ †TODO:
この構成のメリット/デメリット †
ここまで書いて気づく。 料金試算 †TODO:
静的コンテンツの準備 †src/html/index.html <!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> Lambdaの作成 †src/index.js '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 '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 '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; CloudFormationテンプレート †参考) 注意) 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 について †AutoPublishAlias: live を指定すると Version という追加のプロパティが使用できるようになるらしい。 https://github.com/awslabs/serverless-application-model/tree/master/examples/2016-10-31/lambda_edge 例) 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 補足(Lambda@Edge) †Lambda@Edge では、Node.js Lambda 関数により返却するコンテンツをカスタマイズする事ができる。 Lambda 関数が実行出来るタイミングは以下の通り。
当記事では、上記の「オリジンリクエスト」イベントによってLambdaをトリガーし、動的コンテンツか静的コンテンツかを判定する。 |