From a00954740e0679262e05781639d961a11e39ab1d Mon Sep 17 00:00:00 2001 From: Pankaj Rawat Date: Sun, 21 Jun 2026 19:28:03 +0530 Subject: [PATCH 1/3] feat: Add .NET 10 implementation of Lambda S3 Files mount pattern Add a new serverless pattern that deploys a .NET 10 Lambda function with an Amazon S3 Files file system mounted at /mnt/s3data. The function performs standard file operations (read, write, list) using the local filesystem without S3 API calls. Uses isolated VPC subnets with an S3 Gateway VPC endpoint instead of a NAT gateway for a more cost-effective design. --- lambda-s3-files-cdk-dotnet/.gitignore | 42 +++++ lambda-s3-files-cdk-dotnet/README.md | 120 ++++++++++++ lambda-s3-files-cdk-dotnet/cdk.json | 63 +++++++ .../example-pattern.json | 63 +++++++ .../images/architecture.png | Bin 0 -> 62908 bytes lambda-s3-files-cdk-dotnet/src/Cdk/Cdk.csproj | 16 ++ .../src/Cdk/LambdaS3FilesStack.cs | 156 +++++++++++++++ lambda-s3-files-cdk-dotnet/src/Cdk/Program.cs | 24 +++ .../src/LambdaS3Files.sln | 25 +++ .../src/S3FilesLambda/Function.cs | 177 ++++++++++++++++++ .../src/S3FilesLambda/S3FilesLambda.csproj | 18 ++ .../aws-lambda-tools-defaults.json | 15 ++ 12 files changed, 719 insertions(+) create mode 100644 lambda-s3-files-cdk-dotnet/.gitignore create mode 100644 lambda-s3-files-cdk-dotnet/README.md create mode 100644 lambda-s3-files-cdk-dotnet/cdk.json create mode 100644 lambda-s3-files-cdk-dotnet/example-pattern.json create mode 100644 lambda-s3-files-cdk-dotnet/images/architecture.png create mode 100644 lambda-s3-files-cdk-dotnet/src/Cdk/Cdk.csproj create mode 100644 lambda-s3-files-cdk-dotnet/src/Cdk/LambdaS3FilesStack.cs create mode 100644 lambda-s3-files-cdk-dotnet/src/Cdk/Program.cs create mode 100644 lambda-s3-files-cdk-dotnet/src/LambdaS3Files.sln create mode 100644 lambda-s3-files-cdk-dotnet/src/S3FilesLambda/Function.cs create mode 100644 lambda-s3-files-cdk-dotnet/src/S3FilesLambda/S3FilesLambda.csproj create mode 100644 lambda-s3-files-cdk-dotnet/src/S3FilesLambda/aws-lambda-tools-defaults.json diff --git a/lambda-s3-files-cdk-dotnet/.gitignore b/lambda-s3-files-cdk-dotnet/.gitignore new file mode 100644 index 000000000..b4c3bff5b --- /dev/null +++ b/lambda-s3-files-cdk-dotnet/.gitignore @@ -0,0 +1,42 @@ +# CDK asset staging directory +.cdk.staging +cdk.out + +# Build results +[Dd]ebug/ +[Rr]elease/ +x64/ +x86/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio +.vs/ +*.suo +*.user +*.userosscache +*.sln.docstates + +# NuGet +*.nupkg +**/[Pp]ackages/* +!**/[Pp]ackages/build/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# User-specific files +*.userprefs + +# Build logs +*.log +*.binlog + +# Test Results +[Tt]est[Rr]esult*/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ diff --git a/lambda-s3-files-cdk-dotnet/README.md b/lambda-s3-files-cdk-dotnet/README.md new file mode 100644 index 000000000..be58fc347 --- /dev/null +++ b/lambda-s3-files-cdk-dotnet/README.md @@ -0,0 +1,120 @@ +# AWS Lambda with Amazon S3 Files Mount (.NET) + +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. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/lambda-s3-files-cdk-dotnet + +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) +* [.NET 10](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) installed +* [AWS Cloud Development Kit](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) (AWS CDK) installed + +## Deployment Instructions + +1. Clone the project to your local working directory + + ```bash + git clone https://github.com/aws-samples/serverless-patterns + ``` + +2. Change the working directory to this pattern's directory + + ```bash + cd serverless-patterns/lambda-s3-files-cdk-dotnet + ``` + +3. Publish the Lambda function + + ```bash + dotnet publish src/S3FilesLambda -c Release -o src/S3FilesLambda/publish -r linux-x64 --self-contained false + ``` + +4. Deploy the stack to your default AWS account and region + + ```bash + cdk deploy + ``` + +## How it works + +S3 Files provides NFS access to S3 buckets with sub-millisecond latency on small files and full POSIX semantics. The pattern creates: + +- A **VPC** with isolated private subnets (no NAT gateway — uses VPC endpoints instead) +- An **S3 Gateway VPC endpoint** for direct S3 API access from private subnets +- An **S3 bucket** with versioning enabled (required by S3 Files) +- An **S3 Files file system** backed by the bucket +- **Mount targets** in each isolated subnet for NFS connectivity +- An **access point** defining the POSIX identity and root path +- A **.NET 10 Lambda function** with the S3 Files file system mounted at `/mnt/s3data` + +Multiple Lambda functions can connect to the same S3 Files file system simultaneously, sharing data through a common workspace without custom synchronization logic. + +## Architecture + +![Architecture](images/architecture.png) + +## Testing + +After deployment, invoke the Lambda function with different payloads to test file operations: + +### Write a file + +```bash +aws lambda invoke --function-name \ + --payload '{"action": "write", "filename": "hello.txt", "content": "Hello from .NET Lambda!"}' \ + --cli-binary-format raw-in-base64-out \ + output.json && cat output.json +``` + +Expected response: +```json +{"status":"written","path":"/mnt/s3data/hello.txt","size":22} +``` + +### Read a file + +```bash +aws lambda invoke --function-name \ + --payload '{"action": "read", "filename": "hello.txt"}' \ + --cli-binary-format raw-in-base64-out \ + output.json && cat output.json +``` + +Expected response: +```json +{"status":"read","path":"/mnt/s3data/hello.txt","content":"Hello from .NET Lambda!","size":22} +``` + +### List files + +```bash +aws lambda invoke --function-name \ + --payload '{"action": "list"}' \ + --cli-binary-format raw-in-base64-out \ + output.json && cat output.json +``` + +Expected response: +```json +{"status":"listed","path":"/mnt/s3data","count":1,"entries":[{"name":"hello.txt","type":"file"}]} +``` + +Replace `` with the value from the `FunctionName` output of the CloudFormation stack. + +## Cleanup + +Delete the stack: + +```bash +cdk destroy +``` + +---- +Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/lambda-s3-files-cdk-dotnet/cdk.json b/lambda-s3-files-cdk-dotnet/cdk.json new file mode 100644 index 000000000..c1ac29127 --- /dev/null +++ b/lambda-s3-files-cdk-dotnet/cdk.json @@ -0,0 +1,63 @@ +{ + "app": "dotnet run --project src/Cdk/Cdk.csproj", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "src/*/obj", + "src/*/bin", + "src/*.sln", + "src/*/GlobalSuppressions.cs", + "src/*/*.csproj" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true + } +} diff --git a/lambda-s3-files-cdk-dotnet/example-pattern.json b/lambda-s3-files-cdk-dotnet/example-pattern.json new file mode 100644 index 000000000..5d4bf0b79 --- /dev/null +++ b/lambda-s3-files-cdk-dotnet/example-pattern.json @@ -0,0 +1,63 @@ +{ + "title": "AWS Lambda with Amazon S3 Files Mount (.NET)", + "description": "Mount an S3 bucket as a local file system on Lambda using S3 Files for standard file operations without S3 API calls.", + "language": ".NET", + "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 with isolated subnets (no NAT gateway), an S3 gateway VPC endpoint, 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-dotnet", + "templateURL": "serverless-patterns/lambda-s3-files-cdk-dotnet", + "projectFolder": "lambda-s3-files-cdk-dotnet", + "templateFile": "src/Cdk/LambdaS3FilesStack.cs" + } + }, + "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": "Pankaj Rawat", + "image": "https://avatars.githubusercontent.com/u/21244341?s=96&v=4", + "bio": "Pankaj Rawat is a Lead Consultant at Amazon Web Services.", + "linkedin": "pankaj-rawat-14568765", + "twitter": "pankajrawat333" + } + ] +} diff --git a/lambda-s3-files-cdk-dotnet/images/architecture.png b/lambda-s3-files-cdk-dotnet/images/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..13c10af0d77ec6547b3634b96f1e8c659efb18fa GIT binary patch literal 62908 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 + + + Exe + net8.0 + enable + enable + Major + + + + + + + + diff --git a/lambda-s3-files-cdk-dotnet/src/Cdk/LambdaS3FilesStack.cs b/lambda-s3-files-cdk-dotnet/src/Cdk/LambdaS3FilesStack.cs new file mode 100644 index 000000000..2b3a8f93a --- /dev/null +++ b/lambda-s3-files-cdk-dotnet/src/Cdk/LambdaS3FilesStack.cs @@ -0,0 +1,156 @@ +using Amazon.CDK; +using Amazon.CDK.AWS.EC2; +using Amazon.CDK.AWS.IAM; +using Amazon.CDK.AWS.Lambda; +using Amazon.CDK.AWS.S3; +using Constructs; +using S3Files = Amazon.CDK.AWS.S3Files; +using FileSystem = Amazon.CDK.AWS.Lambda.FileSystem; + +namespace Cdk; + +public class LambdaS3FilesStack : Stack +{ + internal LambdaS3FilesStack(Construct scope, string id, IStackProps? props = null) + : base(scope, id, props) + { + var mountPath = "/mnt/s3data"; + + // S3 bucket for the file system (versioning required by S3 Files) + var bucket = new Bucket(this, "DataBucket", new BucketProps + { + RemovalPolicy = RemovalPolicy.DESTROY, + AutoDeleteObjects = true, + Versioned = true + }); + + // VPC for Lambda and S3 Files mount targets (no NAT — uses VPC endpoints instead) + var vpc = new Vpc(this, "Vpc", new VpcProps + { + MaxAzs = 2, + NatGateways = 0, + SubnetConfiguration = new[] + { + new SubnetConfiguration + { + Name = "Private", + SubnetType = SubnetType.PRIVATE_ISOLATED, + CidrMask = 24 + } + } + }); + + // S3 Gateway endpoint for direct S3 API access from private subnets + vpc.AddGatewayEndpoint("S3Endpoint", new GatewayVpcEndpointOptions + { + Service = GatewayVpcEndpointAwsService.S3 + }); + + // Security group allowing NFS traffic (port 2049) + var s3FilesSg = new SecurityGroup(this, "S3FilesSg", new SecurityGroupProps + { + Vpc = vpc, + Description = "Allow NFS traffic for S3 Files mount targets" + }); + s3FilesSg.AddIngressRule(s3FilesSg, Port.Tcp(2049), "NFS from Lambda"); + + // IAM role for S3 Files to access the bucket (uses EFS service principal) + var s3FilesRole = new Role(this, "S3FilesRole", new RoleProps + { + AssumedBy = new ServicePrincipal("elasticfilesystem.amazonaws.com") + }); + bucket.GrantReadWrite(s3FilesRole); + + // S3 Files FileSystem + var fileSystem = new S3Files.CfnFileSystem(this, "S3FileSystem", new S3Files.CfnFileSystemProps + { + Bucket = bucket.BucketArn, + RoleArn = s3FilesRole.RoleArn + }); + + // Mount targets in each isolated subnet + var isolatedSubnets = vpc.IsolatedSubnets; + var mountTargets = new List(); + for (var i = 0; i < isolatedSubnets.Length; i++) + { + var mt = new S3Files.CfnMountTarget(this, $"MountTarget{i}", new S3Files.CfnMountTargetProps + { + FileSystemId = fileSystem.AttrFileSystemId, + SubnetId = isolatedSubnets[i].SubnetId, + SecurityGroups = new[] { s3FilesSg.SecurityGroupId } + }); + mt.AddDependency(fileSystem); + mountTargets.Add(mt); + } + + // Access point for Lambda (UID/GID 1000, root /lambda) + var accessPoint = new S3Files.CfnAccessPoint(this, "S3FilesAccessPoint", new S3Files.CfnAccessPointProps + { + FileSystemId = fileSystem.AttrFileSystemId, + PosixUser = new S3Files.CfnAccessPoint.PosixUserProperty + { + Uid = "1000", + Gid = "1000" + }, + RootDirectory = new S3Files.CfnAccessPoint.RootDirectoryProperty + { + Path = "/lambda", + CreationPermissions = new S3Files.CfnAccessPoint.CreationPermissionsProperty + { + OwnerUid = "1000", + OwnerGid = "1000", + Permissions = "755" + } + } + }); + accessPoint.AddDependency(fileSystem); + + // Lambda function with S3 Files mount + var fn = new Function(this, "S3FilesFn", new FunctionProps + { + Runtime = Runtime.DOTNET_10, + Handler = "S3FilesLambda::S3FilesLambda.Function::FunctionHandler", + Code = Code.FromAsset("src/S3FilesLambda/publish"), + Timeout = Duration.Minutes(1), + MemorySize = 512, + Vpc = vpc, + VpcSubnets = new SubnetSelection { SubnetType = SubnetType.PRIVATE_ISOLATED }, + SecurityGroups = new[] { s3FilesSg }, + Filesystem = FileSystem.FromS3FilesAccessPoint(accessPoint, mountPath), + Environment = new Dictionary + { + ["MOUNT_PATH"] = mountPath + }, + Description = "Lambda function with S3 Files mount (.NET)" + }); + + // Ensure mount targets are ready before Lambda + var cfnFn = (CfnFunction)fn.Node.DefaultChild!; + foreach (var mt in mountTargets) + { + cfnFn.AddDependency(mt); + } + + // Lambda permissions for S3 Files and direct S3 reads + fn.AddToRolePolicy(new PolicyStatement(new PolicyStatementProps + { + Actions = new[] { "s3files:ClientMount", "s3files:ClientWrite" }, + Resources = new[] { accessPoint.AttrAccessPointArn } + })); + bucket.GrantRead(fn); + + // Outputs + _ = new CfnOutput(this, "FunctionName", new CfnOutputProps + { + Value = fn.FunctionName + }); + _ = new CfnOutput(this, "BucketName", new CfnOutputProps + { + Value = bucket.BucketName + }); + _ = new CfnOutput(this, "FileSystemId", new CfnOutputProps + { + Value = fileSystem.AttrFileSystemId + }); + } +} diff --git a/lambda-s3-files-cdk-dotnet/src/Cdk/Program.cs b/lambda-s3-files-cdk-dotnet/src/Cdk/Program.cs new file mode 100644 index 000000000..e4aa68177 --- /dev/null +++ b/lambda-s3-files-cdk-dotnet/src/Cdk/Program.cs @@ -0,0 +1,24 @@ +using Amazon.CDK; + +namespace Cdk; + +internal sealed class Program +{ + public static void Main(string[] args) + { + var app = new App(); + new LambdaS3FilesStack(app, "LambdaS3FilesStack", new StackProps + { + // Uncomment to specialize this stack for the AWS Account and Region + // that are implied by the current CLI configuration. + /* + Env = new Amazon.CDK.Environment + { + Account = System.Environment.GetEnvironmentVariable("CDK_DEFAULT_ACCOUNT"), + Region = System.Environment.GetEnvironmentVariable("CDK_DEFAULT_REGION"), + } + */ + }); + app.Synth(); + } +} diff --git a/lambda-s3-files-cdk-dotnet/src/LambdaS3Files.sln b/lambda-s3-files-cdk-dotnet/src/LambdaS3Files.sln new file mode 100644 index 000000000..1a5347422 --- /dev/null +++ b/lambda-s3-files-cdk-dotnet/src/LambdaS3Files.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cdk", "Cdk\Cdk.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S3FilesLambda", "S3FilesLambda\S3FilesLambda.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/lambda-s3-files-cdk-dotnet/src/S3FilesLambda/Function.cs b/lambda-s3-files-cdk-dotnet/src/S3FilesLambda/Function.cs new file mode 100644 index 000000000..3c21e0310 --- /dev/null +++ b/lambda-s3-files-cdk-dotnet/src/S3FilesLambda/Function.cs @@ -0,0 +1,177 @@ +using Amazon.Lambda.Core; +using System.Text.Json.Serialization; + +// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + +namespace S3FilesLambda; + +public class Function +{ + private static readonly string MountPath = Environment.GetEnvironmentVariable("MOUNT_PATH") ?? "/mnt/s3data"; + + public FileOperationResponse FunctionHandler(FileOperationRequest request, ILambdaContext context) + { + var action = request.Action ?? "list"; + + return action.ToLowerInvariant() switch + { + "write" => HandleWrite(request, context), + "read" => HandleRead(request, context), + _ => HandleList(request, context) + }; + } + + private FileOperationResponse HandleWrite(FileOperationRequest request, ILambdaContext context) + { + var filename = request.Filename ?? "hello.txt"; + var content = request.Content ?? $"Written by Lambda at {DateTime.UtcNow:O}"; + var filePath = SafePath(filename); + + var directory = Path.GetDirectoryName(filePath); + if (!string.IsNullOrEmpty(directory)) + { + Directory.CreateDirectory(directory); + } + + File.WriteAllText(filePath, content); + + context.Logger.LogInformation($"Written file: {filePath}"); + + return new FileOperationResponse + { + Status = "written", + Path = filePath, + Size = System.Text.Encoding.UTF8.GetByteCount(content) + }; + } + + private FileOperationResponse HandleRead(FileOperationRequest request, ILambdaContext context) + { + var filename = request.Filename ?? "hello.txt"; + var filePath = SafePath(filename); + + if (!File.Exists(filePath)) + { + return new FileOperationResponse + { + Status = "not_found", + Path = filePath + }; + } + + var content = File.ReadAllText(filePath); + + context.Logger.LogInformation($"Read file: {filePath}"); + + return new FileOperationResponse + { + Status = "read", + Path = filePath, + Content = content, + Size = content.Length + }; + } + + private FileOperationResponse HandleList(FileOperationRequest request, ILambdaContext context) + { + var dir = request.Directory ?? ""; + var targetPath = SafePath(dir); + + if (!System.IO.Directory.Exists(targetPath)) + { + return new FileOperationResponse + { + Status = "not_found", + Path = targetPath + }; + } + + var entries = System.IO.Directory.GetFileSystemEntries(targetPath) + .Select(entry => + { + var isDir = System.IO.Directory.Exists(entry); + return new FileEntry + { + Name = Path.GetFileName(entry), + Type = isDir ? "directory" : "file" + }; + }) + .ToList(); + + context.Logger.LogInformation($"Listed directory: {targetPath} ({entries.Count} entries)"); + + return new FileOperationResponse + { + Status = "listed", + Path = targetPath, + Count = entries.Count, + Entries = entries + }; + } + + /// + /// Resolve user input to a safe path under MOUNT_PATH. Throws on traversal. + /// + private string SafePath(string userInput) + { + var resolved = Path.GetFullPath(Path.Combine(MountPath, userInput)); + + if (!resolved.StartsWith(MountPath + Path.DirectorySeparatorChar) && resolved != MountPath) + { + throw new InvalidOperationException($"Path traversal blocked: {userInput}"); + } + + return resolved; + } +} + +public class FileOperationRequest +{ + [JsonPropertyName("action")] + public string? Action { get; set; } + + [JsonPropertyName("filename")] + public string? Filename { get; set; } + + [JsonPropertyName("content")] + public string? Content { get; set; } + + [JsonPropertyName("directory")] + public string? Directory { get; set; } +} + +public class FileOperationResponse +{ + [JsonPropertyName("status")] + public string Status { get; set; } = ""; + + [JsonPropertyName("path")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Path { get; set; } + + [JsonPropertyName("content")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Content { get; set; } + + [JsonPropertyName("size")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Size { get; set; } + + [JsonPropertyName("count")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Count { get; set; } + + [JsonPropertyName("entries")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Entries { get; set; } +} + +public class FileEntry +{ + [JsonPropertyName("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("type")] + public string Type { get; set; } = ""; +} diff --git a/lambda-s3-files-cdk-dotnet/src/S3FilesLambda/S3FilesLambda.csproj b/lambda-s3-files-cdk-dotnet/src/S3FilesLambda/S3FilesLambda.csproj new file mode 100644 index 000000000..39f38020e --- /dev/null +++ b/lambda-s3-files-cdk-dotnet/src/S3FilesLambda/S3FilesLambda.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + enable + true + Lambda + true + true + + + + + + + + diff --git a/lambda-s3-files-cdk-dotnet/src/S3FilesLambda/aws-lambda-tools-defaults.json b/lambda-s3-files-cdk-dotnet/src/S3FilesLambda/aws-lambda-tools-defaults.json new file mode 100644 index 000000000..e2108f550 --- /dev/null +++ b/lambda-s3-files-cdk-dotnet/src/S3FilesLambda/aws-lambda-tools-defaults.json @@ -0,0 +1,15 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "", + "region": "", + "configuration": "Release", + "function-runtime": "dotnet10", + "function-memory-size": 512, + "function-timeout": 60, + "function-handler": "S3FilesLambda::S3FilesLambda.Function::FunctionHandler" +} From 7e34466e99c1a3f1ad55df398e01f11c7ed0f2b0 Mon Sep 17 00:00:00 2001 From: biswanathmukherjee <30332793+biswanathmukherjee@users.noreply.github.com> Date: Tue, 23 Jun 2026 10:13:58 +0530 Subject: [PATCH 2/3] Create lambda-s3-files-cdk-dotnet.json --- .../lambda-s3-files-cdk-dotnet.json | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 lambda-s3-files-cdk-dotnet/lambda-s3-files-cdk-dotnet.json diff --git a/lambda-s3-files-cdk-dotnet/lambda-s3-files-cdk-dotnet.json b/lambda-s3-files-cdk-dotnet/lambda-s3-files-cdk-dotnet.json new file mode 100644 index 000000000..7ff013aba --- /dev/null +++ b/lambda-s3-files-cdk-dotnet/lambda-s3-files-cdk-dotnet.json @@ -0,0 +1,82 @@ +{ + "title": "AWS Lambda with Amazon S3 Files Mount (.NET)", + "description": "Mount an S3 bucket as a local file system on Lambda using S3 Files for standard file operations without S3 API calls.", + "language": ".NET", + "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 with isolated subnets (no NAT gateway), an S3 gateway VPC endpoint, 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-dotnet", + "templateURL": "serverless-patterns/lambda-s3-files-cdk-dotnet", + "projectFolder": "lambda-s3-files-cdk-dotnet", + "templateFile": "src/Cdk/LambdaS3FilesStack.cs" + } + }, + "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": "Pankaj Rawat", + "image": "https://avatars.githubusercontent.com/u/21244341?s=96&v=4", + "bio": "Pankaj Rawat is a Lead Consultant at Amazon Web Services.", + "linkedin": "pankaj-rawat-14568765", + "twitter": "pankajrawat333" + } + ], + "patternArch": { + "icon1": { + "x": 20, + "y": 50, + "service": "lambda", + "label": "AWS Lambda" + }, + "icon2": { + "x": 80, + "y": 50, + "service": "s3", + "label": "Amazon S3" + }, + "line1": { + "from": "icon1", + "to": "icon2", + "label": "read/ write" + } + } +} From 67807b1a9421dc39a600deca7a9e1fb2e3b0a45c Mon Sep 17 00:00:00 2001 From: Pankaj Rawat Date: Tue, 23 Jun 2026 17:14:26 +0530 Subject: [PATCH 3/3] docs: Address CR comments on README - Update copyright year from 2024 to 2026 - Move FunctionName replacement instruction to beginning of Testing section so users know what to substitute before running commands --- lambda-s3-files-cdk-dotnet/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lambda-s3-files-cdk-dotnet/README.md b/lambda-s3-files-cdk-dotnet/README.md index be58fc347..6d7f5a210 100644 --- a/lambda-s3-files-cdk-dotnet/README.md +++ b/lambda-s3-files-cdk-dotnet/README.md @@ -60,7 +60,9 @@ Multiple Lambda functions can connect to the same S3 Files file system simultane ## Testing -After deployment, invoke the Lambda function with different payloads to test file operations: +After deployment, invoke the Lambda function with different payloads to test file operations. + +> **Note:** Replace `` in the commands below with the actual function name from the `FunctionName` output of the CloudFormation stack (visible after `cdk deploy` completes). ### Write a file @@ -104,8 +106,6 @@ Expected response: {"status":"listed","path":"/mnt/s3data","count":1,"entries":[{"name":"hello.txt","type":"file"}]} ``` -Replace `` with the value from the `FunctionName` output of the CloudFormation stack. - ## Cleanup Delete the stack: @@ -115,6 +115,6 @@ cdk destroy ``` ---- -Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: MIT-0