diff --git a/packages/@aws-cdk/aws-efs/lib/access-point.ts b/packages/@aws-cdk/aws-efs/lib/access-point.ts index 6fd4b6e927bce..04399991cddb6 100644 --- a/packages/@aws-cdk/aws-efs/lib/access-point.ts +++ b/packages/@aws-cdk/aws-efs/lib/access-point.ts @@ -139,6 +139,11 @@ export class AccessPoint extends Resource implements IAccessPoint { */ public readonly accessPointId: string; + /** + * The filesystem of the access point + */ + public readonly fileSystem: IFileSystem; + constructor(scope: Construct, id: string, props: AccessPointProps) { super(scope, id); @@ -165,5 +170,6 @@ export class AccessPoint extends Resource implements IAccessPoint { resource: 'access-point', resourceName: this.accessPointId, }); + this.fileSystem = props.fileSystem; } } diff --git a/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts b/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts index 35c6ef381a8d5..942be37ca9a3f 100644 --- a/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts +++ b/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts @@ -1,6 +1,6 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as kms from '@aws-cdk/aws-kms'; -import { Construct, IResource, RemovalPolicy, Resource, Size, Tag } from '@aws-cdk/core'; +import { ConcreteDependable, Construct, IDependable, IResource, RemovalPolicy, Resource, Size, Tag } from '@aws-cdk/core'; import { AccessPoint, AccessPointOptions } from './access-point'; import { CfnFileSystem, CfnMountTarget } from './efs.generated'; @@ -83,6 +83,12 @@ export interface IFileSystem extends ec2.IConnectable, IResource { * @attribute */ readonly fileSystemId: string; + + /** + * Dependable that can be depended upon to ensure the mount targets of the filesystem are ready + */ + readonly mountTargetsAvailable: IDependable; + } /** @@ -205,6 +211,7 @@ export class FileSystem extends Resource implements IFileSystem { securityGroups: [attrs.securityGroup], defaultPort: ec2.Port.tcp(FileSystem.DEFAULT_PORT), }); + public readonly mountTargetsAvailable = new ConcreteDependable(); } return new Import(scope, id); @@ -225,6 +232,10 @@ export class FileSystem extends Resource implements IFileSystem { */ public readonly fileSystemId: string; + public readonly mountTargetsAvailable: IDependable; + + private readonly _mountTargetsAvailable = new ConcreteDependable(); + /** * Constructor for creating a new EFS FileSystem. */ @@ -263,15 +274,18 @@ export class FileSystem extends Resource implements IFileSystem { // We now have to create the mount target for each of the mentioned subnet let mountTargetCount = 0; + this.mountTargetsAvailable = []; subnets.subnetIds.forEach((subnetId: string) => { - new CfnMountTarget(this, + const mountTarget = new CfnMountTarget(this, 'EfsMountTarget' + (++mountTargetCount), { fileSystemId: this.fileSystemId, securityGroups: Array.of(securityGroup.securityGroupId), subnetId, }); + this._mountTargetsAvailable.add(mountTarget); }); + this.mountTargetsAvailable = this._mountTargetsAvailable; } /** diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index fb7283b2643d8..55ada8d6da307 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -277,6 +277,46 @@ correct log retention period (never expire, by default). *Further note* that, if the log group already exists and the `logRetention` is not set, the custom resource will reset the log retention to never expire even if it was configured with a different value. +### FileSystem Access + +You can configure a function to mount an Amazon Elastic File System (Amazon EFS) to a +directory in your runtime environment with the `filesystem` property. To access Amaozn EFS +from lambda function, the Amazon EFS access point will be required. + +The following sample allows the lambda function to mount the Amazon EFS access point to `/mnt/msg` in the runtime environment and access the filesystem with the POSIX identity defined in `posixUser`. + +```ts +// create a new Amaozn EFS filesystem +const fileSystem = new efs.FileSystem(stack, 'Efs', { vpc }); + +// create a new access point from the filesystem +const accessPoint = fileSystem.addAccessPoint('AccessPoint', { + // set /export/lambda as the root of the access point + path: '/export/lambda', + // as /export/lambda does not exist in a new efs filesystem, the efs will create the directory with the following createAcl + createAcl: { + ownerUid: '1001', + ownerGid: '1001', + permissions: '750', + }, + // enforce the POSIX identity so lambda function will access with this identity + posixUser: { + uid: '1001', + gid: '1001', + }, +}); + +const fn = new lambda.Function(stack, 'MyLambda', { + code, + handler, + runtime, + vpc, + // mount the access point to /mnt/msg in the lambda runtime enironment + filesystem: lambda.FileSystem.fromEfsAccessPoint(accessPoint, '/mnt/msg'), +}); +``` + + ### Singleton Function The `SingletonFunction` construct is a way to guarantee that a lambda function will be guaranteed to be part of the stack, diff --git a/packages/@aws-cdk/aws-lambda/lib/filesystem.ts b/packages/@aws-cdk/aws-lambda/lib/filesystem.ts new file mode 100644 index 0000000000000..108c7ea4116f7 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/lib/filesystem.ts @@ -0,0 +1,85 @@ +import { Connections } from '@aws-cdk/aws-ec2'; +import * as efs from '@aws-cdk/aws-efs'; +import * as iam from '@aws-cdk/aws-iam'; +import { IDependable, Stack } from '@aws-cdk/core'; + +/** + * FileSystem configurations for the Lambda function + * @experimental + */ +export interface FileSystemConfig { + /** + * mount path in the lambda runtime environment + */ + readonly localMountPath: string; + + /** + * ARN of the access point + */ + readonly arn: string; + + /** + * array of IDependable that lambda function depends on + * + * @default - no dependency + */ + readonly dependency?: IDependable[] + + /** + * connections object used to allow ingress traffic from lambda function + * + * @default - no connections required to add extra ingress rules for Lambda function + */ + readonly connections?: Connections; + + /** + * additional IAM policies required for the lambda function + * + * @default - no additional policies required + */ + readonly policies?: iam.PolicyStatement[]; +} + +/** + * Represents the filesystem for the Lambda function + * @experimental + */ +export class FileSystem { + /** + * mount the filesystem from Amazon EFS + * @param ap the Amazon EFS access point + * @param mountPath the target path in the lambda runtime environment + */ + public static fromEfsAccessPoint(ap: efs.AccessPoint, mountPath: string): FileSystem { + return new FileSystem({ + localMountPath: mountPath, + arn: ap.accessPointArn, + dependency: [ ap.fileSystem.mountTargetsAvailable ], + connections: ap.fileSystem.connections, + policies: [ + new iam.PolicyStatement({ + actions: [ 'elasticfilesystem:ClientMount' ], + resources: [ '*' ], + conditions: { + StringEquals: { + 'elasticfilesystem:AccessPointArn': ap.accessPointArn, + }, + }, + }), + new iam.PolicyStatement({ + actions: ['elasticfilesystem:ClientWrite'], + resources: [ Stack.of(ap).formatArn({ + service: 'elasticfilesystem', + resource: 'file-system', + resourceName: ap.fileSystem.fileSystemId, + }) ], + }), + ], + }); + } + + /** + * @param config the FileSystem configurations for the Lambda function + */ + protected constructor(public readonly config: FileSystemConfig) { } +} diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index d99d1b1ce8377..dbd77a2a9d793 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -7,6 +7,7 @@ import { CfnResource, Construct, Duration, Fn, Lazy, Stack } from '@aws-cdk/core import { Code, CodeConfig } from './code'; import { EventInvokeConfigOptions } from './event-invoke-config'; import { IEventSource } from './event-source'; +import { FileSystem } from './filesystem'; import { FunctionAttributes, FunctionBase, IFunction } from './function-base'; import { calculateFunctionHash, trimFromStart } from './function-hash'; import { Version, VersionOptions } from './lambda-version'; @@ -274,6 +275,13 @@ export interface FunctionProps extends FunctionOptions { * the handler. */ readonly handler: string; + + /** + * The filesystem configuration for the lambda function + * + * @default - will not mount any filesystem + */ + readonly filesystem?: FileSystem; } /** @@ -495,6 +503,16 @@ export class Function extends FunctionBase { }); this.grantPrincipal = this.role; + // add additonal managed policies when necessary + if (props.filesystem) { + const config = props.filesystem.config; + if (config.policies) { + config.policies.forEach(p => { + this.role?.addToPolicy(p); + }); + } + } + for (const statement of (props.initialPolicy || [])) { this.role.addToPolicy(statement); } @@ -570,6 +588,22 @@ export class Function extends FunctionBase { } this.currentVersionOptions = props.currentVersionOptions; + + if (props.filesystem) { + const config = props.filesystem.config; + if (config.dependency) { + this.node.addDependency(...config.dependency); + } + + resource.addPropertyOverride('FileSystemConfigs', + [ + { + LocalMountPath: config.localMountPath, + Arn: config.arn, + }, + ], + ); + } } /** @@ -701,7 +735,7 @@ export class Function extends FunctionBase { // sort environment so the hash of the function used to create // `currentVersion` is not affected by key order (this is how lambda does // it). - const variables: { [key: string]: string } = { }; + const variables: { [key: string]: string } = {}; for (const key of Object.keys(this.environment).sort()) { variables[key] = this.environment[key]; } @@ -745,6 +779,12 @@ export class Function extends FunctionBase { this._connections = new ec2.Connections({ securityGroups }); + if (props.filesystem) { + if (props.filesystem.config.connections) { + props.filesystem.config.connections.allowDefaultPortFrom(this); + } + } + // Pick subnets, make sure they're not Public. Routing through an IGW // won't work because the ENIs don't get a Public IP. // Why are we not simply forcing vpcSubnets? Because you might still be choosing @@ -841,4 +881,4 @@ export function verifyCodeConfig(code: CodeConfig, runtime: Runtime) { if (code.inlineCode && !runtime.supportsInlineCode) { throw new Error(`Inline source not allowed for ${runtime.name}`); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-lambda/lib/index.ts b/packages/@aws-cdk/aws-lambda/lib/index.ts index b494e924c604a..b1d676e234a9b 100644 --- a/packages/@aws-cdk/aws-lambda/lib/index.ts +++ b/packages/@aws-cdk/aws-lambda/lib/index.ts @@ -6,6 +6,7 @@ export * from './layers'; export * from './permission'; export * from './runtime'; export * from './code'; +export * from './filesystem'; export * from './lambda-version'; export * from './singleton-lambda'; export * from './event-source'; diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 4748593961596..115b656e62759 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -85,6 +85,7 @@ "dependencies": { "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-efs": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", @@ -99,6 +100,7 @@ "peerDependencies": { "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-efs": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda/test/integ.lambda.filesystem.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.lambda.filesystem.expected.json new file mode 100644 index 0000000000000..ff7a04f8b7fd5 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.lambda.filesystem.expected.json @@ -0,0 +1,808 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "Efs9E8BF36B": { + "Type": "AWS::EFS::FileSystem", + "Properties": { + "FileSystemTags": [ + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Efs" + } + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "EfsEfsSecurityGroup6F40EA3B": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-cdk-lambda-1/Efs/EfsSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-lambda-1/Efs" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "EfsEfsSecurityGroupfromawscdklambda1MyLambdaSecurityGroup86B085EE20490D9864A8": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "from awscdklambda1MyLambdaSecurityGroup86B085EE:2049", + "FromPort": 2049, + "GroupId": { + "Fn::GetAtt": [ + "EfsEfsSecurityGroup6F40EA3B", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "MyLambdaSecurityGroup1E71A818", + "GroupId" + ] + }, + "ToPort": 2049 + } + }, + "EfsEfsMountTarget195B2DD2E": { + "Type": "AWS::EFS::MountTarget", + "Properties": { + "FileSystemId": { + "Ref": "Efs9E8BF36B" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "EfsEfsSecurityGroup6F40EA3B", + "GroupId" + ] + } + ], + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "EfsEfsMountTarget2315C927F": { + "Type": "AWS::EFS::MountTarget", + "Properties": { + "FileSystemId": { + "Ref": "Efs9E8BF36B" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "EfsEfsSecurityGroup6F40EA3B", + "GroupId" + ] + } + ], + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "EfsEfsMountTarget36646B9A0": { + "Type": "AWS::EFS::MountTarget", + "Properties": { + "FileSystemId": { + "Ref": "Efs9E8BF36B" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "EfsEfsSecurityGroup6F40EA3B", + "GroupId" + ] + } + ], + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "EfsAccessPointE419FED9": { + "Type": "AWS::EFS::AccessPoint", + "Properties": { + "FileSystemId": { + "Ref": "Efs9E8BF36B" + }, + "PosixUser": { + "Gid": "1001", + "Uid": "1001" + }, + "RootDirectory": { + "CreationInfo": { + "OwnerGid": "1001", + "OwnerUid": "1001", + "Permissions": "750" + }, + "Path": "/export/lambda" + } + } + }, + "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" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" + ] + ] + } + ] + }, + "DependsOn": [ + "EfsEfsMountTarget195B2DD2E", + "EfsEfsMountTarget2315C927F", + "EfsEfsMountTarget36646B9A0" + ] + }, + "MyLambdaServiceRoleDefaultPolicy5BBC6F68": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "elasticfilesystem:ClientMount", + "Condition": { + "StringEquals": { + "elasticfilesystem:AccessPointArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":elasticfilesystem:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":access-point/", + { + "Ref": "EfsAccessPointE419FED9" + } + ] + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "elasticfilesystem:ClientWrite", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":elasticfilesystem:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":file-system/", + { + "Ref": "Efs9E8BF36B" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyLambdaServiceRoleDefaultPolicy5BBC6F68", + "Roles": [ + { + "Ref": "MyLambdaServiceRole4539ECB6" + } + ] + }, + "DependsOn": [ + "EfsEfsMountTarget195B2DD2E", + "EfsEfsMountTarget2315C927F", + "EfsEfsMountTarget36646B9A0" + ] + }, + "MyLambdaSecurityGroup1E71A818": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatic security group for Lambda Function awscdklambda1MyLambda82056696", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + }, + "DependsOn": [ + "EfsEfsMountTarget195B2DD2E", + "EfsEfsMountTarget2315C927F", + "EfsEfsMountTarget36646B9A0" + ] + }, + "MyLambdaCCE802FB": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "\nimport json\nimport os\nimport string\nimport random\nimport datetime\n\nMSG_FILE_PATH = '/mnt/msg/content'\n\ndef randomString(stringLength=10):\n letters = string.ascii_lowercase\n return ''.join(random.choice(letters) for i in range(stringLength))\n\ndef lambda_handler(event, context):\n with open(MSG_FILE_PATH, 'a') as f:\n f.write(f\"{datetime.datetime.utcnow():%Y-%m-%d-%H:%M:%S} \" + randomString(5) + ' ')\n\n file = open(MSG_FILE_PATH, \"r\")\n file_content = file.read()\n file.close()\n\n return {\n 'statusCode': 200,\n 'body': str(file_content)\n }\n " + }, + "Handler": "index.lambda_handler", + "Role": { + "Fn::GetAtt": [ + "MyLambdaServiceRole4539ECB6", + "Arn" + ] + }, + "Runtime": "python3.7", + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "MyLambdaSecurityGroup1E71A818", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + }, + "FileSystemConfigs": [ + { + "LocalMountPath": "/mnt/msg", + "Arn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":elasticfilesystem:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":access-point/", + { + "Ref": "EfsAccessPointE419FED9" + } + ] + ] + } + } + ] + }, + "DependsOn": [ + "EfsEfsMountTarget195B2DD2E", + "EfsEfsMountTarget2315C927F", + "EfsEfsMountTarget36646B9A0", + "MyLambdaServiceRoleDefaultPolicy5BBC6F68", + "MyLambdaServiceRole4539ECB6" + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.lambda.filesystem.ts b/packages/@aws-cdk/aws-lambda/test/integ.lambda.filesystem.ts new file mode 100644 index 0000000000000..da6515233e770 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.lambda.filesystem.ts @@ -0,0 +1,68 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as efs from '@aws-cdk/aws-efs'; +import * as cdk from '@aws-cdk/core'; +import * as lambda from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-lambda-1'); + +const vpc = new ec2.Vpc(stack, 'Vpc', { + maxAzs: 3, + natGateways: 1, +}); + +const fileSystem = new efs.FileSystem(stack, 'Efs', { + vpc, + removalPolicy: cdk.RemovalPolicy.DESTROY, +}); + +// create an access point and expose the root of the filesystem +const accessPoint = fileSystem.addAccessPoint('AccessPoint', { + createAcl: { + ownerGid: '1001', + ownerUid: '1001', + permissions: '750', + }, + path: '/export/lambda', + posixUser: { + gid: '1001', + uid: '1001', + }, +}); + +// this function will mount the access point to '/mnt/msg' and write content onto /mnt/msg/content +new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode(` +import json +import os +import string +import random +import datetime + +MSG_FILE_PATH = '/mnt/msg/content' + +def randomString(stringLength=10): + letters = string.ascii_lowercase + return ''.join(random.choice(letters) for i in range(stringLength)) + +def lambda_handler(event, context): + with open(MSG_FILE_PATH, 'a') as f: + f.write(f"{datetime.datetime.utcnow():%Y-%m-%d-%H:%M:%S} " + randomString(5) + ' ') + + file = open(MSG_FILE_PATH, "r") + file_content = file.read() + file.close() + + return { + 'statusCode': 200, + 'body': str(file_content) + } + `), + handler: 'index.lambda_handler', + runtime: lambda.Runtime.PYTHON_3_7, + vpc, + filesystem: lambda.FileSystem.fromEfsAccessPoint(accessPoint, '/mnt/msg'), +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda/test/test.function.ts b/packages/@aws-cdk/aws-lambda/test/test.function.ts index ccdc4fd8ec250..ee4a003b7ad5b 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.function.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.function.ts @@ -1,4 +1,6 @@ -import { expect, haveOutput } from '@aws-cdk/assert'; +import { expect, haveOutput, haveResource } from '@aws-cdk/assert'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as efs from '@aws-cdk/aws-efs'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as sqs from '@aws-cdk/aws-sqs'; @@ -231,4 +233,59 @@ export = testCase({ }, }, + 'filesystem': { + + 'mount efs filesystem'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc', { + maxAzs: 3, + natGateways: 1, + }); + + const fs = new efs.FileSystem(stack, 'Efs', { + vpc, + }); + const accessPoint = fs.addAccessPoint('AccessPoint'); + // WHEN + new lambda.Function(stack, 'MyFunction', { + handler: 'foo', + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromAsset(path.join(__dirname, 'handler.zip')), + filesystem: lambda.FileSystem.fromEfsAccessPoint(accessPoint, '/mnt/msg'), + }); + + // THEN + expect(stack).to(haveResource('AWS::Lambda::Function', { + FileSystemConfigs: [ + { + Arn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':elasticfilesystem:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':access-point/', + { + Ref: 'EfsAccessPointE419FED9', + }, + ], + ], + }, + LocalMountPath: '/mnt/msg', + }], + })); + test.done(); + }, + }, });