In my previous post, I showed how to create a static website in AWS using CloudFormation to create a CloudFront distribution. In this post, I show how to add custom headers to the site using Lambda@Edge.
I expected this to be a very quick post: AWS provides a facility called Lambda@Edge that allows code (written in JavaScript/NodeJS) to be executed by CloudFront, and to modify what is returned by CloudFront. Unfortunately however, it seems that Lambda@Edge isn’t quite up to the usual standard of AWS: it has a number of severe limitations, and the documentation is very poor. This took me several hours of experimenting before I was able to put together the code shown below.
The main problems I found were as follows:
$LATEST
.In short, I can’t really recommend using Lambda@Edge right now - it all feel too bleeding edge.
Putting all that behind me however, here’s my solution.
Since Lambda@Edge only works with versions of lambdas, I needed a way of being able to create and “update” those versions. CloudFormation doesn’t really support updating or managing versions, so I had to fudge it a little.
In the end, I found a workaround: put conditions around the lambda version, and update the CloudFormation stack twice: once with the condition set to false, to delete the version, and again with the condition set to true, to recreate it.
So, my CloudFormation template now includes a condition and a parameter to control it:
Parameters:
RootDomainName:
Type: String
IncludeLambdaEdge:
Type: String
AllowedValues: ['true', 'false']
Conditions:
IncludeLambdaEdge:
!Equals ['true', !Ref IncludeLambdaEdge]
There are various bits of my template that set up debugging facilities for lambdas. I could probably get rid of them now I have it all debugged, but I’ll show those bits here for completeness:
InternalTracingBucket:
Type: 'AWS::S3::Bucket'
Properties:
BucketName: !Join
- '-'
- - !Ref RootDomainName
- 'tracing'
I modified my www
CloudFront distribution to turn on logging as well:
PublicWebsiteWwwCloudfront:
Type: AWS::CloudFront::Distribution
DependsOn:
- PublicWebsiteWwwBucket
- InternalTracingBucket
Properties:
DistributionConfig:
Logging:
Bucket: !GetAtt [InternalTracingBucket, DomainName]
IncludeCookies: false
Prefix: 'www'
(Existing properties snipped for clarity).
Next up is the actual lambda itself:
PoliciesEdgeLambda:
Type: 'AWS::Lambda::Function'
Condition: IncludeLambdaEdge
Properties:
Handler: 'index.handler'
Role:
Fn::GetAtt:
- 'PoliciesEdgeLambdaRole'
- 'Arn'
Code:
ZipFile: |
'use strict';
exports.handler = (event, context, callback) => {
const response = event.Records[0].cf.response;
var addHeader = (header, value) => {
response.headers[header.toLowerCase()] = [{
key: header,
value: value
}];
};
addHeader('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload');
addHeader('X-Content-Type-Options', 'nosniff');
addHeader('Content-Security-Policy', "default-src 'self'");
addHeader('X-Frame-Options', 'DENY');
addHeader('X-XSS-Protection', '1; mode=block')
callback(null, response);
};
Runtime: 'nodejs6.10'
Timeout: '25'
TracingConfig:
Mode: 'Active'
You can specify the code either inline or in a S3 bucket - my code is so small I figured inline was OK.
Again, I could remove the TracingConfig
now I have it all debugged.
The role for this lambda is as follows:
PoliciesEdgeLambdaRole:
Type: 'AWS::IAM::Role'
Condition: IncludeLambdaEdge
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Action: 'sts:AssumeRole'
Principal:
Service:
- lambda.amazonaws.com
- edgelambda.amazonaws.com
- replicator.lambda.amazonaws.com
Effect: Allow
Policies:
- PolicyName: EdgePoliciesLambdaPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Action:
- 'xray:PutTraceSegments'
- 'xray:PutTelemetryRecords'
- 'lambda:GetFunction'
- 'lambda:EnableReplication*'
- 'lambda:InvokeFunction'
- 'logs:CreateLogGroup'
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Effect: Allow
Resource: '*'
Lastly, the lambda version looks like this:
PoliciesEdgeLambdaVersion:
Type: 'AWS::Lambda::Version'
Condition: IncludeLambdaEdge
Properties:
FunctionName:
Ref: 'PoliciesEdgeLambda'
It will be destroyed and recreated in the two-pass stack update mechanism.
At this stage, the lambda is just sitting there: not associated with CloudFront. I can tie the two together as follows:
PublicWebsiteWwwCloudfront:
Type: AWS::CloudFront::Distribution
DependsOn:
- PublicWebsiteWwwBucket
- InternalTracingBucket
Properties:
DistributionConfig:
DefaultCacheBehavior:
LambdaFunctionAssociations:
- !If
- IncludeLambdaEdge
- EventType: 'origin-response'
LambdaFunctionARN: !Join
- ':'
- - !GetAtt [PoliciesEdgeLambda, Arn]
- !GetAtt [PoliciesEdgeLambdaVersion, Version]
- !Ref 'AWS::NoValue'
(Again, existing properties omitted for clarity).
If you prefer, I have put the whole lot in a Gist.
Published: Monday, February 05, 2018
Hackification.io is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to amazon.com. I may earn a small commission for my endorsement, recommendation, testimonial, and/or link to any products or services from this website.