serverless-blue-green-deployments
Version:
A Serverless plugin to implement blue/green deployment of Lambda functions
396 lines (360 loc) • 14.6 kB
JavaScript
const _ = require('lodash/fp')
const flattenObject = require('flat')
const CfGenerators = require('./lib/CfTemplateGenerators')
class ServerlessBlueGreenDeployments {
constructor (serverless, options) {
this.serverless = serverless
this.options = options
this.awsProvider = this.serverless.getProvider('aws')
this.naming = this.awsProvider.naming
this.service = this.serverless.service
this.hooks = {
'before:package:finalize': this.addBlueGreenDeploymentResources.bind(this)
}
}
get codeDeployAppName () {
const stackName = this.naming.getStackName()
const normalizedStackName = this.naming.normalizeNameToAlphaNumericOnly(stackName)
return `${normalizedStackName}DeploymentApplication`
}
get compiledTpl () {
return this.service.provider.compiledCloudFormationTemplate
}
get withDeploymentPreferencesFns () {
return this.serverless.service.getAllFunctions()
.filter(name => _.has('deploymentSettings', this.service.getFunction(name)))
}
get globalSettings () {
return _.pathOr({}, 'custom.deploymentSettings', this.service)
}
get currentStage () {
return this.awsProvider.getStage()
}
addBlueGreenDeploymentResources () {
if (this.shouldDeployDeployGradually()) {
const codeDeployApp = this.buildCodeDeployApp()
const codeDeployRole = this.buildCodeDeployRole()
const functionsResources = this.buildFunctionsResources()
Object.assign(
this.compiledTpl.Resources,
codeDeployApp,
codeDeployRole,
...functionsResources
)
}
}
shouldDeployDeployGradually () {
return this.withDeploymentPreferencesFns.length > 0 && this.currentStageEnabled()
}
currentStageEnabled () {
const enabledStages = _.getOr([], 'stages', this.globalSettings)
return _.isEmpty(enabledStages) || _.includes(this.currentStage, enabledStages)
}
buildFunctionsResources () {
return _.flatMap(
serverlessFunction => this.buildFunctionResources(serverlessFunction),
this.withDeploymentPreferencesFns
)
}
buildFunctionResources (serverlessFnName) {
const functionName = this.naming.getLambdaLogicalId(serverlessFnName)
const deploymentSettings = this.getDeploymentSettingsFor(serverlessFnName)
const deploymentGrTpl = this.buildFunctionDeploymentGroup({ deploymentSettings, functionName })
const deploymentGroup = this.getResourceLogicalName(deploymentGrTpl)
const aliasTpl = this.buildFunctionAlias({ deploymentSettings, functionName, deploymentGroup })
const functionAlias = this.getResourceLogicalName(aliasTpl)
const lambdaPermissions = this.buildPermissionsForAlias({ functionName, functionAlias })
const eventsWithAlias = this.buildEventsForAlias(
{ functionName, functionAlias, serverlessFnName }
)
Object.assign(
this.compiledTpl.Resources,
deploymentGrTpl,
aliasTpl,
...lambdaPermissions,
...eventsWithAlias
)
return [deploymentGrTpl, aliasTpl, ...lambdaPermissions, ...eventsWithAlias]
}
buildCodeDeployApp () {
const logicalName = this.codeDeployAppName
const template = CfGenerators.codeDeploy.buildApplication()
return { [logicalName]: template }
}
buildCodeDeployRole () {
if (this.globalSettings.codeDeployRole) return {}
const logicalName = 'CodeDeployServiceRole'
const template = CfGenerators.iam.buildCodeDeployRole(this.globalSettings.codeDeployRolePermissionsBoundary)
return { [logicalName]: template }
}
buildFunctionDeploymentGroup ({ deploymentSettings, functionName }) {
const logicalName = `${functionName}DeploymentGroup`
const params = {
codeDeployAppName: this.codeDeployAppName,
codeDeployRoleArn: deploymentSettings.codeDeployRole,
deploymentSettings
}
const template = CfGenerators.codeDeploy.buildFnDeploymentGroup(params)
return { [logicalName]: template }
}
buildFunctionAlias ({ deploymentSettings = {}, functionName, deploymentGroup }) {
const { alias } = deploymentSettings
const functionVersion = this.getVersionNameFor(functionName)
const logicalName = `${alias}Alias`
const beforeHook = this.getFunctionName(deploymentSettings.preTrafficHook)
const afterHook = this.getFunctionName(deploymentSettings.postTrafficHook)
const trafficShiftingSettings = {
codeDeployApp: this.codeDeployAppName,
deploymentGroup,
afterHook,
beforeHook
}
const template = CfGenerators.lambda.buildAlias({
alias,
functionName,
functionVersion,
trafficShiftingSettings
})
return { [logicalName]: template }
}
getFunctionName (slsFunctionName) {
return slsFunctionName ? this.naming.getLambdaLogicalId(slsFunctionName) : null
}
buildPermissionsForAlias ({ functionName, functionAlias }) {
const permissions = this.getLambdaPermissionsFor(functionName)
return _.entries(permissions).map(([logicalName, template]) => {
const templateWithAlias = CfGenerators.lambda
.replacePermissionFunctionWithAlias(template, functionAlias)
return { [logicalName]: templateWithAlias }
})
}
buildEventsForAlias ({ functionName, functionAlias, serverlessFnName }) {
const replaceAliasStrategy = {
'AWS::Lambda::EventSourceMapping': CfGenerators.lambda.replaceEventMappingFunctionWithAlias,
'AWS::ApiGateway::Method': CfGenerators.apiGateway.replaceMethodUriWithAlias,
'AWS::ApiGatewayV2::Integration': CfGenerators.apiGateway.replaceV2IntegrationUriWithAlias,
'AWS::ApiGatewayV2::Authorizer': CfGenerators.apiGateway.replaceV2AuthorizerUriWithAlias,
'AWS::SNS::Topic': CfGenerators.sns.replaceTopicSubscriptionFunctionWithAlias,
'AWS::SNS::Subscription': CfGenerators.sns.replaceSubscriptionFunctionWithAlias,
'AWS::S3::Bucket': CfGenerators.s3.replaceS3BucketFunctionWithAlias,
'AWS::Events::Rule': CfGenerators.cloudWatchEvents.replaceCloudWatchEventRuleTargetWithAlias,
'AWS::Logs::SubscriptionFilter': CfGenerators.cloudWatchLogs.replaceCloudWatchLogsDestinationArnWithAlias,
'AWS::IoT::TopicRule': CfGenerators.iot.replaceIotTopicRuleActionArnWithAlias,
'AWS::Cognito::UserPool': CfGenerators.cognitoUserPool.replaceCognitoUserPoolWithAlias
}
try {
const functionEvents = this.getEventsFor(functionName, serverlessFnName)
const functionEventsEntries = _.entries(functionEvents)
return functionEventsEntries.map(([logicalName, event]) => {
const evt = replaceAliasStrategy[event.Type](
event, functionAlias, functionName, this.service, serverlessFnName
)
return { [logicalName]: evt }
})
} catch (e) {
return []
}
}
getEventsFor (functionName, serverlessFnName) {
const apiGatewayMethods = this.getApiGatewayMethodsFor(functionName)
const apiGatewayV2Integrations = this.getApiGatewayV2IntegrationFor(functionName)
const apiGatewayV2Authorizers = this.getApiGatewayV2AuthorizersFor(functionName)
const eventSourceMappings = this.getEventSourceMappingsFor(functionName)
const snsTopics = this.getSnsTopicsFor(functionName)
const snsSubscriptions = this.getSnsSubscriptionsFor(functionName)
const s3Events = this.getS3EventsFor(functionName)
const cloudWatchEvents = this.getCloudWatchEventsFor(functionName)
const cloudWatchLogs = this.getCloudWatchLogsFor(functionName)
const iotTopicRules = this.getIotTopicRulesFor(functionName)
const cognitoUserPools = this.getCognitoUserPoolsFor(functionName, serverlessFnName)
return Object.assign(
{},
apiGatewayMethods,
apiGatewayV2Integrations,
apiGatewayV2Authorizers,
eventSourceMappings,
snsTopics,
s3Events,
cloudWatchEvents,
cloudWatchLogs,
snsSubscriptions,
iotTopicRules,
cognitoUserPools
)
}
getApiGatewayMethodsFor (functionName) {
const isApiGMethod = _.matchesProperty('Type', 'AWS::ApiGateway::Method')
const isMethodForFunction = _.pipe(
_.prop('Properties.Integration'),
flattenObject,
_.includes(functionName)
)
const getMethodsForFunction = _.pipe(
_.pickBy(isApiGMethod),
_.pickBy(isMethodForFunction)
)
return getMethodsForFunction(this.compiledTpl.Resources)
}
getApiGatewayV2IntegrationFor (functionName) {
const isApiGatewayV2Integration = _.matchesProperty('Type', 'AWS::ApiGatewayV2::Integration')
const isIntegrationForFunction = _.pipe(
_.prop('Properties.IntegrationUri'),
flattenObject,
_.includes(functionName)
)
const getIntegrationsForFunction = _.pipe(
_.pickBy(isApiGatewayV2Integration),
_.pickBy(isIntegrationForFunction)
)
return getIntegrationsForFunction(this.compiledTpl.Resources)
}
getApiGatewayV2AuthorizersFor (functionName) {
const isApiGatewayAuthorizer = _.matchesProperty('Type', 'AWS::ApiGatewayV2::Authorizer')
const isAuthorizerForFunction = _.pipe(
_.prop('Properties.AuthorizerUri'),
flattenObject,
_.includes(functionName)
)
const getAuthorizersForFunction = _.pipe(
_.pickBy(isApiGatewayAuthorizer),
_.pickBy(isAuthorizerForFunction)
)
try {
return getAuthorizersForFunction(this.compiledTpl.Resources)
} catch (e) {
return null
}
}
getEventSourceMappingsFor (functionName) {
const isEventSourceMapping = _.matchesProperty('Type', 'AWS::Lambda::EventSourceMapping')
const isMappingForFunction = _.pipe(
_.prop('Properties.FunctionName'),
flattenObject,
_.includes(functionName)
)
const getMappingsForFunction = _.pipe(
_.pickBy(isEventSourceMapping),
_.pickBy(isMappingForFunction)
)
return getMappingsForFunction(this.compiledTpl.Resources)
}
getSnsTopicsFor (functionName) {
const isSnsTopic = _.matchesProperty('Type', 'AWS::SNS::Topic')
const isSnsTopicForFunction = _.pipe(
_.prop('Properties.Subscription'),
_.map(_.prop('Endpoint.Fn::GetAtt')),
_.flatten,
_.includes(functionName)
)
const getSnsTopicsForFunction = _.pipe(
_.pickBy(isSnsTopic),
_.pickBy(isSnsTopicForFunction)
)
return getSnsTopicsForFunction(this.compiledTpl.Resources)
}
getSnsSubscriptionsFor (functionName) {
const isSnsSubscription = _.matchesProperty('Type', 'AWS::SNS::Subscription')
const isSubscriptionForFunction = _.matchesProperty('Properties.Endpoint.Fn::GetAtt[0]', functionName)
const getSnsSubscriptionsForFunction = _.pipe(
_.pickBy(isSnsSubscription),
_.pickBy(isSubscriptionForFunction)
)
return getSnsSubscriptionsForFunction(this.compiledTpl.Resources)
}
getCloudWatchEventsFor (functionName) {
const isCloudWatchEvent = _.matchesProperty('Type', 'AWS::Events::Rule')
const isCwEventForFunction = _.pipe(
_.prop('Properties.Targets'),
_.map(_.prop('Arn.Fn::GetAtt')),
_.flatten,
_.includes(functionName)
)
const getCloudWatchEventsForFunction = _.pipe(
_.pickBy(isCloudWatchEvent),
_.pickBy(isCwEventForFunction)
)
return getCloudWatchEventsForFunction(this.compiledTpl.Resources)
}
getCloudWatchLogsFor (functionName) {
const isLogSubscription = _.matchesProperty('Type', 'AWS::Logs::SubscriptionFilter')
const isLogSubscriptionForFn = _.pipe(
_.prop('Properties.DestinationArn.Fn::GetAtt'),
_.flatten,
_.includes(functionName)
)
const getLogSubscriptionsForFunction = _.pipe(
_.pickBy(isLogSubscription),
_.pickBy(isLogSubscriptionForFn)
)
return getLogSubscriptionsForFunction(this.compiledTpl.Resources)
}
getCognitoUserPoolsFor (functionName, serverlessFnName) {
const cognitoTrigger = CfGenerators.cognitoUserPool.getTypeOfCognitoTrigger(
functionName, this.service, serverlessFnName
)
const pathToFun = `Properties.LambdaConfig.${cognitoTrigger}.Fn::GetAtt`
const isCognitoUserPool = _.matchesProperty('Type', 'AWS::Cognito::UserPool')
const isCognitoUserPoolForFn = _.pipe(
_.prop(pathToFun),
_.flatten,
_.includes(functionName)
)
const getCognitoUserPoolsForFunction = _.pipe(
_.pickBy(isCognitoUserPool),
_.pickBy(isCognitoUserPoolForFn)
)
return getCognitoUserPoolsForFunction(this.compiledTpl.Resources)
}
getS3EventsFor (functionName) {
const isS3Event = _.matchesProperty('Type', 'AWS::S3::Bucket')
const isS3EventForFunction = _.pipe(
_.prop('Properties.NotificationConfiguration.LambdaConfigurations'),
_.map(_.prop('Function.Fn::GetAtt')),
_.flatten,
_.includes(functionName)
)
const getS3EventsForFunction = _.pipe(
_.pickBy(isS3Event),
_.pickBy(isS3EventForFunction)
)
return getS3EventsForFunction(this.compiledTpl.Resources)
}
getIotTopicRulesFor (functionName) {
const isIotTopicRule = _.matchesProperty('Type', 'AWS::IoT::TopicRule')
const isIotTopicRuleForFunction = _.matchesProperty(
'Properties.TopicRulePayload.Actions[0].Lambda.FunctionArn.Fn::GetAtt[0]',
functionName
)
const getIotTopicsForFunction = _.pipe(
_.pickBy(isIotTopicRule),
_.pickBy(isIotTopicRuleForFunction)
)
return getIotTopicsForFunction(this.compiledTpl.Resources)
}
getVersionNameFor (functionName) {
const isLambdaVersion = _.matchesProperty('Type', 'AWS::Lambda::Version')
const isVersionForFunction = _.matchesProperty('Properties.FunctionName.Ref', functionName)
const getVersionNameForFunction = _.pipe(
_.pickBy(isLambdaVersion),
_.findKey(isVersionForFunction)
)
return getVersionNameForFunction(this.compiledTpl.Resources)
}
getLambdaPermissionsFor (functionName) {
const isLambdaPermission = _.matchesProperty('Type', 'AWS::Lambda::Permission')
const isPermissionForFunction = _.matchesProperty('Properties.FunctionName.Fn::GetAtt[0]', functionName)
const getPermissionForFunction = _.pipe(
_.pickBy(isLambdaPermission),
_.pickBy(isPermissionForFunction)
)
return getPermissionForFunction(this.compiledTpl.Resources)
}
getResourceLogicalName (resource) {
return _.head(_.keys(resource))
}
getDeploymentSettingsFor (serverlessFunction) {
const fnDeploymentSetting = this.service.getFunction(serverlessFunction).deploymentSettings
return Object.assign({}, this.globalSettings, fnDeploymentSetting)
}
}
module.exports = ServerlessBlueGreenDeployments