Skip to content

Instantly share code, notes, and snippets.

@carlochess
Created April 16, 2021 15:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save carlochess/3ba0deda982ffa7979d448f245c524eb to your computer and use it in GitHub Desktop.
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
                                                 ┌──────────────────────────────┐
                                                 │                              │
                                  ┌──────┐       │        test/index.html       │
                                  │lambda│       │                              │
                                  │change├───────►                              │
 test.react.app  ┌─────────┐      │origin│       ┌──────────────────────────────┤
 ───────────────►│         ├─────►└──────┴┐      │                              │
                 │cloudfron│              │      │                              │
                 │         │              │      │        test1/index.html      │
test1.react.app  │         │              └─────►│                              │
 ───────────────►│         │                     │                              │
                 └─────────┘◄─────┬──────┐       ├──────────────────────────────┤
                                  │      │       │                              │
                                  │lambda│       │                              │
                                  │ resp │       │                              │
                                  └──────┘       │        testN/index.html      │
                                                 │                              │
                                                 │                              │
                                                 └──────────────────────────────┘
--- # 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