serverless-plugin-canary-deployments
Advanced tools
Comparing version 0.4.8 to 0.5.0
@@ -0,1 +1,7 @@ | ||
# 0.5.0 (09.02.2021) | ||
- Add support for API Gateway v2 #72 | ||
- Update CodeDeploy default policy to AWSCodeDeployRoleForLambdaLimited #98 | ||
- Add support for IAM permissions boundaries #99 | ||
- Patch in CodeDeploy permissions for hooks #110 | ||
# 0.4.8 (28.07.2019) | ||
@@ -2,0 +8,0 @@ - Add support for IoT rules |
@@ -119,2 +119,13 @@ { | ||
] | ||
}, | ||
{ | ||
"Action": [ | ||
"codedeploy:PutLifecycleEventHookExecutionStatus" | ||
], | ||
"Effect": "Allow", | ||
"Resource": [ | ||
{ | ||
"Fn::Sub": "arn:${AWS::Partition}:codedeploy:${AWS::Region}:${AWS::AccountId}:deploymentgroup:${CanarydeploymentstestdevDeploymentApplication}/${HelloLambdaFunctionDeploymentGroup}" | ||
} | ||
] | ||
} | ||
@@ -507,3 +518,3 @@ ] | ||
"ManagedPolicyArns": [ | ||
"arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda", | ||
"arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambdaLimited", | ||
"arn:aws:iam::aws:policy/AWSLambdaFullAccess" | ||
@@ -510,0 +521,0 @@ ], |
@@ -373,3 +373,3 @@ { | ||
"ManagedPolicyArns": [ | ||
"arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda", | ||
"arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambdaLimited", | ||
"arn:aws:iam::aws:policy/AWSLambdaFullAccess" | ||
@@ -376,0 +376,0 @@ ], |
@@ -119,2 +119,13 @@ { | ||
] | ||
}, | ||
{ | ||
"Action": [ | ||
"codedeploy:PutLifecycleEventHookExecutionStatus" | ||
], | ||
"Effect": "Allow", | ||
"Resource": [ | ||
{ | ||
"Fn::Sub": "arn:${AWS::Partition}:codedeploy:${AWS::Region}:${AWS::AccountId}:deploymentgroup:${CanarydeploymentstestdevDeploymentApplication}/${HelloLambdaFunctionDeploymentGroup}" | ||
} | ||
] | ||
} | ||
@@ -121,0 +132,0 @@ ] |
@@ -119,2 +119,13 @@ { | ||
] | ||
}, | ||
{ | ||
"Action": [ | ||
"codedeploy:PutLifecycleEventHookExecutionStatus" | ||
], | ||
"Effect": "Allow", | ||
"Resource": [ | ||
{ | ||
"Fn::Sub": "arn:${AWS::Partition}:codedeploy:${AWS::Region}:${AWS::AccountId}:deploymentgroup:${CanarydeploymentstestdevDeploymentApplication}/${HelloLambdaFunctionDeploymentGroup}" | ||
} | ||
] | ||
} | ||
@@ -507,4 +518,5 @@ ] | ||
"ManagedPolicyArns": [ | ||
"arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda", | ||
"arn:aws:iam::aws:policy/AWSLambdaFullAccess" | ||
"arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambdaLimited", | ||
"arn:aws:iam::aws:policy/AWSLambdaFullAccess", | ||
"arn:aws:iam::aws:policy/AmazonSNSFullAccess" | ||
], | ||
@@ -511,0 +523,0 @@ "AssumeRolePolicyDocument": { |
@@ -102,2 +102,13 @@ { | ||
] | ||
}, | ||
{ | ||
"Action": [ | ||
"codedeploy:PutLifecycleEventHookExecutionStatus" | ||
], | ||
"Effect": "Allow", | ||
"Resource": [ | ||
{ | ||
"Fn::Sub": "arn:${AWS::Partition}:codedeploy:${AWS::Region}:${AWS::AccountId}:deploymentgroup:${CanarydeploymentstestdevDeploymentApplication}/${HelloLambdaFunctionDeploymentGroup}" | ||
} | ||
] | ||
} | ||
@@ -306,3 +317,3 @@ ] | ||
"ManagedPolicyArns": [ | ||
"arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda", | ||
"arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambdaLimited", | ||
"arn:aws:iam::aws:policy/AWSLambdaFullAccess" | ||
@@ -309,0 +320,0 @@ ], |
@@ -104,2 +104,13 @@ { | ||
] | ||
}, | ||
{ | ||
"Action": [ | ||
"codedeploy:PutLifecycleEventHookExecutionStatus" | ||
], | ||
"Effect": "Allow", | ||
"Resource": [ | ||
{ | ||
"Fn::Sub": "arn:${AWS::Partition}:codedeploy:${AWS::Region}:${AWS::AccountId}:deploymentgroup:${CanarydeploymentstestdevDeploymentApplication}/${HelloLambdaFunctionDeploymentGroup}" | ||
} | ||
] | ||
} | ||
@@ -299,3 +310,3 @@ ] | ||
"ManagedPolicyArns": [ | ||
"arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda", | ||
"arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambdaLimited", | ||
"arn:aws:iam::aws:policy/AWSLambdaFullAccess" | ||
@@ -302,0 +313,0 @@ ], |
@@ -83,2 +83,13 @@ { | ||
"Resource": ["*"] | ||
}, | ||
{ | ||
"Action": [ | ||
"codedeploy:PutLifecycleEventHookExecutionStatus" | ||
], | ||
"Effect": "Allow", | ||
"Resource": [ | ||
{ | ||
"Fn::Sub": "arn:${AWS::Partition}:codedeploy:${AWS::Region}:${AWS::AccountId}:deploymentgroup:${CanarydeploymentstestdevDeploymentApplication}/${BelloLambdaFunctionDeploymentGroup}" | ||
} | ||
] | ||
} | ||
@@ -236,3 +247,3 @@ ] | ||
"ManagedPolicyArns": [ | ||
"arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda", | ||
"arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambdaLimited", | ||
"arn:aws:iam::aws:policy/AWSLambdaFullAccess" | ||
@@ -239,0 +250,0 @@ ], |
@@ -134,2 +134,13 @@ { | ||
] | ||
}, | ||
{ | ||
"Action": [ | ||
"codedeploy:PutLifecycleEventHookExecutionStatus" | ||
], | ||
"Effect": "Allow", | ||
"Resource": [ | ||
{ | ||
"Fn::Sub": "arn:${AWS::Partition}:codedeploy:${AWS::Region}:${AWS::AccountId}:deploymentgroup:${CanarydeploymentstestdevDeploymentApplication}/${HelloLambdaFunctionDeploymentGroup}" | ||
} | ||
] | ||
} | ||
@@ -710,4 +721,5 @@ ] | ||
"ManagedPolicyArns": [ | ||
"arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda", | ||
"arn:aws:iam::aws:policy/AWSLambdaFullAccess" | ||
"arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambdaLimited", | ||
"arn:aws:iam::aws:policy/AWSLambdaFullAccess", | ||
"arn:aws:iam::aws:policy/AmazonSNSFullAccess" | ||
], | ||
@@ -870,2 +882,2 @@ "AssumeRolePolicyDocument": { | ||
} | ||
} | ||
} |
@@ -14,2 +14,21 @@ const _ = require('lodash/fp') | ||
function buildV2UriForAlias (functionAlias) { | ||
const aliasArn = [ | ||
'arn:', | ||
{ Ref: 'AWS::Partition' }, | ||
':apigateway:', | ||
{ Ref: 'AWS::Region' }, | ||
':lambda:path/2015-03-31/functions/', | ||
{ Ref: functionAlias }, | ||
'/invocations' | ||
] | ||
return { 'Fn::Join': ['', aliasArn] } | ||
} | ||
function replaceV2IntegrationUriWithAlias (apiGatewayMethod, functionAlias) { | ||
const aliasUri = buildV2UriForAlias(functionAlias) | ||
const newMethod = _.set('Properties.IntegrationUri', aliasUri, apiGatewayMethod) | ||
return newMethod | ||
} | ||
function replaceMethodUriWithAlias (apiGatewayMethod, functionAlias) { | ||
@@ -21,6 +40,14 @@ const aliasUri = buildUriForAlias(functionAlias) | ||
function replaceV2AuthorizerUriWithAlias (apiGatewayMethod, functionAlias) { | ||
const aliasUri = buildV2UriForAlias(functionAlias) | ||
const newMethod = _.set('Properties.AuthorizerUri', aliasUri, apiGatewayMethod) | ||
return newMethod | ||
} | ||
const ApiGateway = { | ||
replaceMethodUriWithAlias | ||
replaceV2IntegrationUriWithAlias, | ||
replaceMethodUriWithAlias, | ||
replaceV2AuthorizerUriWithAlias | ||
} | ||
module.exports = ApiGateway |
@@ -32,2 +32,25 @@ const { expect } = require('chai') | ||
const apiGatewayV2Method = { | ||
Type: 'AWS::ApiGatewayV2::Integration', | ||
Properties: { | ||
IntegrationType: 'AWS_PROXY', | ||
ApiId: { Ref: 'ApiGatewayResourceId' }, | ||
IntegrationUri: { | ||
'Fn::Join': [ | ||
'', | ||
[ | ||
'arn:', | ||
{ Ref: 'AWS::Partition' }, | ||
':apigateway:', | ||
{ Ref: 'AWS::Region' }, | ||
':lambda:path/2015-03-31/functions/', | ||
{ 'Fn:GetAtt': ['HelloLambdaFunction', 'Arn'] }, | ||
'/invocations' | ||
] | ||
] | ||
}, | ||
MethodResponses: [] | ||
} | ||
} | ||
describe('.replaceMethodUriWithAlias', () => { | ||
@@ -49,2 +72,21 @@ it('replaces the method URI with a function alias ARN', () => { | ||
}) | ||
describe('.replaceV2IntegrationUriWithAlias', () => { | ||
it('replaces the integration URI with a function alias ARN', () => { | ||
const functionAlias = 'TheFunctionAlias' | ||
const uriWithAwsVariables = [ | ||
'arn:', | ||
{ Ref: 'AWS::Partition' }, | ||
':apigateway:', | ||
{ Ref: 'AWS::Region' }, | ||
':lambda:path/2015-03-31/functions/', | ||
{ Ref: functionAlias }, | ||
'/invocations' | ||
] | ||
const uri = { 'Fn::Join': ['', uriWithAwsVariables] } | ||
const expected = _.set('Properties.IntegrationUri', uri, apiGatewayV2Method) | ||
const actual = ApiGateway.replaceV2IntegrationUriWithAlias(apiGatewayV2Method, functionAlias) | ||
expect(actual).to.deep.equal(expected) | ||
}) | ||
}) | ||
}) |
@@ -1,9 +0,15 @@ | ||
function buildCodeDeployRole () { | ||
return { | ||
const _ = require('lodash/fp') | ||
function buildCodeDeployRole (codeDeployRolePermissionsBoundaryArn, areTriggerConfigurationsSet) { | ||
const attachedPolicies = [ | ||
'arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambdaLimited', | ||
'arn:aws:iam::aws:policy/AWSLambdaFullAccess' | ||
] | ||
if (areTriggerConfigurationsSet) { | ||
attachedPolicies.push('arn:aws:iam::aws:policy/AmazonSNSFullAccess') | ||
} | ||
const iamRoleCodeDeploy = { | ||
Type: 'AWS::IAM::Role', | ||
Properties: { | ||
ManagedPolicyArns: [ | ||
'arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda', | ||
'arn:aws:iam::aws:policy/AWSLambdaFullAccess' | ||
], | ||
ManagedPolicyArns: attachedPolicies, | ||
AssumeRolePolicyDocument: { | ||
@@ -21,8 +27,36 @@ Version: '2012-10-17', | ||
} | ||
if (codeDeployRolePermissionsBoundaryArn) { | ||
Object.assign(iamRoleCodeDeploy.Properties, { PermissionsBoundary: codeDeployRolePermissionsBoundaryArn }) | ||
} | ||
return iamRoleCodeDeploy | ||
} | ||
function buildExecutionRoleWithCodeDeploy (inputRole, codeDeployAppName, deploymentGroups) { | ||
if (deploymentGroups.length === 0) { | ||
return inputRole | ||
} | ||
const outputRole = _.cloneDeep(inputRole) | ||
const statement = _.prop('Properties.Policies.0.PolicyDocument.Statement', outputRole) | ||
if (!statement) { | ||
return inputRole | ||
} | ||
statement.push({ | ||
Action: ['codedeploy:PutLifecycleEventHookExecutionStatus'], | ||
Effect: 'Allow', | ||
Resource: deploymentGroups.map(deploymentGroup => ({ | ||
'Fn::Sub': `arn:\${AWS::Partition}:codedeploy:\${AWS::Region}:\${AWS::AccountId}:deploymentgroup:\${${codeDeployAppName}}/\${${deploymentGroup}}` | ||
})) | ||
}) | ||
return outputRole | ||
} | ||
const Iam = { | ||
buildCodeDeployRole | ||
buildCodeDeployRole, | ||
buildExecutionRoleWithCodeDeploy | ||
} | ||
module.exports = Iam |
@@ -6,3 +6,56 @@ const { expect } = require('chai') | ||
describe('.buildCodeDeployRole', () => { | ||
it('should generate a CodeDeploy::Application resouce', () => { | ||
context('when trigger configurations are not set', () => { | ||
it('should generate a AWS::IAM::Role resource', () => { | ||
const expected = { | ||
Type: 'AWS::IAM::Role', | ||
Properties: { | ||
ManagedPolicyArns: [ | ||
'arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambdaLimited', | ||
'arn:aws:iam::aws:policy/AWSLambdaFullAccess' | ||
], | ||
AssumeRolePolicyDocument: { | ||
Version: '2012-10-17', | ||
Statement: [ | ||
{ | ||
Action: ['sts:AssumeRole'], | ||
Effect: 'Allow', | ||
Principal: { Service: ['codedeploy.amazonaws.com'] } | ||
} | ||
] | ||
} | ||
} | ||
} | ||
const actual = Iam.buildCodeDeployRole(null, false) | ||
expect(actual).to.deep.equal(expected) | ||
}) | ||
}) | ||
context('when trigger configurations are set', () => { | ||
it('should generate a AWS::IAM::Role resource', () => { | ||
const expected = { | ||
Type: 'AWS::IAM::Role', | ||
Properties: { | ||
ManagedPolicyArns: [ | ||
'arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambdaLimited', | ||
'arn:aws:iam::aws:policy/AWSLambdaFullAccess', | ||
'arn:aws:iam::aws:policy/AmazonSNSFullAccess' | ||
], | ||
AssumeRolePolicyDocument: { | ||
Version: '2012-10-17', | ||
Statement: [ | ||
{ | ||
Action: ['sts:AssumeRole'], | ||
Effect: 'Allow', | ||
Principal: { Service: ['codedeploy.amazonaws.com'] } | ||
} | ||
] | ||
} | ||
} | ||
} | ||
const actual = Iam.buildCodeDeployRole(null, true) | ||
expect(actual).to.deep.equal(expected) | ||
}) | ||
}) | ||
}) | ||
describe('.buildCodeDeployRole with IAM permissions boundary', () => { | ||
it('should generate a AWS::IAM::Role resource with permissions boundary set', () => { | ||
const expected = { | ||
@@ -12,3 +65,3 @@ Type: 'AWS::IAM::Role', | ||
ManagedPolicyArns: [ | ||
'arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda', | ||
'arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambdaLimited', | ||
'arn:aws:iam::aws:policy/AWSLambdaFullAccess' | ||
@@ -25,9 +78,151 @@ ], | ||
] | ||
} | ||
}, | ||
PermissionsBoundary: 'arn:aws:iam::11111:policy/entity/boundary' | ||
} | ||
} | ||
const actual = Iam.buildCodeDeployRole() | ||
const actual = Iam.buildCodeDeployRole('arn:aws:iam::11111:policy/entity/boundary') | ||
expect(actual).to.deep.equal(expected) | ||
}) | ||
}) | ||
describe('.buildExecutionRoleWithCodeDeploy', () => { | ||
const codeDeployAppName = 'ServiceDeploymentApplication' | ||
context('when role is well-formed', () => { | ||
it('should skip patching on zero deployment groups', () => { | ||
const input = { | ||
Type: 'AWS::IAM::Role', | ||
Properties: { | ||
Policies: [ | ||
{ | ||
PolicyDocument: { | ||
Statement: [] | ||
} | ||
} | ||
] | ||
} | ||
} | ||
const expected = JSON.parse(JSON.stringify(input)) | ||
const deploymentGroups = [] | ||
const actual = Iam.buildExecutionRoleWithCodeDeploy(input, codeDeployAppName, deploymentGroups) | ||
expect(actual).to.deep.equal(expected) | ||
}) | ||
it('should patch in one deployment group', () => { | ||
const input = { | ||
Type: 'AWS::IAM::Role', | ||
Properties: { | ||
Policies: [ | ||
{ | ||
PolicyDocument: { | ||
Statement: [ | ||
{ | ||
Action: ['s3:*'], | ||
Effect: 'Deny', | ||
Resource: ['*'] | ||
} | ||
] | ||
} | ||
} | ||
] | ||
} | ||
} | ||
const expected = { | ||
Type: 'AWS::IAM::Role', | ||
Properties: { | ||
Policies: [ | ||
{ | ||
PolicyDocument: { | ||
Statement: [ | ||
{ | ||
Action: ['s3:*'], | ||
Effect: 'Deny', | ||
Resource: ['*'] | ||
}, | ||
{ | ||
Action: ['codedeploy:PutLifecycleEventHookExecutionStatus'], | ||
Effect: 'Allow', | ||
Resource: [ | ||
// eslint-disable-next-line no-template-curly-in-string | ||
{ 'Fn::Sub': 'arn:${AWS::Partition}:codedeploy:${AWS::Region}:${AWS::AccountId}:deploymentgroup:${ServiceDeploymentApplication}/${FirstLambdaFunctionDeploymentGroup}' } | ||
] | ||
} | ||
] | ||
} | ||
} | ||
] | ||
} | ||
} | ||
const deploymentGroups = ['FirstLambdaFunctionDeploymentGroup'] | ||
const actual = Iam.buildExecutionRoleWithCodeDeploy(input, codeDeployAppName, deploymentGroups) | ||
expect(actual).to.deep.equal(expected) | ||
expect(actual).not.to.deep.equal(input) | ||
}) | ||
it('should patch in multiple deployment groups', () => { | ||
const input = { | ||
Type: 'AWS::IAM::Role', | ||
Properties: { | ||
Policies: [ | ||
{ | ||
PolicyDocument: { | ||
Statement: [] | ||
} | ||
} | ||
] | ||
} | ||
} | ||
const expected = { | ||
Type: 'AWS::IAM::Role', | ||
Properties: { | ||
Policies: [ | ||
{ | ||
PolicyDocument: { | ||
Statement: [ | ||
{ | ||
Action: ['codedeploy:PutLifecycleEventHookExecutionStatus'], | ||
Effect: 'Allow', | ||
Resource: [ | ||
// eslint-disable-next-line no-template-curly-in-string | ||
{ 'Fn::Sub': 'arn:${AWS::Partition}:codedeploy:${AWS::Region}:${AWS::AccountId}:deploymentgroup:${ServiceDeploymentApplication}/${FirstLambdaFunctionDeploymentGroup}' }, | ||
// eslint-disable-next-line no-template-curly-in-string | ||
{ 'Fn::Sub': 'arn:${AWS::Partition}:codedeploy:${AWS::Region}:${AWS::AccountId}:deploymentgroup:${ServiceDeploymentApplication}/${SecondLambdaFunctionDeploymentGroup}' } | ||
] | ||
} | ||
] | ||
} | ||
} | ||
] | ||
} | ||
} | ||
const deploymentGroups = ['FirstLambdaFunctionDeploymentGroup', 'SecondLambdaFunctionDeploymentGroup'] | ||
const actual = Iam.buildExecutionRoleWithCodeDeploy(input, codeDeployAppName, deploymentGroups) | ||
expect(actual).to.deep.equal(expected) | ||
expect(actual).not.to.deep.equal(input) | ||
}) | ||
}) | ||
context('when role is unexpected', () => { | ||
it('should skip patching', () => { | ||
const input = { | ||
Type: 'AWS::IAM::Role', | ||
Properties: { | ||
ManagedPolicyArns: [ | ||
'arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambdaLimited', | ||
'arn:aws:iam::aws:policy/AWSLambdaFullAccess' | ||
], | ||
AssumeRolePolicyDocument: { | ||
Version: '2012-10-17', | ||
Statement: [ | ||
{ | ||
Action: ['sts:AssumeRole'], | ||
Effect: 'Allow', | ||
Principal: { Service: ['codedeploy.amazonaws.com'] } | ||
} | ||
] | ||
} | ||
} | ||
} | ||
const expected = JSON.parse(JSON.stringify(input)) | ||
const deploymentGroups = ['FirstLambdaFunctionDeploymentGroup'] | ||
const actual = Iam.buildExecutionRoleWithCodeDeploy(input, codeDeployAppName, deploymentGroups) | ||
expect(actual).to.deep.equal(expected) | ||
}) | ||
}) | ||
}) | ||
}) |
@@ -6,3 +6,3 @@ { | ||
}, | ||
"version": "0.4.8", | ||
"version": "0.5.0", | ||
"description": "A Serverless plugin to implement canary deployment of Lambda functions", | ||
@@ -17,3 +17,3 @@ "main": "serverless-plugin-canary-deployments.js", | ||
"contributors": [ | ||
"Carlos Castellanos <ccverak@gmail.com> (https://github.com/ccverak/)", | ||
"Carlos Castellanos <me@carloscastellanosvera.com> (https://github.com/ccverak/)", | ||
"Kartikeya Verma <kverma23@outlook.com> (https://github.com/kverma23/)" | ||
@@ -20,0 +20,0 @@ ], |
@@ -74,4 +74,4 @@ [![npm version](https://badge.fury.io/js/serverless-plugin-canary-deployments.svg)](https://badge.fury.io/js/serverless-plugin-canary-deployments) | ||
* `alias`: (required) name that will be used to create the Lambda function alias. | ||
* `preTrafficHook`: (optional) validation Lambda function that runs before traffic shifting. It must use te CodeDeploy SDK to notify about this step's success or failure (more info [here](https://docs.aws.amazon.com/codedeploy/latest/userguide/reference-appspec-file-structure-hooks.html)). | ||
* `postTrafficHook`: (optional) validation Lambda function that runs after traffic shifting. It must use te CodeDeploy SDK to notify about this step's success or failure (more info [here](https://docs.aws.amazon.com/codedeploy/latest/userguide/reference-appspec-file-structure-hooks.html)) | ||
* `preTrafficHook`: (optional) validation Lambda function that runs before traffic shifting. It must use the CodeDeploy SDK to notify about this step's success or failure (more info [here](https://docs.aws.amazon.com/codedeploy/latest/userguide/reference-appspec-file-structure-hooks.html)). | ||
* `postTrafficHook`: (optional) validation Lambda function that runs after traffic shifting. It must use the CodeDeploy SDK to notify about this step's success or failure (more info [here](https://docs.aws.amazon.com/codedeploy/latest/userguide/reference-appspec-file-structure-hooks.html)) | ||
* `alarms`: (optional) list of CloudWatch alarms. If any of them is triggered during the deployment, the associated Lambda function will automatically roll back to the previous version. | ||
@@ -88,2 +88,3 @@ * `triggerConfigurations`: (optional) list of CodeDeploy Triggers. See more details in the [CodeDeploy TriggerConfiguration Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-triggerconfig.html), or [this CodeDeploy notifications guide](https://docs.aws.amazon.com/codedeploy/latest/userguide/monitoring-sns-event-notifications-create-trigger.html) for example uses | ||
codeDeployRole: some_arn_value | ||
codeDeployRolePermissionsBoundary: some_arn_value | ||
stages: | ||
@@ -100,2 +101,3 @@ - dev | ||
* `codeDeployRole`: (optional) an ARN specifying an existing IAM role for CodeDeploy. If absent, one will be created for you. See the [codeDeploy policy](./example-code-deploy-policy.json) for an example of what is needed. | ||
* `codeDeployRolePermissionsBoundary`: (optional) an ARN specifying an existing IAM permissions boundary, this permission boundary is set on the code deploy that is being created when codeDeployRole is not defined. | ||
* `stages`: (optional) list of stages where you want to deploy your functions gradually. If not present, it assumes that are all of them. | ||
@@ -102,0 +104,0 @@ |
@@ -43,4 +43,5 @@ const _ = require('lodash/fp') | ||
const codeDeployApp = this.buildCodeDeployApp() | ||
const codeDeployRole = this.buildCodeDeployRole() | ||
const functionsResources = this.buildFunctionsResources() | ||
const codeDeployRole = this.buildCodeDeployRole(this.areTriggerConfigurationsSet(functionsResources)) | ||
const executionRole = this.buildExecutionRole() | ||
Object.assign( | ||
@@ -50,2 +51,3 @@ this.compiledTpl.Resources, | ||
codeDeployRole, | ||
executionRole, | ||
...functionsResources | ||
@@ -56,2 +58,16 @@ ) | ||
areTriggerConfigurationsSet (functionsResources) { | ||
// Checking if the template has trigger configurations. | ||
for (var resource of functionsResources) { | ||
for (var key of Object.keys(resource)) { | ||
if (resource[key].Type === 'AWS::CodeDeploy::DeploymentGroup') { | ||
if (resource[key].Properties.TriggerConfigurations) { | ||
return true | ||
} | ||
} | ||
} | ||
} | ||
return false | ||
} | ||
shouldDeployDeployGradually () { | ||
@@ -66,2 +82,27 @@ return this.withDeploymentPreferencesFns.length > 0 && this.currentStageEnabled() | ||
buildExecutionRole () { | ||
const logicalName = this.naming.getRoleLogicalId() | ||
const inputRole = this.compiledTpl.Resources[logicalName] | ||
if (!inputRole) { | ||
return | ||
} | ||
const hasHook = _.pipe( | ||
this.getDeploymentSettingsFor.bind(this), | ||
settings => settings.preTrafficHook || settings.postTrafficHook | ||
) | ||
const getDeploymentGroup = _.pipe( | ||
this.getFunctionName.bind(this), | ||
this.getFunctionDeploymentGroupId.bind(this) | ||
) | ||
const deploymentGroups = _.pipe( | ||
_.filter(hasHook), | ||
_.map(getDeploymentGroup) | ||
)(this.withDeploymentPreferencesFns) | ||
const outputRole = CfGenerators.iam.buildExecutionRoleWithCodeDeploy(inputRole, this.codeDeployAppName, deploymentGroups) | ||
return { [logicalName]: outputRole } | ||
} | ||
buildFunctionsResources () { | ||
@@ -93,6 +134,6 @@ return _.flatMap( | ||
buildCodeDeployRole () { | ||
buildCodeDeployRole (areTriggerConfigurationsSet) { | ||
if (this.globalSettings.codeDeployRole) return {} | ||
const logicalName = 'CodeDeployServiceRole' | ||
const template = CfGenerators.iam.buildCodeDeployRole() | ||
const template = CfGenerators.iam.buildCodeDeployRole(this.globalSettings.codeDeployRolePermissionsBoundary, areTriggerConfigurationsSet) | ||
return { [logicalName]: template } | ||
@@ -102,3 +143,3 @@ } | ||
buildFunctionDeploymentGroup ({ deploymentSettings, functionName }) { | ||
const logicalName = `${functionName}DeploymentGroup` | ||
const logicalName = this.getFunctionDeploymentGroupId(functionName) | ||
const params = { | ||
@@ -134,2 +175,6 @@ codeDeployAppName: this.codeDeployAppName, | ||
getFunctionDeploymentGroupId (functionLogicalId) { | ||
return `${functionLogicalId}DeploymentGroup` | ||
} | ||
getFunctionName (slsFunctionName) { | ||
@@ -152,2 +197,4 @@ return slsFunctionName ? this.naming.getLambdaLogicalId(slsFunctionName) : null | ||
'AWS::ApiGateway::Method': CfGenerators.apiGateway.replaceMethodUriWithAlias, | ||
'AWS::ApiGatewayV2::Integration': CfGenerators.apiGateway.replaceV2IntegrationUriWithAlias, | ||
'AWS::ApiGatewayV2::Authorizer': CfGenerators.apiGateway.replaceV2AuthorizerUriWithAlias, | ||
'AWS::SNS::Topic': CfGenerators.sns.replaceTopicSubscriptionFunctionWithAlias, | ||
@@ -171,2 +218,4 @@ 'AWS::SNS::Subscription': CfGenerators.sns.replaceSubscriptionFunctionWithAlias, | ||
const apiGatewayMethods = this.getApiGatewayMethodsFor(functionName) | ||
const apiGatewayV2Methods = this.getApiGatewayV2MethodsFor(functionName) | ||
const apiGatewayV2Authorizers = this.getApiGatewayV2AuthorizersFor(functionName) | ||
const eventSourceMappings = this.getEventSourceMappingsFor(functionName) | ||
@@ -182,2 +231,4 @@ const snsTopics = this.getSnsTopicsFor(functionName) | ||
apiGatewayMethods, | ||
apiGatewayV2Methods, | ||
apiGatewayV2Authorizers, | ||
eventSourceMappings, | ||
@@ -207,2 +258,30 @@ snsTopics, | ||
getApiGatewayV2MethodsFor (functionName) { | ||
const isApiGMethod = _.matchesProperty('Type', 'AWS::ApiGatewayV2::Integration') | ||
const isMethodForFunction = _.pipe( | ||
_.prop('Properties.IntegrationUri'), | ||
flattenObject, | ||
_.includes(functionName) | ||
) | ||
const getMethodsForFunction = _.pipe( | ||
_.pickBy(isApiGMethod), | ||
_.pickBy(isMethodForFunction) | ||
) | ||
return getMethodsForFunction(this.compiledTpl.Resources) | ||
} | ||
getApiGatewayV2AuthorizersFor (functionName) { | ||
const isApiGMethod = _.matchesProperty('Type', 'AWS::ApiGatewayV2::Authorizer') | ||
const isMethodForFunction = _.pipe( | ||
_.prop('Properties.AuthorizerUri'), | ||
flattenObject, | ||
_.includes(functionName) | ||
) | ||
const getMethodsForFunction = _.pipe( | ||
_.pickBy(isApiGMethod), | ||
_.pickBy(isMethodForFunction) | ||
) | ||
return getMethodsForFunction(this.compiledTpl.Resources) | ||
} | ||
getEventSourceMappingsFor (functionName) { | ||
@@ -328,4 +407,4 @@ const isEventSourceMapping = _.matchesProperty('Type', 'AWS::Lambda::EventSourceMapping') | ||
getDeploymentSettingsFor (serverlessFunction) { | ||
const fnDeploymentSetting = this.service.getFunction(serverlessFunction).deploymentSettings | ||
getDeploymentSettingsFor (slsFunctionName) { | ||
const fnDeploymentSetting = this.service.getFunction(slsFunctionName).deploymentSettings | ||
return Object.assign({}, this.globalSettings, fnDeploymentSetting) | ||
@@ -332,0 +411,0 @@ } |
512554
69
18080
127