@silvermine/serverless-plugin-cloudfront-lambda-edge
Version:
Plugin for the SLS 1.x branch to provide support for Lambda@Edge (not currently supported by CloudFormation
218 lines (178 loc) • 8.08 kB
JavaScript
'use strict';
var _ = require('underscore'),
Class = require('class.extend'),
VALID_EVENT_TYPES = [ 'viewer-request', 'origin-request', 'viewer-response', 'origin-response' ];
module.exports = Class.extend({
init: function(serverless, opts) {
this._serverless = serverless;
this._provider = serverless ? serverless.getProvider('aws') : null;
this._opts = opts;
this._custom = serverless.service ? serverless.service.custom : null;
if (!this._provider) {
throw new Error('This plugin must be used with AWS');
}
this._configureSchema(serverless.configSchemaHandler);
this.hooks = {
'aws:package:finalize:mergeCustomProviderResources': this._modifyTemplate.bind(this),
};
},
_configureSchema: function(handler) {
if (!handler || !_.isFunction(handler.defineCustomProperties) || !_.isFunction(handler.defineFunctionProperties)) {
return;
}
handler.defineCustomProperties({
type: 'object',
properties: {
'lambdaAtEdge': {
type: 'object',
properties: {
retain: { type: 'boolean' },
},
},
},
});
const functionPropertySchema = {
type: 'object',
properties: {
distribution: { type: 'string' },
eventType: { enum: VALID_EVENT_TYPES },
pathPattern: { type: 'string' },
},
required: [ 'distribution', 'eventType' ],
};
handler.defineFunctionProperties('aws', {
properties: {
'lambdaAtEdge': {
oneOf: [
{
type: 'array',
items: functionPropertySchema,
},
functionPropertySchema,
],
},
},
});
},
_modifyTemplate: function() {
var template = this._serverless.service.provider.compiledCloudFormationTemplate;
this._modifyExecutionRole(template);
this._modifyLambdaFunctionsAndDistributions(this._serverless.service.functions, template);
},
_modifyExecutionRole: function(template) {
var assumeRoleUpdated = false;
if (!template.Resources || !template.Resources.IamRoleLambdaExecution) {
this._serverless.cli.log('WARNING: no IAM role for Lambda execution found - can not modify assume role policy');
return;
}
_.each(template.Resources.IamRoleLambdaExecution.Properties.AssumeRolePolicyDocument.Statement, function(stmt) {
var svc = stmt.Principal.Service;
if (stmt.Principal && svc && _.contains(svc, 'lambda.amazonaws.com') && !_.contains(svc, 'edgelambda.amazonaws.com')) {
svc.push('edgelambda.amazonaws.com');
assumeRoleUpdated = true;
this._serverless.cli.log('Updated Lambda assume role policy to allow Lambda@Edge to assume the role');
}
}.bind(this));
// Serverless creates a LogGroup by a specific name, and grants logs:CreateLogStream
// and logs:PutLogEvents permissions to the function. However, on a replicated
// function, AWS will name the log groups differently, so the Serverless-created
// permissions will not work. Thus, we must give the function permission to create
// log groups and streams, as well as put log events.
//
// Since we don't have control over the naming of the log group, we let this
// function have permission to create and use a log group by any name.
// See http://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-identity-based-access-control-cwl.html
template.Resources.IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.push({
Effect: 'Allow',
Action: [
'logs:CreateLogGroup',
'logs:CreateLogStream',
'logs:PutLogEvents',
'logs:DescribeLogStreams',
],
Resource: 'arn:aws:logs:*:*:*',
});
if (!assumeRoleUpdated) {
this._serverless.cli.log('WARNING: was unable to update the Lambda assume role policy to allow Lambda@Edge to assume the role');
}
},
_modifyLambdaFunctionsAndDistributions: function(functions, template) {
_.chain(functions)
.pick(_.property('lambdaAtEdge')) // `pick` is used like `filter`, but for objects
.each(function(fnDef, fnName) {
var lambdaAtEdge = fnDef.lambdaAtEdge;
if (_.isArray(lambdaAtEdge)) {
_.each(lambdaAtEdge, this._handleSingleFunctionAssociation.bind(this, template, fnDef, fnName));
} else {
this._handleSingleFunctionAssociation(template, fnDef, fnName, lambdaAtEdge);
}
}.bind(this));
},
_handleSingleFunctionAssociation: function(template, fnDef, fnName, lambdaAtEdge) {
var fnLogicalName = this._provider.naming.getLambdaLogicalId(fnName),
pathPattern = lambdaAtEdge.pathPattern,
outputName = this._provider.naming.getLambdaVersionOutputLogicalId(fnName),
distName = lambdaAtEdge.distribution,
fnObj = template.Resources[fnLogicalName],
fnProps = template.Resources[fnLogicalName].Properties,
evtType = lambdaAtEdge.eventType,
includeBody = lambdaAtEdge.includeBody || false,
output = template.Outputs[outputName],
dist = template.Resources[distName],
retainFunctions = this._custom && this._custom.lambdaAtEdge && (this._custom.lambdaAtEdge.retain === true),
distConfig, cacheBehavior, fnAssociations, versionLogicalID;
if (!_.contains(VALID_EVENT_TYPES, evtType)) {
throw new Error('"' + evtType + '" is not a valid event type, must be one of: ' + VALID_EVENT_TYPES.join(', '));
}
if (!dist) {
throw new Error('Could not find resource with logical name "' + distName + '"');
}
if (dist.Type !== 'AWS::CloudFront::Distribution') {
throw new Error('Resource with logical name "' + distName + '" is not type AWS::CloudFront::Distribution');
}
versionLogicalID = (output ? output.Value.Ref : null);
if (!versionLogicalID) {
throw new Error('Could not find output by name of "' + outputName + '" or value from it to use version ARN');
}
if (fnProps && fnProps.Environment && fnProps.Environment.Variables) {
this._serverless.cli.log(
'Removing ' +
_.size(fnProps.Environment.Variables) +
' environment variables from function "' +
fnLogicalName +
'" because Lambda@Edge does not support environment variables'
);
delete fnProps.Environment.Variables;
if (_.isEmpty(fnProps.Environment)) {
delete fnProps.Environment;
}
}
if (retainFunctions) {
fnObj.DeletionPolicy = 'Retain';
}
distConfig = dist.Properties.DistributionConfig;
if (pathPattern) {
cacheBehavior = _.findWhere(distConfig.CacheBehaviors, { PathPattern: pathPattern });
if (!cacheBehavior) {
throw new Error('Could not find cache behavior in "' + distName + '" with path pattern "' + pathPattern + '"');
}
} else {
cacheBehavior = distConfig.DefaultCacheBehavior;
}
fnAssociations = cacheBehavior.LambdaFunctionAssociations;
if (!_.isArray(fnAssociations)) {
fnAssociations = cacheBehavior.LambdaFunctionAssociations = [];
}
fnAssociations.push({
EventType: evtType,
IncludeBody: includeBody,
LambdaFunctionARN: { Ref: versionLogicalID },
});
this._serverless.cli.log(
'Added "' + evtType + '" Lambda@Edge association for version "' +
versionLogicalID + '" to distribution "' + distName + '"' +
(pathPattern ? ' (path pattern "' + pathPattern + '")' : '') +
(includeBody ? ' (IncludeBody)' : '')
);
},
});