From 4247966861a2a61856312fe0aebdb633eff1535a Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Mon, 8 Apr 2019 14:16:10 -0700 Subject: [PATCH] feat(lambda): introduce a new kind of `Code`, `CfnParametersCode`. (#2027) This Code type is helpful when there is no local file/directory to use Assets with, and the Lambda is supposed to be deployed in a CodePipeline. --- .../aws-codepipeline-actions/README.md | 44 +- ...yed-through-codepipeline.lit.expected.json | 934 ++++++++++++++++++ ...ambda-deployed-through-codepipeline.lit.ts | 126 +++ .../@aws-cdk/aws-codepipeline/lib/artifact.ts | 12 + .../@aws-cdk/aws-codepipeline/package.json | 4 +- packages/@aws-cdk/aws-lambda/lib/code.ts | 125 ++- .../@aws-cdk/aws-lambda/test/test.code.ts | 110 ++- packages/@aws-cdk/aws-s3/lib/coordinates.ts | 14 + packages/@aws-cdk/aws-s3/lib/index.ts | 1 + 9 files changed, 1356 insertions(+), 14 deletions(-) create mode 100644 packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json create mode 100644 packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.ts create mode 100644 packages/@aws-cdk/aws-s3/lib/coordinates.ts diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index 1eaa460a66531..4450229449fdc 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -324,9 +324,22 @@ This package defines the following actions: changes from the people (or system) applying the changes. * **CloudFormationExecuteChangeSetAction** - Execute a change set prepared previously. +##### Lambda deployed through CodePipeline + +If you want to deploy your Lambda through CodePipeline, +and you don't use assets (for example, because your CDK code and Lambda code are separate), +you can use a special Lambda `Code` class, `CfnParametersCode`. +Note that your Lambda must be in a different Stack than your Pipeline. +The Lambda itself will be deployed, alongside the entire Stack it belongs to, +using a CloudFormation CodePipeline Action. Example: + +[Example of deploying a Lambda through CodePipeline](test/integ.lambda-deployed-through-codepipeline.lit.ts) + #### AWS CodeDeploy -To use CodeDeploy in a Pipeline: +##### Server deployments + +To use CodeDeploy for EC2/on-premise deployments in a Pipeline: ```ts import codedeploy = require('@aws-cdk/aws-codedeploy'); @@ -348,6 +361,35 @@ pipeline.addStage({ }); ``` +##### Lambda deployments + +To use CodeDeploy for blue-green Lambda deployments in a Pipeline: + +```typescript +const lambdaCode = lambda.Code.cfnParameters(); +const func = new lambda.Function(lambdaStack, 'Lambda', { + code: lambdaCode, + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810, +}); +// used to make sure each CDK synthesis produces a different Version +const version = func.newVersion(); +const alias = new lambda.Alias(lambdaStack, 'LambdaAlias', { + aliasName: 'Prod', + version, +}); + +new codedeploy.LambdaDeploymentGroup(lambdaStack, 'DeploymentGroup', { + alias, + deploymentConfig: codedeploy.LambdaDeploymentConfig.Linear10PercentEvery1Minute, +}); +``` + +Then, you need to create your Pipeline Stack, +where you will define your Pipeline, +and deploy the `lambdaStack` using a CloudFormation CodePipeline Action +(see above for a complete example). + #### AWS S3 To use an S3 Bucket as a deployment target in CodePipeline: diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json new file mode 100644 index 0000000000000..f7976a8bf1f94 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json @@ -0,0 +1,934 @@ +{ + "Resources": { + "PipelineArtifactsBucket22248F97": { + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain" + }, + "PipelineRoleD68726F7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "codepipeline.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleDefaultPolicyC7A05455": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "codecommit:GetBranch", + "codecommit:GetCommit", + "codecommit:UploadArchive", + "codecommit:GetUploadArchiveStatus", + "codecommit:CancelUploadArchive" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CdkCodeRepo7D2EC742", + "Arn" + ] + } + }, + { + "Action": [ + "codecommit:GetBranch", + "codecommit:GetCommit", + "codecommit:UploadArchive", + "codecommit:GetUploadArchiveStatus", + "codecommit:CancelUploadArchive" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "LambdaCodeRepoE08DD409", + "Arn" + ] + } + }, + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CdkBuildProject9382C38D", + "Arn" + ] + } + }, + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "LambdaBuildProject7E2DAB11", + "Arn" + ] + } + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineDeployLambdaCFNDeployRole89CA1043", + "Arn" + ] + } + }, + { + "Action": [ + "cloudformation:CreateStack", + "cloudformation:DescribeStack*", + "cloudformation:GetStackPolicy", + "cloudformation:GetTemplate*", + "cloudformation:SetStackPolicy", + "cloudformation:UpdateStack", + "cloudformation:ValidateTemplate" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":stack/LambdaStackDeployedName/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicyC7A05455", + "Roles": [ + { + "Ref": "PipelineRoleD68726F7" + } + ] + } + }, + "PipelineC660917D": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleD68726F7", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "CodeCommit", + "Version": "1" + }, + "Configuration": { + "RepositoryName": { + "Fn::GetAtt": [ + "CdkCodeRepo7D2EC742", + "Name" + ] + }, + "BranchName": "master", + "PollForSourceChanges": false + }, + "InputArtifacts": [], + "Name": "CdkCode_Source", + "OutputArtifacts": [ + { + "Name": "Artifact_CdkCode_Source_PipelineStackCdkCodeRepo766C119A" + } + ], + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "CodeCommit", + "Version": "1" + }, + "Configuration": { + "RepositoryName": { + "Fn::GetAtt": [ + "LambdaCodeRepoE08DD409", + "Name" + ] + }, + "BranchName": "master", + "PollForSourceChanges": false + }, + "InputArtifacts": [], + "Name": "LambdaCode_Source", + "OutputArtifacts": [ + { + "Name": "Artifact_LambdaCode_Source_PipelineStackLambdaCodeRepo0F311735" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "CdkBuildProject9382C38D" + } + }, + "InputArtifacts": [ + { + "Name": "Artifact_CdkCode_Source_PipelineStackCdkCodeRepo766C119A" + } + ], + "Name": "CDK_Build", + "OutputArtifacts": [ + { + "Name": "Artifact_CDK_Build_PipelineStackCdkBuildProject3F998A60" + } + ], + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "LambdaBuildProject7E2DAB11" + } + }, + "InputArtifacts": [ + { + "Name": "Artifact_LambdaCode_Source_PipelineStackLambdaCodeRepo0F311735" + } + ], + "Name": "Lambda_Build", + "OutputArtifacts": [ + { + "Name": "Artifact_Lambda_Build_PipelineStackLambdaBuildProjectC90BA2C4" + } + ], + "RunOrder": 1 + } + ], + "Name": "Build" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "LambdaStackDeployedName", + "ActionMode": "CREATE_UPDATE", + "TemplatePath": "Artifact_CDK_Build_PipelineStackCdkBuildProject3F998A60::LambdaStack.template.yaml", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineDeployLambdaCFNDeployRole89CA1043", + "Arn" + ] + }, + "ParameterOverrides": "{\"LambdaLambdaSourceBucketNameParameter159473FC\":{\"Fn::GetArtifactAtt\":[\"Artifact_Lambda_Build_PipelineStackLambdaBuildProjectC90BA2C4\",\"BucketName\"]},\"LambdaLambdaSourceObjectKeyParameter06573F1D\":{\"Fn::GetArtifactAtt\":[\"Artifact_Lambda_Build_PipelineStackLambdaBuildProjectC90BA2C4\",\"ObjectKey\"]}}" + }, + "InputArtifacts": [ + { + "Name": "Artifact_Lambda_Build_PipelineStackLambdaBuildProjectC90BA2C4" + }, + { + "Name": "Artifact_CDK_Build_PipelineStackCdkBuildProject3F998A60" + } + ], + "Name": "Lambda_CFN_Deploy", + "OutputArtifacts": [], + "RunOrder": 1 + } + ], + "Name": "Deploy" + } + ], + "ArtifactStore": { + "Location": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "Type": "S3" + } + }, + "DependsOn": [ + "PipelineRoleDefaultPolicyC7A05455", + "PipelineRoleD68726F7" + ] + }, + "PipelineEventsRole46BEEA7C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "events.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineEventsRoleDefaultPolicyFF4FCCE0": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "codepipeline:StartPipelineExecution", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codepipeline:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "PipelineC660917D" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineEventsRoleDefaultPolicyFF4FCCE0", + "Roles": [ + { + "Ref": "PipelineEventsRole46BEEA7C" + } + ] + } + }, + "PipelineDeployLambdaCFNDeployRole89CA1043": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "cloudformation.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineDeployLambdaCFNDeployRoleDefaultPolicyE83FD793": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "*", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineDeployLambdaCFNDeployRoleDefaultPolicyE83FD793", + "Roles": [ + { + "Ref": "PipelineDeployLambdaCFNDeployRole89CA1043" + } + ] + } + }, + "CdkCodeRepo7D2EC742": { + "Type": "AWS::CodeCommit::Repository", + "Properties": { + "RepositoryName": "CdkCodeRepo", + "Triggers": [] + } + }, + "CdkCodeRepoPipelineStackPipeline9DB740AFEventRule97707F9A": { + "Type": "AWS::Events::Rule", + "Properties": { + "EventPattern": { + "source": [ + "aws.codecommit" + ], + "resources": [ + { + "Fn::GetAtt": [ + "CdkCodeRepo7D2EC742", + "Arn" + ] + } + ], + "detail-type": [ + "CodeCommit Repository State Change" + ], + "detail": { + "event": [ + "referenceCreated", + "referenceUpdated" + ], + "referenceName": [ + "master" + ] + } + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codepipeline:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "PipelineC660917D" + } + ] + ] + }, + "Id": "Pipeline", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineEventsRole46BEEA7C", + "Arn" + ] + } + } + ] + } + }, + "LambdaCodeRepoE08DD409": { + "Type": "AWS::CodeCommit::Repository", + "Properties": { + "RepositoryName": "LambdaCodeRepo", + "Triggers": [] + } + }, + "LambdaCodeRepoPipelineStackPipeline9DB740AFEventRule2C34743D": { + "Type": "AWS::Events::Rule", + "Properties": { + "EventPattern": { + "source": [ + "aws.codecommit" + ], + "resources": [ + { + "Fn::GetAtt": [ + "LambdaCodeRepoE08DD409", + "Arn" + ] + } + ], + "detail-type": [ + "CodeCommit Repository State Change" + ], + "detail": { + "event": [ + "referenceCreated", + "referenceUpdated" + ], + "referenceName": [ + "master" + ] + } + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codepipeline:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "PipelineC660917D" + } + ] + ] + }, + "Id": "Pipeline", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineEventsRole46BEEA7C", + "Arn" + ] + } + } + ] + } + }, + "CdkBuildProjectRoleE0B6FEB0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "codebuild.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "CdkBuildProjectRoleDefaultPolicy3C7ECB00": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "CdkBuildProject9382C38D" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "CdkBuildProject9382C38D" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CdkBuildProjectRoleDefaultPolicy3C7ECB00", + "Roles": [ + { + "Ref": "CdkBuildProjectRoleE0B6FEB0" + } + ] + } + }, + "CdkBuildProject9382C38D": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "NO_ARTIFACTS" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/nodejs:10.1.0", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "CdkBuildProjectRoleE0B6FEB0", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": \"npm install\"\n },\n \"build\": {\n \"commands\": [\n \"npm run build\",\n \"npm run cdk synth LambdaStack -- -o .\"\n ]\n }\n },\n \"artifacts\": {\n \"files\": \"LambdaStack.template.yaml\"\n }\n}", + "Type": "NO_SOURCE" + } + } + }, + "LambdaBuildProjectRoleD0C4F982": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "codebuild.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "LambdaBuildProjectRoleDefaultPolicyA3A66624": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "LambdaBuildProject7E2DAB11" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "LambdaBuildProject7E2DAB11" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaBuildProjectRoleDefaultPolicyA3A66624", + "Roles": [ + { + "Ref": "LambdaBuildProjectRoleD0C4F982" + } + ] + } + }, + "LambdaBuildProject7E2DAB11": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "NO_ARTIFACTS" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/nodejs:10.1.0", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "LambdaBuildProjectRoleD0C4F982", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": \"npm install\"\n },\n \"build\": {\n \"commands\": \"npm run build\"\n }\n },\n \"artifacts\": {\n \"files\": [\n \"index.js\",\n \"node_modules/**/*\"\n ]\n }\n}", + "Type": "NO_SOURCE" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.ts new file mode 100644 index 0000000000000..0418bf57fe41a --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.ts @@ -0,0 +1,126 @@ +import codebuild = require('@aws-cdk/aws-codebuild'); +import codecommit = require('@aws-cdk/aws-codecommit'); +import codepipeline = require('@aws-cdk/aws-codepipeline'); +import lambda = require('@aws-cdk/aws-lambda'); +import cdk = require('@aws-cdk/cdk'); +import codepipeline_actions = require('../lib'); + +const app = new cdk.App(); + +/// !show +const lambdaStack = new cdk.Stack(app, 'LambdaStack', { + // remove the Stack from `cdk synth` and `cdk deploy` + // unless you explicitly filter for it + autoDeploy: false, +}); +const lambdaCode = lambda.Code.cfnParameters(); +new lambda.Function(lambdaStack, 'Lambda', { + code: lambdaCode, + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810, +}); +// other resources that your Lambda needs, added to the lambdaStack... + +const pipelineStack = new cdk.Stack(app, 'PipelineStack'); +const pipeline = new codepipeline.Pipeline(pipelineStack, 'Pipeline'); + +// add the source code repository containing this code to your Pipeline, +// and the source code of the Lambda Function, if they're separate +const cdkSourceAction = new codepipeline_actions.CodeCommitSourceAction({ + repository: new codecommit.Repository(pipelineStack, 'CdkCodeRepo', { repositoryName: 'CdkCodeRepo'}), + actionName: 'CdkCode_Source', +}); +const lambdaSourceAction = new codepipeline_actions.CodeCommitSourceAction({ + repository: new codecommit.Repository(pipelineStack, 'LambdaCodeRepo', { repositoryName: 'LambdaCodeRepo'}), + actionName: 'LambdaCode_Source', +}); +pipeline.addStage({ + name: 'Source', + actions: [cdkSourceAction, lambdaSourceAction], +}); + +// synthesize the Lambda CDK template, using CodeBuild +// the below values are just examples, assuming your CDK code is in TypeScript/JavaScript - +// adjust the build environment and/or commands accordingly +const cdkBuildProject = new codebuild.Project(pipelineStack, 'CdkBuildProject', { + environment: { + buildImage: codebuild.LinuxBuildImage.UBUNTU_14_04_NODEJS_10_1_0, + }, + buildSpec: { + version: '0.2', + phases: { + install: { + commands: 'npm install', + }, + build: { + commands: [ + 'npm run build', + 'npm run cdk synth LambdaStack -- -o .', + ], + }, + }, + artifacts: { + files: 'LambdaStack.template.yaml', + }, + }, +}); +const cdkBuildAction = new codepipeline_actions.CodeBuildBuildAction({ + actionName: 'CDK_Build', + project: cdkBuildProject, + inputArtifact: cdkSourceAction.outputArtifact, +}); + +// build your Lambda code, using CodeBuild +// again, this example assumes your Lambda is written in TypeScript/JavaScript - +// make sure to adjust the build environment and/or commands if they don't match your specific situation +const lambdaBuildProject = new codebuild.Project(pipelineStack, 'LambdaBuildProject', { + environment: { + buildImage: codebuild.LinuxBuildImage.UBUNTU_14_04_NODEJS_10_1_0, + }, + buildSpec: { + version: '0.2', + phases: { + install: { + commands: 'npm install', + }, + build: { + commands: 'npm run build', + }, + }, + artifacts: { + files: [ + 'index.js', + 'node_modules/**/*', + ], + }, + }, +}); +const lambdaBuildAction = new codepipeline_actions.CodeBuildBuildAction({ + actionName: 'Lambda_Build', + project: lambdaBuildProject, + inputArtifact: lambdaSourceAction.outputArtifact, +}); + +pipeline.addStage({ + name: 'Build', + actions: [cdkBuildAction, lambdaBuildAction], +}); + +// finally, deploy your Lambda Stack +pipeline.addStage({ + name: 'Deploy', + actions: [ + new codepipeline_actions.CloudFormationCreateUpdateStackAction({ + actionName: 'Lambda_CFN_Deploy', + templatePath: cdkBuildAction.outputArtifact.atPath('LambdaStack.template.yaml'), + stackName: 'LambdaStackDeployedName', + adminPermissions: true, + parameterOverrides: { + ...lambdaCode.assign(lambdaBuildAction.outputArtifact.s3Coordinates), + }, + additionalInputArtifacts: [ + lambdaBuildAction.outputArtifact, + ], + }), + ], +}); diff --git a/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts b/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts index 0e4f03a1e0c6e..ea99ed0ac6262 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts @@ -1,3 +1,4 @@ +import s3 = require("@aws-cdk/aws-s3"); import { Token } from "@aws-cdk/cdk"; /** @@ -48,6 +49,17 @@ export class Artifact { return artifactGetParam(this, jsonFile, keyName); } + /** + * Returns the coordinates of the .zip file in S3 that this Artifact represents. + * Used by Lambda's `CfnParametersCode` when being deployed in a CodePipeline. + */ + public get s3Coordinates(): s3.Coordinates { + return { + bucketName: this.bucketName, + objectKey: this.objectKey, + }; + } + public toString() { return this.artifactName; } diff --git a/packages/@aws-cdk/aws-codepipeline/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index 3f5940570ede5..ffbcaa7cd7341 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -75,7 +75,6 @@ "@aws-cdk/aws-events": "^0.28.0", "@aws-cdk/aws-iam": "^0.28.0", "@aws-cdk/aws-s3": "^0.28.0", - "@aws-cdk/aws-sns": "^0.28.0", "@aws-cdk/cdk": "^0.28.0" }, "homepage": "https://github.com/awslabs/aws-cdk", @@ -83,7 +82,6 @@ "@aws-cdk/aws-events": "^0.28.0", "@aws-cdk/aws-iam": "^0.28.0", "@aws-cdk/aws-s3": "^0.28.0", - "@aws-cdk/aws-sns": "^0.28.0", "@aws-cdk/cdk": "^0.28.0" }, "engines": { @@ -97,4 +95,4 @@ "import-props-interface:@aws-cdk/aws-codepipeline.PipelineImportProps" ] } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-lambda/lib/code.ts b/packages/@aws-cdk/aws-lambda/lib/code.ts index 4eaf5ef069bc6..0c0ddc19e3bf1 100644 --- a/packages/@aws-cdk/aws-lambda/lib/code.ts +++ b/packages/@aws-cdk/aws-lambda/lib/code.ts @@ -11,7 +11,7 @@ export abstract class Code { * @param key The object key * @param objectVersion Optional S3 object version */ - public static bucket(bucket: s3.IBucket, key: string, objectVersion?: string) { + public static bucket(bucket: s3.IBucket, key: string, objectVersion?: string): S3Code { return new S3Code(bucket, key, objectVersion); } @@ -19,7 +19,7 @@ export abstract class Code { * @returns `LambdaInlineCode` with inline code. * @param code The actual handler code (limited to 4KiB) */ - public static inline(code: string) { + public static inline(code: string): InlineCode { return new InlineCode(code); } @@ -27,7 +27,7 @@ export abstract class Code { * Loads the function code from a local disk asset. * @param path Either a directory with the Lambda code bundle or a .zip file */ - public static asset(path: string) { + public static asset(path: string): AssetCode { return new AssetCode(path); } @@ -37,7 +37,7 @@ export abstract class Code { * @param directoryToZip The directory to zip * @deprecated use `lambda.Code.asset(path)` (no need to specify if it's a file or a directory) */ - public static directory(directoryToZip: string) { + public static directory(directoryToZip: string): AssetCode { return new AssetCode(directoryToZip, assets.AssetPackaging.ZipDirectory); } @@ -46,10 +46,20 @@ export abstract class Code { * @param filePath The file path * @deprecated use `lambda.Code.asset(path)` (no need to specify if it's a file or a directory) */ - public static file(filePath: string) { + public static file(filePath: string): AssetCode { return new AssetCode(filePath, assets.AssetPackaging.File); } + /** + * Creates a new Lambda source defined using CloudFormation parameters. + * + * @returns a new instance of `CfnParametersCode` + * @param props optional construction properties of {@link CfnParametersCode} + */ + public static cfnParameters(props?: CfnParametersCodeProps): CfnParametersCode { + return new CfnParametersCode(props); + } + /** * Determines whether this Code is inline code or not. */ @@ -158,8 +168,8 @@ export class AssetCode extends Code { this.packaging = packaging; } else { this.packaging = fs.lstatSync(path).isDirectory() - ? assets.AssetPackaging.ZipDirectory - : assets.AssetPackaging.File; + ? assets.AssetPackaging.ZipDirectory + : assets.AssetPackaging.File; } } @@ -186,9 +196,108 @@ export class AssetCode extends Code { this.asset!.addResourceMetadata(resource, 'Code'); } - return { + return { s3Bucket: this.asset!.s3BucketName, s3Key: this.asset!.s3ObjectKey }; } } + +/** + * Construction properties for {@link CfnParametersCode}. + */ +export interface CfnParametersCodeProps { + /** + * The CloudFormation parameter that represents the name of the S3 Bucket + * where the Lambda code will be located in. + * Must be of type 'String'. + * + * @default a new parameter will be created + */ + readonly bucketNameParam?: cdk.CfnParameter; + + /** + * The CloudFormation parameter that represents the path inside the S3 Bucket + * where the Lambda code will be located at. + * Must be of type 'String'. + * + * @default a new parameter will be created + */ + readonly objectKeyParam?: cdk.CfnParameter; +} + +/** + * Lambda code defined using 2 CloudFormation parameters. + * Useful when you don't have access to the code of your Lambda from your CDK code, so you can't use Assets, + * and you want to deploy the Lambda in a CodePipeline, using CloudFormation Actions - + * you can fill the parameters using the {@link #assign} method. + */ +export class CfnParametersCode extends Code { + public readonly isInline = false; + private _bucketNameParam?: cdk.CfnParameter; + private _objectKeyParam?: cdk.CfnParameter; + + constructor(props: CfnParametersCodeProps = {}) { + super(); + + this._bucketNameParam = props.bucketNameParam; + this._objectKeyParam = props.objectKeyParam; + } + + public bind(construct: cdk.Construct) { + if (!this._bucketNameParam) { + this._bucketNameParam = new cdk.CfnParameter(construct, 'LambdaSourceBucketNameParameter', { + type: 'String', + }); + } + + if (!this._objectKeyParam) { + this._objectKeyParam = new cdk.CfnParameter(construct, 'LambdaSourceObjectKeyParameter', { + type: 'String', + }); + } + } + + /** + * Create a parameters map from this instance's CloudFormation parameters. + * + * It returns a map with 2 keys that correspond to the names of the parameters defined in this Lambda code, + * and as values it contains the appropriate expressions pointing at the provided S3 coordinates + * (most likely, obtained from a CodePipeline Artifact by calling the `artifact.s3Coordinates` method). + * The result should be provided to the CloudFormation Action + * that is deploying the Stack that the Lambda with this code is part of, + * in the `parameterOverrides` property. + * + * @param coordinates the coordinates of the object in S3 that represents the Lambda code + */ + public assign(coordinates: s3.Coordinates): { [name: string]: any } { + const ret: { [name: string]: any } = {}; + ret[this.bucketNameParam] = coordinates.bucketName; + ret[this.objectKeyParam] = coordinates.objectKey; + return ret; + } + + /** @internal */ + public _toJSON(_?: cdk.CfnResource): CfnFunction.CodeProperty { + return { + s3Bucket: this._bucketNameParam!.stringValue, + s3Key: this._objectKeyParam!.stringValue, + }; + } + + public get bucketNameParam(): string { + if (this._bucketNameParam) { + return this._bucketNameParam.logicalId; + } else { + throw new Error('Pass CfnParametersCode to a Lambda Function before accessing the bucketNameParam property'); + } + } + + public get objectKeyParam(): string { + if (this._objectKeyParam) { + return this._objectKeyParam.logicalId; + } else { + throw new Error('Pass CfnParametersCode to a Lambda Function before accessing the objectKeyParam property'); + } + } +} diff --git a/packages/@aws-cdk/aws-lambda/test/test.code.ts b/packages/@aws-cdk/aws-lambda/test/test.code.ts index ea08dd02d83e3..f74809a71fc37 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.code.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.code.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert'; import assets = require('@aws-cdk/assets'); import cdk = require('@aws-cdk/cdk'); import cxapi = require('@aws-cdk/cx-api'); @@ -6,6 +6,8 @@ import { Test } from 'nodeunit'; import path = require('path'); import lambda = require('../lib'); +// tslint:disable:no-string-literal + export = { 'lambda.Code.inline': { 'fails if used with unsupported runtimes'(test: Test) { @@ -93,7 +95,111 @@ export = { }, ResourcePart.CompleteDefinition)); test.done(); } - } + }, + + 'lambda.Code.cfnParameters': { + "automatically creates the Bucket and Key parameters when it's used in a Function"(test: Test) { + const stack = new cdk.Stack(); + const code = new lambda.CfnParametersCode(); + new lambda.Function(stack, 'Function', { + code, + runtime: lambda.Runtime.NodeJS810, + handler: 'index.handler', + }); + + expect(stack).to(haveResourceLike('AWS::Lambda::Function', { + Code: { + S3Bucket: { + Ref: "FunctionLambdaSourceBucketNameParameter9E9E108F", + }, + S3Key: { + Ref: "FunctionLambdaSourceObjectKeyParameter1C7AED11", + }, + }, + })); + + test.equal(stack.node.resolve(code.bucketNameParam), 'FunctionLambdaSourceBucketNameParameter9E9E108F'); + test.equal(stack.node.resolve(code.objectKeyParam), 'FunctionLambdaSourceObjectKeyParameter1C7AED11'); + + test.done(); + }, + + 'does not allow accessing the Parameter properties before being used in a Function'(test: Test) { + const code = new lambda.CfnParametersCode(); + + test.throws(() => { + test.notEqual(code.bucketNameParam, undefined); + }, /bucketNameParam/); + + test.throws(() => { + test.notEqual(code.objectKeyParam, undefined); + }, /objectKeyParam/); + + test.done(); + }, + + 'allows passing custom Parameters when creating it'(test: Test) { + const stack = new cdk.Stack(); + const bucketNameParam = new cdk.CfnParameter(stack, 'BucketNameParam', { + type: 'String', + }); + const bucketKeyParam = new cdk.CfnParameter(stack, 'ObjectKeyParam', { + type: 'String', + }); + + const code = lambda.Code.cfnParameters({ + bucketNameParam, + objectKeyParam: bucketKeyParam, + }); + + test.equal(stack.node.resolve(code.bucketNameParam), 'BucketNameParam'); + test.equal(stack.node.resolve(code.objectKeyParam), 'ObjectKeyParam'); + + new lambda.Function(stack, 'Function', { + code, + runtime: lambda.Runtime.NodeJS810, + handler: 'index.handler', + }); + + expect(stack).to(haveResourceLike('AWS::Lambda::Function', { + Code: { + S3Bucket: { + Ref: "BucketNameParam", + }, + S3Key: { + Ref: "ObjectKeyParam", + }, + }, + })); + + test.done(); + }, + + 'can assign parameters'(test: Test) { + // given + const stack = new cdk.Stack(); + const code = new lambda.CfnParametersCode({ + bucketNameParam: new cdk.CfnParameter(stack, 'BucketNameParam', { + type: 'String', + }), + objectKeyParam: new cdk.CfnParameter(stack, 'ObjectKeyParam', { + type: 'String', + }), + }); + + // when + const overrides = stack.node.resolve(code.assign({ + bucketName: 'SomeBucketName', + objectKey: 'SomeObjectKey', + })); + + // then + test.equal(overrides['BucketNameParam'], 'SomeBucketName'); + test.equal(overrides['ObjectKeyParam'], 'SomeObjectKey'); + + test.done(); + }, + }, }; function defineFunction(code: lambda.Code, runtime: lambda.Runtime = lambda.Runtime.NodeJS810) { diff --git a/packages/@aws-cdk/aws-s3/lib/coordinates.ts b/packages/@aws-cdk/aws-s3/lib/coordinates.ts new file mode 100644 index 0000000000000..6b096762fd573 --- /dev/null +++ b/packages/@aws-cdk/aws-s3/lib/coordinates.ts @@ -0,0 +1,14 @@ +/** + * An interface that represents the coordinates of a specific object in an S3 Bucket. + */ +export interface Coordinates { + /** + * The name of the S3 Bucket the object is in. + */ + readonly bucketName: string; + + /** + * The path inside the Bucket where the object is located at. + */ + readonly objectKey: string; +} diff --git a/packages/@aws-cdk/aws-s3/lib/index.ts b/packages/@aws-cdk/aws-s3/lib/index.ts index 593c797757b3f..ca25e093b7613 100644 --- a/packages/@aws-cdk/aws-s3/lib/index.ts +++ b/packages/@aws-cdk/aws-s3/lib/index.ts @@ -1,5 +1,6 @@ export * from './bucket'; export * from './bucket-policy'; +export * from './coordinates'; export * from './rule'; // AWS::S3 CloudFormation Resources: