diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index 7e7c7ad76ad66..10374daeeb80e 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -33,17 +33,24 @@ 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. +### Layers + +The `lambda.LayerVersion` class can be used to define Lambda layers and manage +granting permissions to other AWS accounts or organizations. + +[Example of Lambda Layer usage](test/integ.layer-version.lit.ts) + ### Event Sources AWS Lambda supports a [variety of event sources](https://docs.aws.amazon.com/lambda/latest/dg/invoking-lambda-function.html). -In most cases, it is possible to trigger a function as a result of an event by -using one of the `onXxx` methods on the source construct. For example, the `s3.Bucket` -construct has an `onEvent` method which can be used to trigger a Lambda when an event, +In most cases, it is possible to trigger a function as a result of an event by +using one of the `onXxx` methods on the source construct. For example, the `s3.Bucket` +construct has an `onEvent` method which can be used to trigger a Lambda when an event, such as PutObject occurs on an S3 bucket. -An alternative way to add event sources to a function is to use `function.addEventSource(source)`. -This method accepts an `IEventSource` object. The module __@aws-cdk/aws-lambda-event-sources__ +An alternative way to add event sources to a function is to use `function.addEventSource(source)`. +This method accepts an `IEventSource` object. The module __@aws-cdk/aws-lambda-event-sources__ includes classes for the various event sources supported by AWS Lambda. For example, the following code adds an SQS queue as an event source for a function: @@ -109,7 +116,7 @@ lambdaAction.outputArtifact('Out2'); // returns the named output Artifact, or th See [the AWS documentation](https://docs.aws.amazon.com/codepipeline/latest/userguide/actions-invoke-lambda-function.html) on how to write a Lambda function invoked from CodePipeline. -### Lambda with DLQ +### Lambda with DLQ ```ts import lambda = require('@aws-cdk/aws-lambda'); @@ -124,7 +131,7 @@ const fn = new lambda.Function(this, 'MyFunction', { See [the AWS documentation](https://docs.aws.amazon.com/lambda/latest/dg/dlq.html) to learn more about AWS Lambdas and DLQs. -### Lambda with X-Ray Tracing +### Lambda with X-Ray Tracing ```ts import lambda = require('@aws-cdk/aws-lambda'); diff --git a/packages/@aws-cdk/aws-lambda/lib/code.ts b/packages/@aws-cdk/aws-lambda/lib/code.ts index ebbd34940d0c3..e252d32fe675a 100644 --- a/packages/@aws-cdk/aws-lambda/lib/code.ts +++ b/packages/@aws-cdk/aws-lambda/lib/code.ts @@ -1,7 +1,7 @@ import assets = require('@aws-cdk/assets'); import s3 = require('@aws-cdk/aws-s3'); +import cdk = require('@aws-cdk/cdk'); import fs = require('fs'); -import { Function as Func } from './lambda'; import { CfnFunction } from './lambda.generated'; export abstract class Code { @@ -50,17 +50,24 @@ export abstract class Code { return new AssetCode(filePath, assets.AssetPackaging.File); } + /** + * Determines whether this Code is inline code or not. + */ + public abstract readonly isInline: boolean; + /** * Called during stack synthesis to render the CodePropery for the * Lambda function. + * + * @param resource the resource to which the code will be attached (a CfnFunction, or a CfnLayerVersion). */ - public abstract toJSON(resource: CfnFunction): CfnFunction.CodeProperty; + public abstract _toJSON(resource?: cdk.Resource): CfnFunction.CodeProperty; /** - * Called when the lambda is initialized to allow this object to + * Called when the lambda or layer is initialized to allow this object to * bind to the stack, add resources and have fun. */ - public bind(_lambda: Func) { + public bind(_construct: cdk.Construct) { return; } } @@ -69,6 +76,7 @@ export abstract class Code { * Lambda code from an S3 archive. */ export class S3Code extends Code { + public readonly isInline = false; private bucketName: string; constructor(bucket: s3.IBucket, private key: string, private objectVersion?: string) { @@ -81,7 +89,7 @@ export class S3Code extends Code { this.bucketName = bucket.bucketName; } - public toJSON(_: CfnFunction): CfnFunction.CodeProperty { + public _toJSON(_?: cdk.Resource): CfnFunction.CodeProperty { return { s3Bucket: this.bucketName, s3Key: this.key, @@ -94,6 +102,8 @@ export class S3Code extends Code { * Lambda code from an inline string (limited to 4KiB). */ export class InlineCode extends Code { + public readonly isInline = true; + constructor(private code: string) { super(); @@ -102,13 +112,14 @@ export class InlineCode extends Code { } } - public bind(lambda: Func) { - if (!lambda.runtime.supportsInlineCode) { - throw new Error(`Inline source not allowed for ${lambda.runtime.name}`); + public bind(construct: cdk.Construct) { + const runtime = (construct as any).runtime; + if (!runtime.supportsInlineCode) { + throw new Error(`Inline source not allowed for ${runtime && runtime.name}`); } } - public toJSON(_: CfnFunction): CfnFunction.CodeProperty { + public _toJSON(_?: cdk.Resource): CfnFunction.CodeProperty { return { zipFile: this.code }; @@ -119,6 +130,8 @@ export class InlineCode extends Code { * Lambda code from a local directory. */ export class AssetCode extends Code { + public readonly isInline = false; + /** * The asset packaging. */ @@ -142,10 +155,10 @@ export class AssetCode extends Code { } } - public bind(lambda: Func) { + public bind(construct: cdk.Construct) { // If the same AssetCode is used multiple times, retain only the first instantiation. if (!this.asset) { - this.asset = new assets.Asset(lambda, 'Code', { + this.asset = new assets.Asset(construct, 'Code', { path: this.path, packaging: this.packaging }); @@ -156,9 +169,11 @@ export class AssetCode extends Code { } } - public toJSON(resource: CfnFunction): CfnFunction.CodeProperty { - // https://github.com/awslabs/aws-cdk/issues/1432 - this.asset!.addResourceMetadata(resource, 'Code'); + public _toJSON(resource?: cdk.Resource): CfnFunction.CodeProperty { + if (resource) { + // https://github.com/awslabs/aws-cdk/issues/1432 + this.asset!.addResourceMetadata(resource, 'Code'); + } return { s3Bucket: this.asset!.s3BucketName, diff --git a/packages/@aws-cdk/aws-lambda/lib/index.ts b/packages/@aws-cdk/aws-lambda/lib/index.ts index 1304dd96c2618..dd5d5fd46e668 100644 --- a/packages/@aws-cdk/aws-lambda/lib/index.ts +++ b/packages/@aws-cdk/aws-lambda/lib/index.ts @@ -1,6 +1,7 @@ export * from './alias'; export * from './lambda-ref'; export * from './lambda'; +export * from './layers'; export * from './permission'; export * from './pipeline-action'; export * from './runtime'; diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda.ts b/packages/@aws-cdk/aws-lambda/lib/lambda.ts index c3ae60f81953d..3bd09a52c14e4 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda.ts @@ -7,6 +7,7 @@ import { Code } from './code'; import { FunctionBase, FunctionImportProps, IFunction } from './lambda-ref'; import { Version } from './lambda-version'; import { CfnFunction } from './lambda.generated'; +import { ILayerVersion } from './layers'; import { Runtime } from './runtime'; /** @@ -173,6 +174,15 @@ export interface FunctionProps { */ tracing?: Tracing; + /** + * A list of layers to add to the function's execution environment. You can configure your Lambda function to pull in + * additional code during initialization in the form of layers. Layers are packages of libraries or other dependencies + * that can be used by mulitple functions. + * + * @default no layers + */ + layers?: ILayerVersion[]; + /** * The maximum of concurrent executions you want to reserve for the function. * @@ -306,6 +316,8 @@ export class Function extends FunctionBase { protected readonly canCreatePermissions = true; + private readonly layers: ILayerVersion[] = []; + /** * Environment variables for this function */ @@ -338,7 +350,8 @@ export class Function extends FunctionBase { const resource = new CfnFunction(this, 'Resource', { functionName: props.functionName, description: props.description, - code: new cdk.Token(() => props.code.toJSON(resource)), + code: new cdk.Token(() => props.code._toJSON(resource)), + layers: new cdk.Token(() => this.layers.length > 0 ? this.layers.map(layer => layer.layerVersionArn) : undefined), handler: props.handler, timeout: props.timeout, runtime: props.runtime.name, @@ -360,6 +373,10 @@ export class Function extends FunctionBase { // allow code to bind to stack. props.code.bind(this); + + for (const layer of props.layers || []) { + this.addLayer(layer); + } } /** @@ -380,12 +397,32 @@ export class Function extends FunctionBase { * @param key The environment variable key. * @param value The environment variable's value. */ - public addEnvironment(key: string, value: any) { + public addEnvironment(key: string, value: any): this { if (!this.environment) { // TODO: add metadata - return; + return this; } this.environment[key] = value; + return this; + } + + /** + * Adds a Lambda Layer to this Lambda function. + * + * @param layer the layer to be added. + * + * @throws if there are already 5 layers on this function, or the layer is incompatible with this function's runtime. + */ + public addLayer(layer: ILayerVersion): this { + if (this.layers.length === 5) { + throw new Error('Unable to add layer: this lambda function already uses 5 layers.'); + } + if (layer.compatibleRuntimes && layer.compatibleRuntimes.indexOf(this.runtime) === -1) { + const runtimes = layer.compatibleRuntimes.map(runtime => runtime.name).join(', '); + throw new Error(`This lambda function uses a runtime that is incompatible with this layer (${this.runtime.name} is not in [${runtimes}])`); + } + this.layers.push(layer); + return this; } /** @@ -542,4 +579,4 @@ export class ImportedFunction extends FunctionBase { */ function extractNameFromArn(arn: string) { return cdk.Fn.select(6, cdk.Fn.split(':', arn)); -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-lambda/lib/layers.ts b/packages/@aws-cdk/aws-lambda/lib/layers.ts new file mode 100644 index 0000000000000..c467a8685159e --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/lib/layers.ts @@ -0,0 +1,239 @@ +import cdk = require('@aws-cdk/cdk'); +import { Code } from './code'; +import { Runtime } from './runtime'; + +export interface LayerVersionProps { + /** + * The runtimes that this layer is compatible with. + * + * @default All runtimes are supported + */ + compatibleRuntimes?: Runtime[]; + + /** + * The content of this Layer. Using *inline* (per ``code.isInline``) code is not permitted. + */ + code: Code; + + /** + * The description the this Lambda Layer. + */ + description?: string; + + /** + * The SPDX licence identifier or URL to the license file for this layer. + * + * @default no license information will be recorded. + */ + license?: string; + + /** + * The name of the layer. + * @default a name will be generated. + */ + name?: string; +} + +export interface ILayerVersion { + /** + * The ARN of the Lambda Layer version that this Layer defines. + */ + readonly layerVersionArn: string; + + /** + * The runtimes compatible with this Layer. + */ + readonly compatibleRuntimes?: Runtime[]; + + /** + * Grants usage of this layer to specific entities. Usage within the same account where the layer is defined is always + * allowed and does not require calling this method. Note that the principal that creates the Lambda function using + * the layer (for example, a CloudFormation changeset execution role) also needs to have the + * ``lambda:GetLayerVersion`` permission on the layer version. + * + * @param id the ID of the grant in the construct tree. + * @param grantee the identification of the grantee. + */ + grantUsage(id: string, grantee: LayerVersionUsageGrantee): ILayerVersion +} + +/** + * A reference to a Lambda Layer version. + */ +export abstract class LayerVersionBase extends cdk.Construct implements ILayerVersion { + public abstract readonly layerVersionArn: string; + public abstract readonly compatibleRuntimes?: Runtime[]; + + public grantUsage(id: string, grantee: LayerVersionUsageGrantee): ILayerVersion { + if (grantee.organizationId != null && grantee.accountId !== '*') { + throw new Error(`OrganizationId can only be specified if AwsAccountId is '*', but it is ${grantee.accountId}`); + } + + new cdk.Resource(this, id, { + type: 'AWS::Lambda::LayerVersionPermission', + properties: { + Action: 'lambda:GetLayerVersion', + LayerVersionArn: this.layerVersionArn, + Principal: grantee.accountId, + OrganizationId: grantee.organizationId, + } + }); + return this; + } + + /** + * Exports this layer for use in another Stack. The resulting object can be passed to the ``LayerVersion.import`` + * function to obtain an ``ILayerVersion`` in the user stack. + */ + public export(): ImportedLayerVersionProps { + return { + layerVersionArn: new cdk.Output(this, 'LayerVersionArn', { value: this.layerVersionArn }).makeImportValue().toString(), + compatibleRuntimes: this.compatibleRuntimes, + }; + } +} + +/** + * Identification of an account (or organization) that is allowed to access a Lambda Layer Version. + */ +export interface LayerVersionUsageGrantee { + /** + * The AWS Account id of the account that is authorized to use a Lambda Layer Version. The wild-card ``'*'`` can be + * used to grant access to "any" account (or any account in an organization when ``organizationId`` is specified). + */ + accountId: string; + + /** + * The ID of the AWS Organization to hwich the grant is restricted. + * + * Can only be specified if ``accountId`` is ``'*'`` + */ + organizationId?: string; +} + +/** + * Properties necessary to import a LayerVersion. + */ +export interface ImportedLayerVersionProps { + /** + * The ARN of the LayerVersion. + */ + layerVersionArn: string; + + /** + * The list of compatible runtimes with this Layer. + */ + compatibleRuntimes?: Runtime[]; +} + +/** + * Defines a new Lambda Layer version. + */ +export class LayerVersion extends LayerVersionBase { + /** + * Imports a Layer that has been defined externally. + * + * @param parent the parent Construct that will use the imported layer. + * @param id the id of the imported layer in the construct tree. + * @param props the properties of the imported layer. + */ + public static import(parent: cdk.Construct, id: string, props: ImportedLayerVersionProps): ILayerVersion { + return new ImportedLayerVersion(parent, id, props); + } + + public readonly layerVersionArn: string; + public readonly compatibleRuntimes?: Runtime[]; + + constructor(scope: cdk.Construct, id: string, props: LayerVersionProps) { + super(scope, id); + if (props.compatibleRuntimes && props.compatibleRuntimes.length === 0) { + throw new Error('Attempted to define a Lambda layer that supports no runtime!'); + } + if (props.code.isInline) { + throw new Error('Lambda layers cannot be created from inline code'); + } + // Allow usage of the code in this context... + props.code.bind(this); + + const resource = new cdk.Resource(this, 'Resource', { + type: 'AWS::Lambda::LayerVersion', + properties: { + CompatibleRuntimes: props.compatibleRuntimes && props.compatibleRuntimes.map(r => r.name), + Content: new cdk.Token(() => props.code._toJSON(resource)), + Description: props.description, + LayerName: props.name, + LicenseInfo: props.license, + } + }); + + this.layerVersionArn = resource.ref; + this.compatibleRuntimes = props.compatibleRuntimes; + } +} + +class ImportedLayerVersion extends LayerVersionBase { + public readonly layerVersionArn: string; + public readonly compatibleRuntimes?: Runtime[]; + + public constructor(parent: cdk.Construct, id: string, props: ImportedLayerVersionProps) { + super(parent, id); + + if (props.compatibleRuntimes && props.compatibleRuntimes.length === 0) { + throw new Error('Attempted to import a Lambda layer that supports no runtime!'); + } + + this.layerVersionArn = props.layerVersionArn; + this.compatibleRuntimes = props.compatibleRuntimes; + } +} + +/** + * Properties of a Singleton Lambda Layer Version. + */ +export interface SingletonLayerVersionProps extends LayerVersionProps { + /** + * A unique identifier to identify this lambda layer version. + * + * The identifier should be unique across all layer providers. + * We recommend generating a UUID per provider. + */ + uuid: string; +} + +/** + * A Singleton Lambda Layer Version. The construct gurantees exactly one LayerVersion will be created in a given Stack + * for the provided ``uuid``. It is recommended to use ``uuidgen`` to create a new ``uuid`` each time a new singleton + * layer is created. + */ +export class SingletonLayerVersion extends cdk.Construct implements ILayerVersion { + private readonly layerVersion: ILayerVersion; + + constructor(scope: cdk.Construct, id: string, props: SingletonLayerVersionProps) { + super(scope, id); + + this.layerVersion = this.ensureLayerVersion(props); + } + + public get layerVersionArn(): string { + return this.layerVersion.layerVersionArn; + } + + public get compatibleRuntimes(): Runtime[] | undefined { + return this.layerVersion.compatibleRuntimes; + } + + public grantUsage(id: string, grantee: LayerVersionUsageGrantee): ILayerVersion { + this.layerVersion.grantUsage(id, grantee); + return this; + } + + private ensureLayerVersion(props: SingletonLayerVersionProps): ILayerVersion { + const singletonId = `SingletonLayer-${props.uuid}`; + const stack = cdk.Stack.find(this); + const existing = stack.node.tryFindChild(singletonId); + if (existing) { + return existing as unknown as ILayerVersion; + } + return new LayerVersion(stack, singletonId, props); + } +} diff --git a/packages/@aws-cdk/aws-lambda/lib/runtime.ts b/packages/@aws-cdk/aws-lambda/lib/runtime.ts index 1cf5cfc559668..e5a4ab552996c 100644 --- a/packages/@aws-cdk/aws-lambda/lib/runtime.ts +++ b/packages/@aws-cdk/aws-lambda/lib/runtime.ts @@ -22,6 +22,9 @@ export enum RuntimeFamily { * can instantiate a `Runtime` object, e.g: `new Runtime('nodejs99.99')`. */ export class Runtime { + /** A list of all the know ``Runtime``s. */ + public static readonly All = new Array(); + public static readonly NodeJS = new Runtime('nodejs', RuntimeFamily.NodeJS, { supportsInlineCode: true }); public static readonly NodeJS43 = new Runtime('nodejs4.3', RuntimeFamily.NodeJS, { supportsInlineCode: true }); public static readonly NodeJS610 = new Runtime('nodejs6.10', RuntimeFamily.NodeJS, { supportsInlineCode: true }); @@ -56,6 +59,8 @@ export class Runtime { this.name = name; this.supportsInlineCode = !!props.supportsInlineCode; this.family = family; + + Runtime.All.push(this); } public toString(): string { diff --git a/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.expected.json new file mode 100644 index 0000000000000..c75ea2f19d958 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.expected.json @@ -0,0 +1,129 @@ +{ + "Parameters": { + "MyLayerCodeS3Bucket20DB8EB9": { + "Type": "String", + "Description": "S3 bucket for asset \"aws-cdk-layer-version-1/MyLayer/Code\"" + }, + "MyLayerCodeS3VersionKeyA45254EC": { + "Type": "String", + "Description": "S3 key for asset version \"aws-cdk-layer-version-1/MyLayer/Code\"" + } + }, + "Resources": { + "MyLayer38944FA5": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "CompatibleRuntimes": [ + "nodejs8.10" + ], + "Content": { + "s3Bucket": { + "Ref": "MyLayerCodeS3Bucket20DB8EB9" + }, + "s3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "MyLayerCodeS3VersionKeyA45254EC" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "MyLayerCodeS3VersionKeyA45254EC" + } + ] + } + ] + } + ] + ] + } + }, + "Description": "A layer to test the L2 construct", + "LicenseInfo": "Apache-2.0" + } + }, + "MyLayerremoteaccountgrant715E5D21": { + "Type": "AWS::Lambda::LayerVersionPermission", + "Properties": { + "Action": "lambda:GetLayerVersion", + "LayerVersionArn": { + "Ref": "MyLayer38944FA5" + }, + "Principal": { + "Ref": "AWS::AccountId" + } + } + }, + "MyLayeredLambdaServiceRole1A7DC118": { + "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" + ] + ] + } + ] + } + }, + "MyLayeredLambda9A3008D1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "foo" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyLayeredLambdaServiceRole1A7DC118", + "Arn" + ] + }, + "Runtime": "nodejs8.10", + "Layers": [ + { + "Ref": "MyLayer38944FA5" + } + ] + }, + "DependsOn": [ + "MyLayeredLambdaServiceRole1A7DC118" + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.ts b/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.ts new file mode 100644 index 0000000000000..de01eb0c1d5f0 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.ts @@ -0,0 +1,34 @@ +import cdk = require('@aws-cdk/cdk'); +import path = require('path'); +import lambda = require('../lib'); + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-layer-version-1'); + +// Just for the example - granting to the current account is not necessary. +const awsAccountId = stack.accountId; + +/// !show +const layer = new lambda.LayerVersion(stack, 'MyLayer', { + code: lambda.Code.directory(path.join(__dirname, 'layer-code')), + compatibleRuntimes: [lambda.Runtime.NodeJS810], + license: 'Apache-2.0', + description: 'A layer to test the L2 construct', +}); + +// To grant usage by other AWS accounts +layer.grantUsage('remote-account-grant', { accountId: awsAccountId }); + +// To grant usage to all accounts in some AWS Ogranization +// layer.grantUsage({ accountId: '*', organizationId }); + +new lambda.Function(stack, 'MyLayeredLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810, + layers: [layer], +}); +/// !hide + +app.run(); diff --git a/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.expected.json index 3325043c8414e..f432ae88789d3 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.expected.json @@ -67,9 +67,6 @@ }, "VPCPublicSubnet1DefaultRoute91CEF279": { "Type": "AWS::EC2::Route", - "DependsOn": [ - "VPCVPCGW99B986DC" - ], "Properties": { "RouteTableId": { "Ref": "VPCPublicSubnet1RouteTableFEE4B781" @@ -78,7 +75,10 @@ "GatewayId": { "Ref": "VPCIGWB7E252D3" } - } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] }, "VPCPublicSubnet1EIP6AD938E8": { "Type": "AWS::EC2::EIP", @@ -158,9 +158,6 @@ }, "VPCPublicSubnet2DefaultRouteB7481BBA": { "Type": "AWS::EC2::Route", - "DependsOn": [ - "VPCVPCGW99B986DC" - ], "Properties": { "RouteTableId": { "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" @@ -169,7 +166,10 @@ "GatewayId": { "Ref": "VPCIGWB7E252D3" } - } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] }, "VPCPublicSubnet2EIP4947BC00": { "Type": "AWS::EC2::EIP", diff --git a/packages/@aws-cdk/aws-lambda/test/layer-code/layer.ts b/packages/@aws-cdk/aws-lambda/test/layer-code/layer.ts new file mode 100644 index 0000000000000..de36deb9a37a2 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/layer-code/layer.ts @@ -0,0 +1,3 @@ +export async function main(_event: any, _context: any) { + return 'Done!'; +} diff --git a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts index 6e99777ce4c84..370453b926a1b 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts @@ -1116,6 +1116,47 @@ export = { DependsOn: [ 'MyLambdaServiceRole4539ECB6' ] } } }); test.done(); }, + + 'using an incompatible layer'(test: Test) { + // GIVEN + const stack = new cdk.Stack(undefined, 'TestStack'); + const layer = lambda.LayerVersion.import(stack, 'TestLayer', { + layerVersionArn: 'arn:aws:...', + compatibleRuntimes: [lambda.Runtime.NodeJS810], + }); + + // THEN + test.throws(() => new lambda.Function(stack, 'Function', { + layers: [layer], + runtime: lambda.Runtime.NodeJS610, + code: lambda.Code.inline('exports.main = function() { console.log("DONE"); }'), + handler: 'index.main' + }), + /nodejs6.10 is not in \[nodejs8.10\]/); + + test.done(); + }, + + 'using more than 5 layers'(test: Test) { + // GIVEN + const stack = new cdk.Stack(undefined, 'TestStack'); + const layers = new Array(6).fill(lambda.LayerVersion.import(stack, 'TestLayer', { + layerVersionArn: 'arn:aws:...', + compatibleRuntimes: [lambda.Runtime.NodeJS810], + })); + + // THEN + test.throws(() => new lambda.Function(stack, 'Function', { + layers, + runtime: lambda.Runtime.NodeJS810, + code: lambda.Code.inline('exports.main = function() { console.log("DONE"); }'), + handler: 'index.main' + }), + /Unable to add layer:/); + + test.done(); + }, + 'support reserved concurrent executions'(test: Test) { const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-lambda/test/test.layers.ts b/packages/@aws-cdk/aws-lambda/test/test.layers.ts new file mode 100644 index 0000000000000..3629b3c33399d --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/test.layers.ts @@ -0,0 +1,89 @@ +import { countResources, expect, haveResource } from '@aws-cdk/assert'; +import s3 = require('@aws-cdk/aws-s3'); +import cdk = require('@aws-cdk/cdk'); +import { Test, testCase } from 'nodeunit'; +import lambda = require('../lib'); + +export = testCase({ + 'creating a layer'(test: Test) { + // GIVEN + const stack = new cdk.Stack(undefined, 'TestStack'); + const bucket = new s3.Bucket(stack, 'Bucket'); + const code = new lambda.S3Code(bucket, 'ObjectKey'); + + // WHEN + new lambda.LayerVersion(stack, 'LayerVersion', { + code, + compatibleRuntimes: [lambda.Runtime.NodeJS810] + }); + + // THEN + expect(stack).to(haveResource('AWS::Lambda::LayerVersion', { + Content: stack.node.resolve(code._toJSON()), + CompatibleRuntimes: ['nodejs8.10'] + })); + + test.done(); + }, + + 'granting access to a layer'(test: Test) { + // GIVEN + const stack = new cdk.Stack(undefined, 'TestStack'); + const bucket = new s3.Bucket(stack, 'Bucket'); + const code = new lambda.S3Code(bucket, 'ObjectKey'); + const layer = new lambda.LayerVersion(stack, 'LayerVersion', { + code, + compatibleRuntimes: [lambda.Runtime.NodeJS810] + }); + + // WHEN + layer.grantUsage('GrantUsage-123456789012', { accountId: '123456789012' }); + layer.grantUsage('GrantUsage-o-123456', { accountId: '*', organizationId: 'o-123456' }); + + // THEN + expect(stack).to(haveResource('AWS::Lambda::LayerVersionPermission', { + Action: 'lambda:GetLayerVersion', + LayerVersionArn: stack.node.resolve(layer.layerVersionArn), + Principal: '123456789012', + })); + expect(stack).to(haveResource('AWS::Lambda::LayerVersionPermission', { + Action: 'lambda:GetLayerVersion', + LayerVersionArn: stack.node.resolve(layer.layerVersionArn), + Principal: '*', + OrganizationId: 'o-123456' + })); + + test.done(); + }, + + 'creating a layer with no runtimes compatible'(test: Test) { + // GIVEN + const stack = new cdk.Stack(undefined, 'TestStack'); + const bucket = new s3.Bucket(stack, 'Bucket'); + const code = new lambda.S3Code(bucket, 'ObjectKey'); + + // THEN + test.throws(() => new lambda.LayerVersion(stack, 'LayerVersion', { code, compatibleRuntimes: [] }), + /supports no runtime/); + + test.done(); + }, + + 'singleton layers are created exactly once'(test: Test) { + // Given + const stack = new cdk.Stack(undefined, 'TestStack'); + const uuid = '75F9D74A-67AF-493E-888A-20976130F0B1'; + const bucket = new s3.Bucket(stack, 'Bucket'); + const code = new lambda.S3Code(bucket, 'ObjectKey'); + + // When + for (let i = 0 ; i < 5 ; i++) { + new lambda.SingletonLayerVersion(stack, `Layer-${i}`, { uuid, code }); + } + + // Then + expect(stack).to(countResources('AWS::Lambda::LayerVersion', 1)); + + test.done(); + } +});