UNPKG

serverless-blue-green-deployments

Version:

A Serverless plugin to implement blue/green deployment of Lambda functions

396 lines (360 loc) 14.6 kB
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