┌──────────────────────────────┐
│ │
┌──────┐ │ test/index.html │
│lambda│ │ │
│change├───────► │
test.react.app ┌─────────┐ │origin│ ┌──────────────────────────────┤
───────────────►│ ├─────►└──────┴┐ │ │
│cloudfron│ │ │ │
│ │ │ │ test1/index.html │
test1.react.app │ │ └─────►│ │
───────────────►│ │ │ │
└─────────┘◄─────┬──────┐ ├──────────────────────────────┤
│ │ │ │
│lambda│ │ │
│ resp │ │ │
└──────┘ │ testN/index.html │
│ │
│ │
└──────────────────────────────┘
Created
April 16, 2021 15:41
-
-
Save carlochess/3ba0deda982ffa7979d448f245c524eb to your computer and use it in GitHub Desktop.
CF Cloudformation template that creates a Cloudfront with OAI to S3 for a frontend SPA
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- # problema con el manejo de las versiones de las lambdas :( | |
AWSTemplateFormatVersion: '2010-09-09' | |
Parameters: {} | |
Resources: | |
IamLambdaExecutionRoleFullAccessS3: | |
Type: AWS::IAM::Role | |
Properties: | |
RoleName: | |
Fn::Sub: | |
- ws-lambda-at-edge-full-access-s3-${UniqueId} | |
- UniqueId: | |
Fn::Select: | |
- 0 | |
- Fn::Split: | |
- "-" | |
- Fn::Select: | |
- 2 | |
- Fn::Split: | |
- "/" | |
- Ref: AWS::StackId | |
ManagedPolicyArns: | |
- arn:aws:iam::aws:policy/CloudWatchLogsFullAccess | |
- arn:aws:iam::aws:policy/AmazonS3FullAccess | |
Path: "/" | |
AssumeRolePolicyDocument: | |
Version: '2012-10-17' | |
Statement: | |
- Effect: Allow | |
Principal: | |
Service: | |
- lambda.amazonaws.com | |
- edgelambda.amazonaws.com | |
Action: | |
- sts:AssumeRole | |
IamLambdaExecutionRoleFullAccess: | |
Type: AWS::IAM::Role | |
Properties: | |
RoleName: | |
Fn::Sub: | |
- ws-lambda-at-edge-full-access-${UniqueId} | |
- UniqueId: | |
Fn::Select: | |
- 0 | |
- Fn::Split: | |
- "-" | |
- Fn::Select: | |
- 2 | |
- Fn::Split: | |
- "/" | |
- Ref: AWS::StackId | |
ManagedPolicyArns: | |
- arn:aws:iam::aws:policy/CloudWatchLogsFullAccess | |
Path: "/" | |
AssumeRolePolicyDocument: | |
Version: '2012-10-17' | |
Statement: | |
- Effect: Allow | |
Principal: | |
Service: | |
- lambda.amazonaws.com | |
- edgelambda.amazonaws.com | |
Action: | |
- sts:AssumeRole | |
AlienCardsS3Bucket: | |
Type: AWS::S3::Bucket | |
Properties: | |
BucketName: | |
Fn::Sub: | |
- col-alien-cards-${UniqueId} | |
- UniqueId: | |
Fn::Select: | |
- 0 | |
- Fn::Split: | |
- "-" | |
- Fn::Select: | |
- 2 | |
- Fn::Split: | |
- "/" | |
- Ref: AWS::StackId | |
AlienCardsS3BucketPolicy: | |
Type: AWS::S3::BucketPolicy | |
DependsOn: | |
- AlienCardsS3Bucket | |
Properties: | |
Bucket: | |
Ref: AlienCardsS3Bucket | |
PolicyDocument: | |
Id: PolicyForCloudFrontPrivateContent | |
Statement: | |
- Effect: Allow | |
Principal: | |
AWS: | |
Fn::Sub: arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity | |
${AlienCardsCloudFrontOAI} | |
Action: s3:GetObject | |
Resource: | |
Fn::Sub: | |
- "${AlienCardsS3BucketArn}/*" | |
- AlienCardsS3BucketArn: | |
Fn::GetAtt: | |
- AlienCardsS3Bucket | |
- Arn | |
AlienCardsCloudFrontOAI: | |
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity | |
Properties: | |
CloudFrontOriginAccessIdentityConfig: | |
Comment: | |
Fn::Sub: | |
- OAI For Lambda@Edge Workshop ${UniqueId} | |
- UniqueId: | |
Fn::Select: | |
- 0 | |
- Fn::Split: | |
- "-" | |
- Fn::Select: | |
- 2 | |
- Fn::Split: | |
- "/" | |
- Ref: AWS::StackId | |
AlienCardsCloudFrontDistribution: | |
Type: AWS::CloudFront::Distribution | |
DependsOn: | |
- AlienCardsS3Bucket | |
- AlienCardsCloudFrontOAI | |
Properties: | |
DistributionConfig: | |
Comment: | |
Fn::Sub: | |
- Lambda@Edge Workshop ${UniqueId} | |
- UniqueId: | |
Fn::Select: | |
- 0 | |
- Fn::Split: | |
- "-" | |
- Fn::Select: | |
- 2 | |
- Fn::Split: | |
- "/" | |
- Ref: AWS::StackId | |
Origins: | |
- Id: AlienCardsS3BucketOriginId | |
DomainName: | |
Fn::GetAtt: | |
- AlienCardsS3Bucket | |
- RegionalDomainName | |
S3OriginConfig: | |
OriginAccessIdentity: | |
Fn::Sub: origin-access-identity/cloudfront/${AlienCardsCloudFrontOAI} | |
DefaultRootObject: index.html | |
Aliases: | |
- !Sub 'test.myreact.app' | |
- !Sub 'test2.myreact.app' | |
- !Sub 'test3.myreact.app' | |
DefaultCacheBehavior: | |
TargetOriginId: AlienCardsS3BucketOriginId | |
ViewerProtocolPolicy: redirect-to-https | |
ForwardedValues: | |
Headers: ['host'] | |
Cookies: | |
Forward: whitelist | |
WhitelistedNames: ['project', 'environment', 'micro', 'branch'] | |
QueryString: false | |
QueryStringCacheKeys: [] | |
MinTTL: 0 | |
MaxTTL: 0 | |
DefaultTTL: 0 | |
LambdaFunctionAssociations: | |
- EventType: 'origin-request' | |
LambdaFunctionARN: !Join [":", [!GetAtt BootstrapFunction.Arn, !GetAtt Version.Version]] | |
- EventType: 'origin-response' | |
LambdaFunctionARN: !Join [":", [!GetAtt BootstrapFunction2.Arn, !GetAtt Version2.Version]] | |
ViewerCertificate: | |
AcmCertificateArn: !ImportValue CertificateGetConApp | |
SslSupportMethod: "sni-only" | |
HttpVersion: http2 | |
PriceClass: PriceClass_All | |
IPV6Enabled: true | |
Enabled: true | |
Version: | |
Type: AWS::Lambda::Version | |
Properties: | |
FunctionName: !Ref BootstrapFunction | |
BootstrapFunction: | |
Type: AWS::Lambda::Function | |
Properties: | |
Handler: index.handler | |
Runtime: nodejs12.x | |
Timeout: 30 | |
Role: | |
Fn::GetAtt: | |
- IamLambdaExecutionRoleFullAccess | |
- Arn | |
Code: | |
ZipFile: | | |
exports.handler = async event => { | |
console.log(JSON.stringify(event)) | |
const default_sub_domain = "test" | |
const request = event.Records[0].cf.request; | |
let project="", environment="", micro="", branch=""; | |
if(request.headers.cookie != null ) { | |
for(let i = 0 ; i < request.headers.cookie.length ; i++){ | |
let cookie = request.headers.cookie[i].value.split("=") | |
let cookie_key = cookie[0] | |
let cookie_value = cookie[1] | |
if (cookie_key === "project"){ | |
project = cookie_value | |
} else if (cookie_key === "environment"){ | |
environment = cookie_value | |
} else if (cookie_key === "micro"){ | |
micro = cookie_value | |
} else if (cookie_key === "branch"){ | |
branch = cookie_value | |
} | |
} | |
} | |
let folder_by_cookie = "" | |
if (project != ""){ | |
if (environment != ""){ | |
if (micro != ""){ | |
if (branch != ""){ | |
folder_by_cookie = [project, environment, micro, branch].join("/") | |
} | |
} | |
} | |
} | |
let folder_by_host = request.headers.host[0].value.split(".")[0] | |
let folder = "" | |
if (folder_by_host != default_sub_domain) | |
folder += "/" + folder_by_host | |
if (folder_by_cookie != "") | |
folder += "/" + folder_by_cookie | |
request.uri = folder + request.uri | |
delete request.headers.host | |
request.headers.host = [{ | |
"key": "Host", | |
"value": request.origin.s3.domainName | |
}] | |
return request; | |
}; | |
Version2: | |
Type: AWS::Lambda::Version | |
Properties: | |
FunctionName: !Ref BootstrapFunction2 | |
BootstrapFunction2: | |
Type: AWS::Lambda::Function | |
Properties: | |
Handler: index.handler | |
Runtime: nodejs12.x | |
Timeout: 30 | |
Role: | |
Fn::GetAtt: | |
- IamLambdaExecutionRoleFullAccessS3 | |
- Arn | |
Code: | |
ZipFile: | | |
'use strict'; | |
var AWS = require('aws-sdk'); | |
var s3 = new AWS.S3(); | |
exports.handler = async (event, context, callback) => { | |
const cf = event.Records[0].cf; | |
const request = cf.request; | |
const response = cf.response; | |
console.log(JSON.stringify(request)) | |
console.log(JSON.stringify(response)) | |
const statusCode = response.status; | |
// Only replace 403 and 404 requests typically received | |
// when loading a page for a SPA that uses client-side routing | |
const doReplace = request.method === 'GET' | |
&& (statusCode == '403' || statusCode == '404'); | |
const result = doReplace | |
? await generateResponseAndLog(cf, request, response) | |
: response; | |
callback(null, result); | |
}; | |
async function generateResponseAndLog(cf, request, response){ | |
const domain = request.origin.s3.domainName.split(".")[0]; | |
const appPath = getAppPath(request.uri); | |
const indexPage = 'index.html'; | |
const indexPath = `${appPath}/${indexPage}`; | |
const final_response = await generateResponse(domain, indexPath, response); | |
console.log('response: ' + JSON.stringify(final_response)); | |
return final_response; | |
} | |
async function generateResponse(bucket, path, response){ | |
try { | |
// Load HTML index from the CloudFront cache | |
console.log(`get s3://${bucket}/${path}`) | |
const s3Response = await s3.getObject({ Bucket: bucket, Key: path }).promise(); | |
//console.log("Raw text:\n" + s3Response.Body.toString('ascii')); | |
const headers = { | |
'content-type': [{ key: "Content-Type", value: s3Response.ContentType }], | |
'content-length': [{ key: "Content-Length", value: s3Response.ContentLength.toString() }] | |
}; | |
if ("transfer-encoding" in response.headers) | |
headers["transfer-encoding"] = response.headers["transfer-encoding"] | |
if ("via" in response.headers) | |
headers["via"] = response.headers["via"] | |
return { | |
status: '200', | |
headers: headers, | |
body: s3Response.Body.toString('utf8') | |
}; | |
} catch (error) { | |
console.log(JSON.stringify(error)) | |
if(error.code == "NoSuchKey"){ | |
response.status = 404; | |
response.statusDescription = 'OK'; | |
response.body = 'index html not found'; | |
return response | |
} | |
response.status = 500; | |
response.body = 'An error has ocurred'; | |
return response | |
} | |
} | |
function getAppPath(path){ | |
if(!path){ | |
return ''; | |
} | |
if(path[0] === '/'){ | |
path = path.slice(1); | |
} | |
const segments = path.split('/'); | |
return segments[0]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment