diff --git a/lambda-s3-files-cdk/.gitignore b/lambda-s3-files-cdk/.gitignore new file mode 100644 index 000000000..2303e9b95 --- /dev/null +++ b/lambda-s3-files-cdk/.gitignore @@ -0,0 +1,6 @@ +node_modules +cdk.out +*.js +!src/**/*.js +*.d.ts +package-lock.json diff --git a/lambda-s3-files-cdk/README.md b/lambda-s3-files-cdk/README.md new file mode 100644 index 000000000..8389befb9 --- /dev/null +++ b/lambda-s3-files-cdk/README.md @@ -0,0 +1,134 @@ +# Lambda with Amazon S3 Files Mount + +This pattern deploys a Lambda function with an Amazon S3 Files file system mounted as a local directory, enabling standard file operations on S3 data without downloading objects. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/lambda-s3-files-cdk + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +- [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +- [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- [Node and NPM](https://nodejs.org/en/download/) installed +- [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) installed + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ```bash + git clone https://github.com/aws-samples/serverless-patterns + ``` +2. Change directory to the pattern directory: + ```bash + cd serverless-patterns/lambda-s3-files-cdk + ``` +3. Install CDK dependencies: + ```bash + npm install + ``` +4. Deploy the stack: + ```bash + cdk deploy + ``` + +## How it works + +[Amazon S3 Files](https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files.html) provides NFS access to S3 buckets with full POSIX semantics. This pattern mounts an S3 bucket on a Lambda function at `/mnt/s3data`. + +### What gets deployed + +| Resource | Purpose | +|---|---| +| S3 Bucket | Data store backing the file system | +| VPC (2 AZs) | Network for Lambda and mount targets | +| S3 Files FileSystem | NFS file system linked to the S3 bucket | +| S3 Files MountTargets | Network endpoints in each private subnet | +| S3 Files AccessPoint | Application entry point (UID/GID 1000, root `/lambda`) | +| Security Group | Allows NFS traffic (port 2049) | +| Lambda Function | Reads, writes, and lists files via the mount | + +### Architecture + +``` +┌──────────┐ ┌─────────────────────────────────────────┐ +│ S3 Bucket│◄───►│ S3 Files FileSystem │ +└──────────┘ │ (auto-sync between S3 and filesystem) │ + └──────────────┬────────────────────────────┘ + │ NFS (port 2049) + ┌──────────────┴────────────────────────────┐ + │ VPC │ + │ ┌────────────────┐ ┌────────────────┐ │ + │ │ Mount Target │ │ Mount Target │ │ + │ │ (AZ-1) │ │ (AZ-2) │ │ + │ └────────────────┘ └────────────────┘ │ + │ ▲ │ + │ │ │ + │ ┌────────┴───────┐ │ + │ │ Lambda Function│ │ + │ │ /mnt/s3data │ │ + │ └────────────────┘ │ + └────────────────────────────────────────────┘ +``` + +### Key S3 Files concepts + +- **FileSystem** — A shared file system linked to your S3 bucket. Changes sync bidirectionally. +- **MountTarget** — Network endpoint in a specific AZ. Lambda must be in the same VPC/subnet. +- **AccessPoint** — Application-specific entry point with POSIX user identity and root directory. +- **High-performance storage** — Actively used data cached locally for sub-millisecond latency. + +## Testing + +1. After deployment, note the `FunctionName` and `BucketName` outputs. + +2. **Write a file** through the Lambda mount: + ```bash + aws lambda invoke \ + --function-name \ + --payload '{"action": "write", "filename": "hello.txt", "content": "Hello from Lambda via S3 Files!"}' \ + --cli-binary-format raw-in-base64-out \ + output.json + + cat output.json + ``` + +3. **Verify the file appeared in S3** (sync takes ~1 minute): + ```bash + aws s3 ls s3:///lambda/ + ``` + +4. **Read the file** back through Lambda: + ```bash + aws lambda invoke \ + --function-name \ + --payload '{"action": "read", "filename": "hello.txt"}' \ + --cli-binary-format raw-in-base64-out \ + output.json + + cat output.json + ``` + +5. **List directory** contents: + ```bash + aws lambda invoke \ + --function-name \ + --payload '{"action": "list"}' \ + --cli-binary-format raw-in-base64-out \ + output.json + + cat output.json + ``` + +## Cleanup + +```bash +cdk destroy +``` + +--- + +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/lambda-s3-files-cdk/bin/app.ts b/lambda-s3-files-cdk/bin/app.ts new file mode 100644 index 000000000..95c7f4486 --- /dev/null +++ b/lambda-s3-files-cdk/bin/app.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import "source-map-support/register"; +import * as cdk from "aws-cdk-lib"; +import { LambdaS3FilesStack } from "../lib/lambda-s3-files-stack"; + +const app = new cdk.App(); +new LambdaS3FilesStack(app, "LambdaS3FilesStack"); diff --git a/lambda-s3-files-cdk/cdk.json b/lambda-s3-files-cdk/cdk.json new file mode 100644 index 000000000..822400f59 --- /dev/null +++ b/lambda-s3-files-cdk/cdk.json @@ -0,0 +1,11 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/app.ts", + "watch": { + "include": ["**"], + "exclude": ["README.md", "cdk*.json", "**/*.d.ts", "**/*.js", "node_modules", "src"] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true + } +} diff --git a/lambda-s3-files-cdk/example-pattern.json b/lambda-s3-files-cdk/example-pattern.json new file mode 100644 index 000000000..e9e8f70df --- /dev/null +++ b/lambda-s3-files-cdk/example-pattern.json @@ -0,0 +1,61 @@ +{ + "title": "AWS Lambda with Amazon S3 Files Mount", + "description": "Mount an Amazon S3 bucket as a local file system on AWS Lambda using Amazon S3 Files, enabling standard file operations (read, write, list) without downloading objects.", + "language": "TypeScript", + "level": "300", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern deploys a Lambda function with an Amazon S3 Files file system mounted at /mnt/s3data. The function performs standard file operations (read, write, list) on S3 data using the local filesystem — no S3 API calls needed.", + "S3 Files provides NFS access to S3 buckets with sub-millisecond latency on small files and full POSIX semantics. The pattern creates a VPC, S3 Files file system, mount targets, access point, and a Lambda function wired together.", + "Multiple Lambda functions can connect to the same S3 Files file system simultaneously, sharing data through a common workspace without custom synchronization logic." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-s3-files-cdk", + "templateURL": "serverless-patterns/lambda-s3-files-cdk", + "projectFolder": "lambda-s3-files-cdk", + "templateFile": "lib/lambda-s3-files-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Amazon S3 Files Documentation", + "link": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files.html" + }, + { + "text": "Configuring Amazon S3 Files access for Lambda", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/configuration-filesystem-s3files.html" + }, + { + "text": "Mounting S3 file systems on Lambda functions", + "link": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-mounting-lambda.html" + } + ] + }, + "deploy": { + "text": [ + "cdk deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk destroy." + ] + }, + "authors": [ + { + "name": "Nithin Chandran R", + "bio": "Technical Account Manager at AWS", + "linkedin": "nithin-chandran-r" + } + ] +} diff --git a/lambda-s3-files-cdk/lib/lambda-s3-files-stack.ts b/lambda-s3-files-cdk/lib/lambda-s3-files-stack.ts new file mode 100644 index 000000000..1f54d36cc --- /dev/null +++ b/lambda-s3-files-cdk/lib/lambda-s3-files-stack.ts @@ -0,0 +1,126 @@ +import * as cdk from "aws-cdk-lib"; +import * as lambda from "aws-cdk-lib/aws-lambda"; +import * as s3 from "aws-cdk-lib/aws-s3"; +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as iam from "aws-cdk-lib/aws-iam"; +import { Construct } from "constructs"; + +export class LambdaS3FilesStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const mountPath = "/mnt/s3data"; + + // S3 bucket for the file system (versioning required by S3 Files) + const bucket = new s3.Bucket(this, "DataBucket", { + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, + versioned: true, + }); + + // VPC for Lambda and S3 Files mount targets + const vpc = new ec2.Vpc(this, "Vpc", { + maxAzs: 2, + natGateways: 1, + }); + + // Security group allowing NFS traffic (port 2049) + const s3FilesSg = new ec2.SecurityGroup(this, "S3FilesSg", { + vpc, + description: "Allow NFS traffic for S3 Files mount targets", + }); + s3FilesSg.addIngressRule(s3FilesSg, ec2.Port.tcp(2049), "NFS from Lambda"); + + // IAM role for S3 Files to access the bucket (uses EFS service principal) + const s3FilesRole = new iam.Role(this, "S3FilesRole", { + assumedBy: new iam.ServicePrincipal("elasticfilesystem.amazonaws.com"), + }); + bucket.grantReadWrite(s3FilesRole); + + // S3 Files FileSystem (L1 construct — no L2 yet) + const fileSystem = new cdk.CfnResource(this, "S3FileSystem", { + type: "AWS::S3Files::FileSystem", + properties: { + Bucket: bucket.bucketArn, + RoleArn: s3FilesRole.roleArn, + AcceptBucketWarning: true, + }, + }); + + // Mount targets in each private subnet + const privateSubnets = vpc.privateSubnets; + const mountTargets = privateSubnets.map((subnet, i) => { + const mt = new cdk.CfnResource(this, `MountTarget${i}`, { + type: "AWS::S3Files::MountTarget", + properties: { + FileSystemId: fileSystem.getAtt("FileSystemId"), + SubnetId: subnet.subnetId, + SecurityGroups: [s3FilesSg.securityGroupId], + }, + }); + mt.addDependency(fileSystem); + return mt; + }); + + // Access point for Lambda (UID/GID 1000, root /lambda) + const accessPoint = new cdk.CfnResource(this, "S3FilesAccessPoint", { + type: "AWS::S3Files::AccessPoint", + properties: { + FileSystemId: fileSystem.getAtt("FileSystemId"), + PosixUser: { Uid: "1000", Gid: "1000" }, + RootDirectory: { + Path: "/lambda", + CreationPermissions: { + OwnerUid: "1000", + OwnerGid: "1000", + Permissions: "755", + }, + }, + }, + }); + accessPoint.addDependency(fileSystem); + + // Lambda function with S3 Files mount + const fn = new lambda.Function(this, "S3FilesFn", { + runtime: lambda.Runtime.NODEJS_22_X, // overridden to nodejs24.x below + handler: "index.handler", + code: lambda.Code.fromAsset("src"), + timeout: cdk.Duration.minutes(1), + memorySize: 512, + vpc, + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, + securityGroups: [s3FilesSg], + environment: { MOUNT_PATH: mountPath }, + description: "Lambda function with S3 Files mount", + }); + + // Attach S3 Files filesystem config via escape hatch + const cfnFn = fn.node.defaultChild as lambda.CfnFunction; + cfnFn.addOverride("Properties.Runtime", "nodejs24.x"); + cfnFn.fileSystemConfigs = [ + { + arn: accessPoint.getAtt("AccessPointArn").toString(), + localMountPath: mountPath, + }, + ]; + + // Ensure mount targets are ready before Lambda + mountTargets.forEach((mt) => cfnFn.addDependency(mt)); + cfnFn.addDependency(accessPoint); + + // Lambda permissions for S3 Files and direct S3 reads + fn.addToRolePolicy( + new iam.PolicyStatement({ + actions: ["s3files:ClientMount", "s3files:ClientWrite"], + resources: [accessPoint.getAtt("AccessPointArn").toString()], + }) + ); + bucket.grantRead(fn); + + new cdk.CfnOutput(this, "FunctionName", { value: fn.functionName }); + new cdk.CfnOutput(this, "BucketName", { value: bucket.bucketName }); + new cdk.CfnOutput(this, "FileSystemId", { + value: fileSystem.getAtt("FileSystemId").toString(), + }); + } +} diff --git a/lambda-s3-files-cdk/package.json b/lambda-s3-files-cdk/package.json new file mode 100644 index 000000000..76b5f6dad --- /dev/null +++ b/lambda-s3-files-cdk/package.json @@ -0,0 +1,20 @@ +{ + "name": "lambda-s3-files-cdk", + "version": "1.0.0", + "bin": { + "app": "bin/app.ts" + }, + "scripts": { + "build": "tsc", + "cdk": "cdk" + }, + "dependencies": { + "aws-cdk-lib": "2.180.0", + "constructs": "10.4.2" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "ts-node": "^10.9.0", + "typescript": "~5.7.0" + } +} diff --git a/lambda-s3-files-cdk/src/index.js b/lambda-s3-files-cdk/src/index.js new file mode 100644 index 000000000..0e99d3ef6 --- /dev/null +++ b/lambda-s3-files-cdk/src/index.js @@ -0,0 +1,43 @@ +const fs = require("fs"); +const path = require("path"); + +const MOUNT_PATH = process.env.MOUNT_PATH || "/mnt/s3data"; + +exports.handler = async (event) => { + const action = event.action || "list"; + + switch (action) { + case "write": { + const filename = event.filename || "hello.txt"; + const content = event.content || `Written by Lambda at ${new Date().toISOString()}`; + const filePath = path.join(MOUNT_PATH, filename); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content); + return { status: "written", path: filePath, size: Buffer.byteLength(content) }; + } + + case "read": { + const filename = event.filename || "hello.txt"; + const filePath = path.join(MOUNT_PATH, filename); + if (!fs.existsSync(filePath)) { + return { status: "not_found", path: filePath }; + } + const content = fs.readFileSync(filePath, "utf-8"); + return { status: "read", path: filePath, content, size: content.length }; + } + + case "list": + default: { + const dir = event.directory || ""; + const targetPath = path.join(MOUNT_PATH, dir); + if (!fs.existsSync(targetPath)) { + return { status: "not_found", path: targetPath }; + } + const entries = fs.readdirSync(targetPath, { withFileTypes: true }).map((e) => ({ + name: e.name, + type: e.isDirectory() ? "directory" : "file", + })); + return { status: "listed", path: targetPath, count: entries.length, entries }; + } + } +}; diff --git a/lambda-s3-files-cdk/tsconfig.json b/lambda-s3-files-cdk/tsconfig.json new file mode 100644 index 000000000..15e54de36 --- /dev/null +++ b/lambda-s3-files-cdk/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "outDir": "build", + "rootDir": ".", + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "esModuleInterop": true + }, + "exclude": ["node_modules", "build", "src"] +}