Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,50 @@ var lambdaConstruct = new LambdaFunctionConstruct(this, "MyLambda", new LambdaFu
});
```

### Lambda with Custom Memory and Timeout

```csharp
var lambdaConstruct = new LambdaFunctionConstruct(this, "MyLambda", new LambdaFunctionConstructProps
{
FunctionName = "my-function",
FunctionSuffix = "prod",
AssetPath = "./lambda-deployment.zip",
RoleName = "my-function-role",
PolicyName = "my-function-policy",
MemorySize = 2048, // Custom memory allocation (default: 1024 MB)
TimeoutInSeconds = 30, // Custom timeout (default: 6 seconds)
EnvironmentVariables = new Dictionary<string, string>
{
{ "ENVIRONMENT", "production" }
}
});
```

### Lambda with Function URL for Direct HTTP Access

```csharp
var lambdaConstruct = new LambdaFunctionConstruct(this, "MyLambda", new LambdaFunctionConstructProps
{
FunctionName = "my-api",
FunctionSuffix = "prod",
AssetPath = "./lambda-deployment.zip",
RoleName = "my-api-role",
PolicyName = "my-api-policy",
GenerateUrl = true, // Enable Function URL for direct HTTP access
MemorySize = 512, // Optimized for lightweight API
TimeoutInSeconds = 15, // API timeout
EnvironmentVariables = new Dictionary<string, string>
{
{ "ENVIRONMENT", "production" },
{ "CORS_ORIGINS", "*" }
}
});

// Access the function URL domain
string functionUrlDomain = lambdaConstruct.LiveAliasFunctionUrlDomain;
Console.WriteLine($"Function URL: https://{functionUrlDomain}");
```

## Features

### 🚀 LambdaFunctionConstruct
Expand All @@ -111,6 +155,8 @@ The main construct that creates a complete Lambda function setup with:
- **CloudWatch Logs**: Explicit log group with configurable retention (default: 2 weeks)
- **OpenTelemetry**: Optional AWS OTEL Collector layer integration
- **SnapStart**: Optional AWS Lambda SnapStart for improved cold start performance
- **Function URLs**: Optional direct HTTP access without API Gateway
- **Configurable Resources**: Customizable memory allocation and timeout settings
- **Versioning**: Automatic version creation with "live" alias
- **Multi-target Permissions**: Applies permissions to function, version, and alias

Expand Down Expand Up @@ -276,6 +322,9 @@ table.AttachStreamLambda(streamProcessor.Function);
| `AssetPath` | `string` | Path to Lambda deployment package | Required |
| `RoleName` | `string` | IAM role name | Required |
| `PolicyName` | `string` | IAM policy name | Required |
| `MemorySize` | `double` | Memory allocation in MB | `1024` |
| `TimeoutInSeconds` | `double` | Function timeout in seconds | `6` |
| `GenerateUrl` | `bool` | Enable Function URL for direct HTTP access | `false` |
| `PolicyStatements` | `PolicyStatement[]` | Custom IAM policy statements | `[]` |
| `EnvironmentVariables` | `IDictionary<string, string>` | Environment variables | `{}` |
| `IncludeOtelLayer` | `bool` | Enable OpenTelemetry layer | `true` |
Expand Down Expand Up @@ -325,6 +374,9 @@ public void MyStack_ShouldCreateApiFunction()
var props = CdkTestHelper.CreatePropsBuilder("./my-lambda.zip")
.WithFunctionName("my-api")
.WithFunctionSuffix("prod")
.WithMemorySize(2048)
.WithTimeoutInSeconds(30)
.WithGenerateUrl()
.WithDynamoDbAccess("users-table")
.WithApiGatewayPermission("arn:aws:execute-api:us-east-1:123456789012:abcdef123/prod/GET/users")
.WithEnvironmentVariable("TABLE_NAME", "users-table")
Expand Down Expand Up @@ -646,6 +698,11 @@ var staticAssetPath = CdkTestHelper.GetTestAssetPath("TestAssets/my-site");
| `ShouldHaveLogGroup(functionName, retentionDays)` | Verify log group configuration |
| `ShouldHaveSnapStart()` | Verify SnapStart is enabled |
| `ShouldNotHaveSnapStart()` | Verify SnapStart is disabled |
| `ShouldHaveMemorySize(memorySize)` | Verify Lambda memory allocation |
| `ShouldHaveTimeout(timeoutInSeconds)` | Verify Lambda timeout configuration |
| `ShouldHaveFunctionUrl(authType)` | Verify Function URL exists with specified auth |
| `ShouldNotHaveFunctionUrl()` | Verify no Function URL is configured |
| `ShouldHaveFunctionUrlOutput(stackName, constructId)` | Verify Function URL CloudFormation output |

#### Static Site Assertions

Expand Down Expand Up @@ -693,6 +750,9 @@ var staticAssetPath = CdkTestHelper.GetTestAssetPath("TestAssets/my-site");
| `WithCustomPolicy(policyStatement)` | Add custom IAM policy statement |
| `WithCustomPermission(permission)` | Add custom Lambda permission |
| `WithSnapStart(bool)` | Enable/disable Lambda SnapStart |
| `WithMemorySize(memorySize)` | Set custom memory allocation |
| `WithTimeoutInSeconds(timeout)` | Set custom timeout |
| `WithGenerateUrl(bool)` | Enable/disable Function URL |

#### Static Site Props Builder

Expand Down
47 changes: 39 additions & 8 deletions src/LayeredCraft.Cdk.Constructs/LambdaFunctionConstruct.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@
using Amazon.CDK.AWS.Lambda;
using Amazon.CDK.AWS.Logs;
using Constructs;
using LayeredCraft.Cdk.Constructs.Extensions;
using LayeredCraft.Cdk.Constructs.Models;

namespace LayeredCraft.Cdk.Constructs;

public class LambdaFunctionConstruct : Construct
{
/// <summary>
/// The domain of the function URL if GenerateUrl is enabled, null otherwise.
/// </summary>
public readonly string? LiveAliasFunctionUrlDomain;

/// <summary>
/// The underlying Lambda function for advanced configuration.
/// </summary>
public readonly Function LambdaFunction;
public LambdaFunctionConstruct(Construct scope, string id, ILambdaFunctionConstructProps props) : base(scope, id)
{
var region = Stack.Of(this).Region;
Expand Down Expand Up @@ -59,15 +69,15 @@ public LambdaFunctionConstruct(Construct scope, string id, ILambdaFunctionConstr
RemovalPolicy = RemovalPolicy.DESTROY
});

var lambda = new Function(this, $"{id}-function", new FunctionProps
LambdaFunction = new Function(this, $"{id}-function", new FunctionProps
{
FunctionName = $"{props.FunctionName}-{props.FunctionSuffix}",
Runtime = Runtime.PROVIDED_AL2023,
Handler = "bootstrap",
Code = Code.FromAsset(props.AssetPath),
Role = role,
MemorySize = 1024,
Timeout = Duration.Seconds(6),
MemorySize = props.MemorySize,
Timeout = Duration.Seconds(props.TimeoutInSeconds),
Environment = props.EnvironmentVariables,
LogGroup = logGroup,
Tracing = props.IncludeOtelLayer ? Tracing.ACTIVE : Tracing.DISABLED,
Expand All @@ -78,16 +88,16 @@ public LambdaFunctionConstruct(Construct scope, string id, ILambdaFunctionConstr
});
if (props.IncludeOtelLayer)
{
lambda.AddLayers(LayerVersion.FromLayerVersionArn(this, "OTELLambdaLayer",
LambdaFunction.AddLayers(LayerVersion.FromLayerVersionArn(this, "OTELLambdaLayer",
$"arn:aws:lambda:{region}:901920570463:layer:aws-otel-collector-amd64-ver-0-102-1:1"));
}

// ✅ Create a new version on every deployment
var currentVersion = lambda.CurrentVersion;
var currentVersion = LambdaFunction.CurrentVersion;

if (props.EnableSnapStart)
{
var cfnFunction = (CfnFunction)lambda.Node.DefaultChild!;
var cfnFunction = (CfnFunction)LambdaFunction.Node.DefaultChild!;
cfnFunction.AddPropertyOverride("SnapStart", new Dictionary<string, object>
{
["ApplyOn"] = "PublishedVersions"
Expand All @@ -99,13 +109,34 @@ public LambdaFunctionConstruct(Construct scope, string id, ILambdaFunctionConstr
AliasName = "live",
Version = currentVersion,
});

AddPermissionsToAllTargets(
baseId: $"{id}-permission",
function: lambda,
version: lambda.CurrentVersion,
function: LambdaFunction,
version: LambdaFunction.CurrentVersion,
alias: alias,
permissions: props.Permissions
);

if (props.GenerateUrl)
{
var functionUrl = alias.AddFunctionUrl(new FunctionUrlProps
{
AuthType = FunctionUrlAuthType.NONE
});

LiveAliasFunctionUrlDomain = Fn.Select(2, Fn.Split("/", functionUrl.Url));

_ = new CfnOutput(this, $"{id}-url-output", new CfnOutputProps
{
ExportName = Stack.Of(this).CreateExportName(id, "url-output"),
Value = functionUrl.Url
});
}
else
{
LiveAliasFunctionUrlDomain = null;
}
}

private void AddPermissionsToAllTargets(string baseId, IFunction function, IVersion version, IAlias? alias, List<LambdaPermission> permissions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ public interface ILambdaFunctionConstructProps
string AssetPath { get; set; }
string RoleName { get; set; }
string PolicyName { get; set; }
double MemorySize { get; set; }
double TimeoutInSeconds { get; set; }
PolicyStatement[] PolicyStatements { get; set; }
IDictionary<string, string> EnvironmentVariables { get; set; }
bool IncludeOtelLayer { get; set; }
List<LambdaPermission> Permissions { get; set; }
bool EnableSnapStart { get; set; }
bool GenerateUrl { get; set; }
}
public sealed record LambdaFunctionConstructProps : ILambdaFunctionConstructProps
{
Expand All @@ -22,9 +25,12 @@ public sealed record LambdaFunctionConstructProps : ILambdaFunctionConstructProp
public required string AssetPath { get; set; }
public required string RoleName { get; set; }
public required string PolicyName { get; set; }
public double MemorySize { get; set; } = 1024;
public double TimeoutInSeconds { get; set; } = 6;
public PolicyStatement[] PolicyStatements { get; set; } = [];
public IDictionary<string, string> EnvironmentVariables { get; set; } = new Dictionary<string, string>();
public bool IncludeOtelLayer { get; set; } = true;
public List<LambdaPermission> Permissions { get; set; } = [];
public bool EnableSnapStart { get; set; } = false;
public bool GenerateUrl { get; set; } = false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,73 @@ public static void ShouldNotHaveSnapStart(this Template template)
{ "SnapStart", Match.AnyValue() }
})));
}

/// <summary>
/// Asserts that the template contains a Lambda function with the specified memory size.
/// </summary>
/// <param name="template">The CDK template to assert against</param>
/// <param name="memorySize">Expected memory size in MB</param>
public static void ShouldHaveMemorySize(this Template template, int memorySize)
{
template.HasResourceProperties("AWS::Lambda::Function", Match.ObjectLike(new Dictionary<string, object>
{
{ "MemorySize", memorySize }
}));
}

/// <summary>
/// Asserts that the template contains a Lambda function with the specified timeout.
/// </summary>
/// <param name="template">The CDK template to assert against</param>
/// <param name="timeoutInSeconds">Expected timeout in seconds</param>
public static void ShouldHaveTimeout(this Template template, int timeoutInSeconds)
{
template.HasResourceProperties("AWS::Lambda::Function", Match.ObjectLike(new Dictionary<string, object>
{
{ "Timeout", timeoutInSeconds }
}));
}

/// <summary>
/// Asserts that the template contains a Lambda function URL with the specified configuration.
/// </summary>
/// <param name="template">The CDK template to assert against</param>
/// <param name="authType">Expected authentication type (default: NONE)</param>
public static void ShouldHaveFunctionUrl(this Template template, string authType = "NONE")
{
template.HasResourceProperties("AWS::Lambda::Url", Match.ObjectLike(new Dictionary<string, object>
{
{ "AuthType", authType }
}));
}

/// <summary>
/// Asserts that the template does not contain any Lambda function URLs.
/// </summary>
/// <param name="template">The CDK template to assert against</param>
public static void ShouldNotHaveFunctionUrl(this Template template)
{
template.ResourceCountIs("AWS::Lambda::Url", 0);
}

/// <summary>
/// Asserts that the template contains a CloudFormation output for the function URL.
/// </summary>
/// <param name="template">The CDK template to assert against</param>
/// <param name="stackName">The stack name used for export naming</param>
/// <param name="constructId">The construct ID used for export naming</param>
public static void ShouldHaveFunctionUrlOutput(this Template template, string stackName, string constructId)
{
var outputs = template.FindOutputs("*", new Dictionary<string, object>
{
{ "Export", Match.ObjectLike(new Dictionary<string, object>
{
{ "Name", $"{stackName}-{constructId}-url-output" }
}) }
});

if (outputs.Count != 1)
throw new Exception($"Expected 1 function URL output but found {outputs.Count}");

Copilot AI Jul 9, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Throwing a generic Exception for assertion failures can obscure test failures. Consider using a more specific assertion helper or test framework assertion to deliver clearer error messages.

Copilot uses AI. Check for mistakes.
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ public class LambdaFunctionConstructPropsBuilder
private string _assetPath = "./test-lambda.zip";
private string _roleName = "test-role";
private string _policyName = "test-policy";
private double _memorySize = 1024;
Comment thread
ncipollina marked this conversation as resolved.
private double _timeoutInSeconds = 6;
Comment thread
ncipollina marked this conversation as resolved.
private readonly List<PolicyStatement> _policyStatements = new();
private readonly Dictionary<string, string> _environmentVariables = new();
private bool _includeOtelLayer = true;
private readonly List<LambdaPermission> _permissions = new();
private bool _enableSnapStart = false;
private bool _generateUrl = false;

/// <summary>
/// Sets the function name.
Expand Down Expand Up @@ -219,6 +222,39 @@ public LambdaFunctionConstructPropsBuilder WithSnapStart(bool enabled = true)
return this;
}

/// <summary>
/// Sets the memory size for the Lambda function.
/// </summary>
/// <param name="memorySize">The memory size in MB</param>
/// <returns>The builder instance for method chaining</returns>
public LambdaFunctionConstructPropsBuilder WithMemorySize(double memorySize)
{
_memorySize = memorySize;
return this;
}

/// <summary>
/// Sets the timeout for the Lambda function.
/// </summary>
/// <param name="timeoutInSeconds">The timeout in seconds</param>
/// <returns>The builder instance for method chaining</returns>
public LambdaFunctionConstructPropsBuilder WithTimeoutInSeconds(double timeoutInSeconds)
{
_timeoutInSeconds = timeoutInSeconds;
return this;
}

/// <summary>
/// Enables or disables function URL generation for direct HTTP access.
/// </summary>
/// <param name="generateUrl">Whether to generate a function URL</param>
/// <returns>The builder instance for method chaining</returns>
public LambdaFunctionConstructPropsBuilder WithGenerateUrl(bool generateUrl = true)
Comment thread
ncipollina marked this conversation as resolved.
{
_generateUrl = generateUrl;
return this;
}

/// <summary>
/// Builds the LambdaFunctionConstructProps instance.
/// </summary>
Expand All @@ -232,11 +268,14 @@ public LambdaFunctionConstructProps Build()
AssetPath = _assetPath,
RoleName = _roleName,
PolicyName = _policyName,
MemorySize = _memorySize,
TimeoutInSeconds = _timeoutInSeconds,
PolicyStatements = [.. _policyStatements],
EnvironmentVariables = _environmentVariables,
IncludeOtelLayer = _includeOtelLayer,
Permissions = _permissions,
EnableSnapStart = _enableSnapStart
EnableSnapStart = _enableSnapStart,
GenerateUrl = _generateUrl
};
}
}
Loading