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..6d7f5a210 --- /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. + +> **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 + +```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"}]} +``` + +## Cleanup + +Delete the stack: + +```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-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 000000000..13c10af0d Binary files /dev/null and b/lambda-s3-files-cdk-dotnet/images/architecture.png differ 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" + } + } +} diff --git a/lambda-s3-files-cdk-dotnet/src/Cdk/Cdk.csproj b/lambda-s3-files-cdk-dotnet/src/Cdk/Cdk.csproj new file mode 100644 index 000000000..eb8acbcb5 --- /dev/null +++ b/lambda-s3-files-cdk-dotnet/src/Cdk/Cdk.csproj @@ -0,0 +1,16 @@ + + + + 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" +}