diff --git a/packages/@aws-cdk/lambda/README.md b/packages/@aws-cdk/lambda/README.md index e7e79008e97b5..4ee34bb2cb4c5 100644 --- a/packages/@aws-cdk/lambda/README.md +++ b/packages/@aws-cdk/lambda/README.md @@ -1,55 +1,33 @@ ## AWS Lambda Construct Library +This construct library allows you to define AWS Lambda functions. + ```ts const fn = new Lambda(this, 'MyFunction', { - runtime: LambdaRuntime.DOTNETCORE_2, - code: new LambdaS3Code(bucket, 'myKey'), + runtime: LambdaRuntime.NodeJS810, handler: 'index.handler' + code: LambdaCode.inline('exports.handler = function(event, ctx, cb) { return cb(null, "hi"); }'), }); - -fn.role.addToPolicy(new PolicyStatement()....); ``` -### Inline node.js Lambda Functions - -The subclass called `LambdaInlineNodeJS` allows embedding the function's handler -as a JavaScript function within the construct code. +### Handler Code -The following example defines a node.js Lambda and an S3 bucket. When invoked, -a file named "myfile.txt" will be uploaded to the bucket with the string "hello, world". +The `LambdaCode` class includes static convenience methods for various types of +runtime code. -A few things to note: + * `LambdaCode.bucket(bucket, key[, objectVersion])` - specify an S3 object that + contains the archive of your runtime code. + * `LambdaCode.inline(code)` - inline the handle code as a string. This is + limited to 4KB. The class `InlineJavaScriptLambda` can be used to simplify + inlining JavaScript functions. + * `LambdaCode.asset(directory)` - specify a directory in the local filesystem + which will be zipped and uploaded to S3 before deployment. - - The function's execution role is granted read/write permissions on the - bucket. - - The require statement for `aws-sdk` is invoked within the function's body. Due to - node.js' module caching, this is equivalent in performance to requiring - outside. - - The bucket name is passed to the function via the environment variable - `BUCKET_NAME`. +The following example shows how to define a Python function and deploy the code from the +local directory `my-lambda-handler` to it: -```ts -const bucket = new Bucket(this, 'MyBucket'); - -const lambda = new InlineJavaScriptLambda(this, 'MyLambda', { - environment: { - BUCKET_NAME: bucket.bucketName - }, - handler: { - fn: (_event: any, _context: any, callback: any) => { - const S3 = require('aws-sdk').S3; - const s3 = new S3(); - const bucketName = process.env.BUCKET_NAME; - s3.upload({ Bucket: bucketName, Key: 'myfile.txt', Body: 'Hello, world' }, (err, data) => { - if (err) { - return callback(err); - } - console.log(data); - return callback(); - }); - } - } -}); +[Example of Lambda Code from Local Assets](test/integ.assets.lit.ts) -bucket.grantReadWrite(lambda.role); -``` +When deploying a stack that contains this code, the directory will be zip +archived and then uploaded to an S3 bucket, then the exact location of the S3 +objects will be passed when the stack is deployed. diff --git a/packages/@aws-cdk/lambda/lib/code.ts b/packages/@aws-cdk/lambda/lib/code.ts index 5cca73fdf4f8d..42a61251b0ee3 100644 --- a/packages/@aws-cdk/lambda/lib/code.ts +++ b/packages/@aws-cdk/lambda/lib/code.ts @@ -1,15 +1,58 @@ -import { BucketName, BucketRef } from '@aws-cdk/s3'; +import assets = require('@aws-cdk/cdk-assets'); +import s3 = require('@aws-cdk/s3'); +import { Lambda } from './lambda'; import { cloudformation } from './lambda.generated'; -import { LambdaRuntime } from './runtime'; export abstract class LambdaCode { - public abstract toJSON(runtime: LambdaRuntime): cloudformation.FunctionResource.CodeProperty; + /** + * @returns `LambdaCodeS3` associated with the specified S3 object. + * @param bucket The S3 bucket + * @param key The object key + * @param objectVersion Optional S3 object version + */ + public static bucket(bucket: s3.BucketRef, key: string, objectVersion?: string) { + return new LambdaS3Code(bucket, key, objectVersion); + } + + /** + * @returns `LambdaCodeInline` with inline code. + * @param code The actual handler code (limited to 4KiB) + */ + public static inline(code: string) { + return new LambdaInlineCode(code); + } + + /** + * @returns `LambdaCodeAsset` + * @param directoryToZip + */ + public static asset(directoryToZip: string) { + return new LambdaAssetCode(directoryToZip); + } + + /** + * Called during stack synthesis to render the CodePropery for the + * Lambda function. + */ + + public abstract toJSON(): cloudformation.FunctionResource.CodeProperty; + + /** + * Called when the lambda is initialized to allow this object to + * bind to the stack, add resources and have fun. + */ + public bind(_parent: Lambda) { + return; + } } +/** + * Lambda code from an S3 archive. + */ export class LambdaS3Code extends LambdaCode { - private bucketName: BucketName; + private bucketName: s3.BucketName; - constructor(bucket: BucketRef, private key: string, private objectVersion?: string) { + constructor(bucket: s3.BucketRef, private key: string, private objectVersion?: string) { super(); if (!bucket.bucketName) { @@ -19,7 +62,7 @@ export class LambdaS3Code extends LambdaCode { this.bucketName = bucket.bucketName; } - public toJSON(_runtime: LambdaRuntime): cloudformation.FunctionResource.CodeProperty { + public toJSON(): cloudformation.FunctionResource.CodeProperty { return { s3Bucket: this.bucketName, s3Key: this.key, @@ -28,6 +71,9 @@ export class LambdaS3Code extends LambdaCode { } } +/** + * Lambda code from an inline string (limited to 4KiB). + */ export class LambdaInlineCode extends LambdaCode { constructor(private code: string) { super(); @@ -37,13 +83,39 @@ export class LambdaInlineCode extends LambdaCode { } } - public toJSON(runtime: LambdaRuntime): cloudformation.FunctionResource.CodeProperty { - if (!runtime.supportsInlineCode) { - throw new Error(`Inline source not supported for: ${runtime.name}`); + public bind(parent: Lambda) { + if (!parent.runtime.supportsInlineCode) { + throw new Error(`Inline source not allowed for ${parent.runtime.name}`); } + } + + public toJSON(): cloudformation.FunctionResource.CodeProperty { return { zipFile: this.code }; } } + +/** + * Lambda code from a local directory. + */ +export class LambdaAssetCode extends LambdaCode { + private asset?: assets.Asset; + + constructor(private readonly directory: string) { + super(); + } + + public bind(parent: Lambda) { + this.asset = new assets.ZipDirectoryAsset(parent, 'Code', { path: this.directory }); + this.asset.grantRead(parent.role); + } + + public toJSON(): cloudformation.FunctionResource.CodeProperty { + return { + s3Bucket: this.asset!.s3BucketName, + s3Key: this.asset!.s3ObjectKey + }; + } +} diff --git a/packages/@aws-cdk/lambda/lib/lambda.ts b/packages/@aws-cdk/lambda/lib/lambda.ts index 333f84b7be910..b524a0db4bcd8 100644 --- a/packages/@aws-cdk/lambda/lib/lambda.ts +++ b/packages/@aws-cdk/lambda/lib/lambda.ts @@ -118,6 +118,16 @@ export class Lambda extends LambdaRef { */ public readonly role?: Role; + /** + * The runtime configured for this lambda. + */ + public readonly runtime: LambdaRuntime; + + /** + * The name of the handler configured for this lambda. + */ + public readonly handler: string; + protected readonly canCreatePermissions = true; /** @@ -149,7 +159,7 @@ export class Lambda extends LambdaRef { const resource = new cloudformation.FunctionResource(this, 'Resource', { functionName: props.functionName, description: props.description, - code: props.code.toJSON(props.runtime), + code: new Token(() => props.code.toJSON()), handler: props.handler, timeout: props.timeout, runtime: props.runtime.name, @@ -162,6 +172,11 @@ export class Lambda extends LambdaRef { this.functionName = resource.ref; this.functionArn = resource.functionArn; + this.handler = props.handler; + this.runtime = props.runtime; + + // allow code to bind to stack. + props.code.bind(this); } /** diff --git a/packages/@aws-cdk/lambda/package.json b/packages/@aws-cdk/lambda/package.json index e7bb2a06df201..9d7d894dddfe1 100644 --- a/packages/@aws-cdk/lambda/package.json +++ b/packages/@aws-cdk/lambda/package.json @@ -47,9 +47,11 @@ "dependencies": { "@aws-cdk/cloudwatch": "^0.7.3-beta", "@aws-cdk/core": "^0.7.3-beta", + "@aws-cdk/cdk-assets": "^0.7.3-beta", "@aws-cdk/events": "^0.7.3-beta", "@aws-cdk/iam": "^0.7.3-beta", "@aws-cdk/s3": "^0.7.3-beta", - "@aws-cdk/logs": "^0.7.3-beta" + "@aws-cdk/logs": "^0.7.3-beta", + "@aws-cdk/cx-api": "^0.7.3-beta" } } diff --git a/packages/@aws-cdk/lambda/test/integ.assets.lit.expected.json b/packages/@aws-cdk/lambda/test/integ.assets.lit.expected.json new file mode 100644 index 0000000000000..97e93682df598 --- /dev/null +++ b/packages/@aws-cdk/lambda/test/integ.assets.lit.expected.json @@ -0,0 +1,161 @@ +{ + "Resources": { + "MyLambdaServiceRole4539ECB6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "MyLambdaServiceRoleDefaultPolicy5BBC6F68": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "s3", + ":", + "", + ":", + "", + ":", + { + "Ref": "MyLambdaCodeS3BucketC82A5870" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "s3", + ":", + "", + ":", + "", + ":", + { + "Ref": "MyLambdaCodeS3BucketC82A5870" + } + ] + ] + }, + "/", + { + "Ref": "MyLambdaCodeS3ObjectKeyA7272AC7" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyLambdaServiceRoleDefaultPolicy5BBC6F68", + "Roles": [ + { + "Ref": "MyLambdaServiceRole4539ECB6" + } + ] + } + }, + "MyLambdaCCE802FB": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "MyLambdaCodeS3BucketC82A5870" + }, + "S3Key": { + "Ref": "MyLambdaCodeS3ObjectKeyA7272AC7" + } + }, + "Handler": "index.main", + "Role": { + "Fn::GetAtt": [ + "MyLambdaServiceRole4539ECB6", + "Arn" + ] + }, + "Runtime": "python3.6" + }, + "DependsOn": [ + "MyLambdaServiceRole4539ECB6", + "MyLambdaServiceRoleDefaultPolicy5BBC6F68" + ] + } + }, + "Parameters": { + "MyLambdaCodeS3BucketC82A5870": { + "Type": "String", + "Description": "S3 bucket for asset \"lambda-test-assets/MyLambda/Code\"" + }, + "MyLambdaCodeS3ObjectKeyA7272AC7": { + "Type": "String", + "Description": "S3 object for asset \"lambda-test-assets/MyLambda/Code\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/lambda/test/integ.assets.lit.ts b/packages/@aws-cdk/lambda/test/integ.assets.lit.ts new file mode 100644 index 0000000000000..d4278625aa101 --- /dev/null +++ b/packages/@aws-cdk/lambda/test/integ.assets.lit.ts @@ -0,0 +1,23 @@ +import cdk = require('@aws-cdk/core'); +import path = require('path'); +import { Lambda, LambdaCode, LambdaRuntime } from '../lib'; + +class TestStack extends cdk.Stack { + constructor(parent: cdk.App, id: string) { + super(parent, id); + + /// !show + new Lambda(this, 'MyLambda', { + code: LambdaCode.asset(path.join(__dirname, 'my-lambda-handler')), + handler: 'index.main', + runtime: LambdaRuntime.Python36 + }); + /// !hide + } +} + +const app = new cdk.App(process.argv); + +new TestStack(app, 'lambda-test-assets'); + +process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/@aws-cdk/lambda/test/integ.inline.expected.json b/packages/@aws-cdk/lambda/test/integ.inline.expected.json index 394c36a2e1a47..2a75a694998ac 100644 --- a/packages/@aws-cdk/lambda/test/integ.inline.expected.json +++ b/packages/@aws-cdk/lambda/test/integ.inline.expected.json @@ -100,13 +100,6 @@ "Code": { "ZipFile": "exports.handler = (_event, _context, callback) => {\n // tslint:disable:no-console\n const S3 = require('aws-sdk').S3;\n const s3 = new S3();\n const bucketName = process.env.BUCKET_NAME;\n const req = {\n Bucket: bucketName,\n Key: 'myfile.txt',\n Body: 'Hello, world'\n };\n return s3.upload(req, (err, data) => {\n if (err) {\n return callback(err);\n }\n console.log(data);\n return callback();\n });\n }" }, - "Environment": { - "Variables": { - "BUCKET_NAME": { - "Ref": "MyBucketF68F3FF0" - } - } - }, "Handler": "index.handler", "Role": { "Fn::GetAtt": [ @@ -115,6 +108,13 @@ ] }, "Runtime": "nodejs6.10", + "Environment": { + "Variables": { + "BUCKET_NAME": { + "Ref": "MyBucketF68F3FF0" + } + } + }, "Timeout": 30 }, "DependsOn": [ diff --git a/packages/@aws-cdk/lambda/test/my-lambda-handler/index.py b/packages/@aws-cdk/lambda/test/my-lambda-handler/index.py new file mode 100644 index 0000000000000..179dcbbb27423 --- /dev/null +++ b/packages/@aws-cdk/lambda/test/my-lambda-handler/index.py @@ -0,0 +1,4 @@ +def main(event, context): + return { + 'message': 'Hello, world!' + } \ No newline at end of file diff --git a/packages/@aws-cdk/lambda/test/test.lambda.ts b/packages/@aws-cdk/lambda/test/test.lambda.ts index b3488e75df5fb..a95c85e4526a6 100644 --- a/packages/@aws-cdk/lambda/test/test.lambda.ts +++ b/packages/@aws-cdk/lambda/test/test.lambda.ts @@ -1,9 +1,10 @@ import { expect, haveResource } from '@aws-cdk/assert'; -import { AccountPrincipal, Arn, ArnPrincipal, AwsAccountId, Construct, PolicyStatement, ServicePrincipal, Stack } from '@aws-cdk/core'; +import { AccountPrincipal, Arn, ArnPrincipal, AwsAccountId, Construct, PolicyStatement, ServicePrincipal, Stack, PATH_SEP } from '@aws-cdk/core'; import { EventRule } from '@aws-cdk/events'; import { Role } from '@aws-cdk/iam'; import { Test } from 'nodeunit'; -import { Lambda, LambdaInlineCode, LambdaRef, LambdaRuntime } from '../lib'; +import { Lambda, LambdaInlineCode, LambdaRef, LambdaRuntime, LambdaCode } from '../lib'; +import path = require('path'); // tslint:disable:object-literal-key-quotes @@ -277,6 +278,38 @@ export = { ] })); + test.done(); + }, + + 'Lambda code can be read from a local directory via an asset'(test: Test) { + // GIVEN + const stack = new Stack(); + new Lambda(stack, 'MyLambda', { + code: LambdaCode.asset(path.join(__dirname, 'my-lambda-handler')), + handler: 'index.handler', + runtime: LambdaRuntime.Python36 + }); + + // THEN + expect(stack).to(haveResource('AWS::Lambda::Function', { + "Code": { + "S3Bucket": { + "Ref": "MyLambdaCodeS3BucketC82A5870" + }, + "S3Key": { + "Ref": "MyLambdaCodeS3ObjectKeyA7272AC7" + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyLambdaServiceRole4539ECB6", + "Arn" + ] + }, + "Runtime": "python3.6" + })); + test.done(); } };