diff --git a/Directory.Build.props b/Directory.Build.props index ec37eea..39888d2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 0.1.0-beta + 1.0.0 MIT diff --git a/LayeredCraft.Cdk.Constructs.sln b/LayeredCraft.Cdk.Constructs.sln index 8ab3553..c5eb02e 100644 --- a/LayeredCraft.Cdk.Constructs.sln +++ b/LayeredCraft.Cdk.Constructs.sln @@ -23,6 +23,29 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "git", "git", "{32F31203-71F .github\workflows\pr-build.yaml = .github\workflows\pr-build.yaml EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{B4C8E2F1-3A2D-4B8F-9C7E-1F5A8D9E2B3C}" + ProjectSection(SolutionItems) = preProject + docs\_config.yml = docs\_config.yml + docs\index.md = docs\index.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "constructs", "constructs", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" + ProjectSection(SolutionItems) = preProject + docs\constructs\lambda-function.md = docs\constructs\lambda-function.md + docs\constructs\static-site.md = docs\constructs\static-site.md + docs\constructs\dynamodb-table.md = docs\constructs\dynamodb-table.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testing", "testing", "{F9E8D7C6-B5A4-9382-7160-5E4D3C2B1A09}" + ProjectSection(SolutionItems) = preProject + docs\testing\index.md = docs\testing\index.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{9F8E7D6C-5B4A-3928-1607-E5D4C3B2A190}" + ProjectSection(SolutionItems) = preProject + docs\examples\index.md = docs\examples\index.md + EndProjectSection +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LayeredCraft.Cdk.Constructs", "src\LayeredCraft.Cdk.Constructs\LayeredCraft.Cdk.Constructs.csproj", "{63FCBE95-6714-49D5-A9CD-0BE725BAE259}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LayeredCraft.Cdk.Constructs.Tests", "test\LayeredCraft.Cdk.Constructs.Tests\LayeredCraft.Cdk.Constructs.Tests.csproj", "{7F2D2EA4-D201-4B0E-AE44-6D03B1B7AEBC}" @@ -38,6 +61,9 @@ Global GlobalSection(NestedProjects) = preSolution {63FCBE95-6714-49D5-A9CD-0BE725BAE259} = {5C4245CB-F794-42A5-8226-AF6AA00D0E5E} {7F2D2EA4-D201-4B0E-AE44-6D03B1B7AEBC} = {F802A3E0-E887-4EAA-87FA-AB090DCCB788} + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {B4C8E2F1-3A2D-4B8F-9C7E-1F5A8D9E2B3C} + {F9E8D7C6-B5A4-9382-7160-5E4D3C2B1A09} = {B4C8E2F1-3A2D-4B8F-9C7E-1F5A8D9E2B3C} + {9F8E7D6C-5B4A-3928-1607-E5D4C3B2A190} = {B4C8E2F1-3A2D-4B8F-9C7E-1F5A8D9E2B3C} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {63FCBE95-6714-49D5-A9CD-0BE725BAE259}.Debug|Any CPU.ActiveCfg = Debug|Any CPU diff --git a/README.md b/README.md index f3bcfae..c2d4657 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,32 @@ -# cdk-constructs +# LayeredCraft CDK Constructs [![Build Status](https://github.com/LayeredCraft/cdk-constructs/actions/workflows/build.yaml/badge.svg)](https://github.com/LayeredCraft/cdk-constructs/actions/workflows/build.yaml) [![NuGet](https://img.shields.io/nuget/v/LayeredCraft.Cdk.Constructs.svg)](https://www.nuget.org/packages/LayeredCraft.Cdk.Constructs/) [![Downloads](https://img.shields.io/nuget/dt/LayeredCraft.Cdk.Constructs.svg)](https://www.nuget.org/packages/LayeredCraft.Cdk.Constructs/) -A reusable library of AWS CDK constructs for .NET projects, optimized for serverless applications and static websites. Features comprehensive Lambda functions with OpenTelemetry support, and complete static site hosting with CloudFront CDN, SSL certificates, and DNS management. Built for speed, observability, and cost efficiency across the LayeredCraft project ecosystem. +A comprehensive library of reusable AWS CDK constructs for .NET projects, designed for serverless applications and static websites. Built with best practices, observability, and cost efficiency in mind. -## Installation +## Features -Install the package via NuGet: +- **๐Ÿš€ Lambda Functions**: Comprehensive Lambda construct with OpenTelemetry support, IAM management, and environment configuration +- **๐ŸŒ Static Sites**: Complete static website hosting with S3, CloudFront, SSL certificates, and Route53 DNS management +- **๐Ÿ“Š DynamoDB Tables**: Full-featured DynamoDB construct with streams, TTL, and global secondary indexes +- **๐Ÿงช Testing Helpers**: Extensive testing utilities with fluent assertions and builders +- **๐Ÿ“ Type Safety**: Full intellisense and compile-time validation +- **โšก Performance**: Optimized for cold starts with AWS Lambda SnapStart support + +## Installation ```bash dotnet add package LayeredCraft.Cdk.Constructs ``` -Or via Package Manager Console: - -```powershell -Install-Package LayeredCraft.Cdk.Constructs -``` - ## Quick Start ### Basic Lambda Function ```csharp using Amazon.CDK; -using Amazon.CDK.AWS.IAM; using LayeredCraft.Cdk.Constructs; using LayeredCraft.Cdk.Constructs.Models; @@ -34,13 +34,14 @@ public class MyStack : Stack { public MyStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) { - var lambdaConstruct = new LambdaFunctionConstruct(this, "MyLambda", new LambdaFunctionConstructProps + var lambda = new LambdaFunctionConstruct(this, "MyLambda", new LambdaFunctionConstructProps { - FunctionName = "my-function", + FunctionName = "my-api", FunctionSuffix = "prod", AssetPath = "./lambda-deployment.zip", - RoleName = "my-function-role", - PolicyName = "my-function-policy", + RoleName = "my-api-role", + PolicyName = "my-api-policy", + GenerateUrl = true, // Creates Function URL for HTTP access EnvironmentVariables = new Dictionary { { "ENVIRONMENT", "production" }, @@ -51,851 +52,96 @@ public class MyStack : Stack } ``` -### Lambda with Custom IAM Permissions - -```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", - PolicyStatements = - [ - new PolicyStatement(new PolicyStatementProps - { - Actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query"], - Resources = ["arn:aws:dynamodb:us-east-1:123456789012:table/MyTable"], - Effect = Effect.ALLOW - }) - }, - Permissions = new List - { - new LambdaPermission - { - Principal = "apigateway.amazonaws.com", - Action = "lambda:InvokeFunction", - SourceArn = "arn:aws:execute-api:us-east-1:123456789012:*/*/GET/my-endpoint" - } - } -}); -``` - -### Lambda with SnapStart for Improved Performance - -```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", - EnableSnapStart = true, // Enable SnapStart for faster cold starts - EnvironmentVariables = new Dictionary - { - { "ENVIRONMENT", "production" } - } -}); -``` - -### Lambda with Custom Memory and Timeout +### Static Website ```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 - { - { "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 - { - { "ENVIRONMENT", "production" }, - { "CORS_ORIGINS", "*" } - } -}); - -// Access the function URL domain -string functionUrlDomain = lambdaConstruct.LiveAliasFunctionUrlDomain; -Console.WriteLine($"Function URL: https://{functionUrlDomain}"); -``` - -## Features - -### ๐Ÿš€ LambdaFunctionConstruct - -The main construct that creates a complete Lambda function setup with: - -- **Lambda Function**: Configured for custom runtimes (`PROVIDED_AL2023`) with bootstrap handler -- **IAM Role & Policy**: Automatic CloudWatch Logs permissions + custom policy statements -- **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 - -### ๐ŸŒ StaticSiteConstruct - -A comprehensive construct for hosting static websites with global content delivery: - -- **S3 Static Website**: Bucket configured for website hosting with proper public access -- **CloudFront Distribution**: Global CDN with custom error pages and caching optimization -- **SSL Certificate**: Automatic SSL/TLS certificate with DNS validation -- **Route53 DNS**: A records with CloudFront alias targets for the domain and alternates -- **Optional API Proxying**: Forward `/api/*` requests to a backend API domain -- **Asset Deployment**: Automatic deployment of static assets with cache invalidation - -#### Quick Start - Static Site - -```csharp -using Amazon.CDK; -using LayeredCraft.Cdk.Constructs; -using LayeredCraft.Cdk.Constructs.Models; - -public class MyStack : Stack -{ - public MyStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) - { - var staticSite = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps - { - DomainName = "example.com", - SiteSubDomain = "www", - AssetPath = "./build" - }); - } -} -``` - -#### Static Site with API Proxying - -```csharp -var staticSite = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps +var website = new StaticSiteConstruct(this, "Website", new StaticSiteConstructProps { + SiteBucketName = "my-website-bucket", DomainName = "example.com", - SiteSubDomain = "app", - AssetPath = "./build", - ApiDomain = "api.example.com", // Proxy /api/* to this domain - AlternateDomains = ["example.com", "www.example.com"] + SiteSubDomain = "www", + AssetPath = "./website-build" }); ``` -#### Static Site Configuration Options - -| Property | Type | Description | Default | -|----------|------|-------------|---------| -| `DomainName` | `string` | Root domain name (e.g., "example.com") | Required | -| `SiteSubDomain` | `string` | Subdomain for the site (e.g., "www", "app") | Required | -| `AssetPath` | `string` | Path to static assets directory | Required | -| `ApiDomain` | `string?` | API domain for /api/* proxying | `null` | -| `AlternateDomains` | `string[]` | Additional domains to serve content | `[]` | - -### ๐Ÿ—ƒ๏ธ DynamoDbTableConstruct - -A comprehensive construct for creating DynamoDB tables with advanced configuration: - -- **Flexible Key Schema**: Support for partition keys, sort keys, and composite keys -- **Global Secondary Indexes**: Easy GSI configuration with automatic outputs -- **DynamoDB Streams**: Real-time data processing with Lambda integration -- **Time-to-Live (TTL)**: Automatic data expiration for cost optimization -- **CloudFormation Outputs**: Automatic export of table ARN, name, and stream ARN -- **Stream Lambda Integration**: Built-in method for attaching Lambda functions to streams -- **Billing Flexibility**: Support for both PAY_PER_REQUEST and PROVISIONED billing modes - -#### Quick Start - DynamoDB Table - -```csharp -using Amazon.CDK; -using Amazon.CDK.AWS.DynamoDB; -using LayeredCraft.Cdk.Constructs; -using LayeredCraft.Cdk.Constructs.Models; - -public class MyStack : Stack -{ - public MyStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) - { - var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps - { - TableName = "users", - PartitionKey = new Attribute { Name = "userId", Type = AttributeType.STRING }, - SortKey = new Attribute { Name = "itemId", Type = AttributeType.STRING }, - BillingMode = BillingMode.PAY_PER_REQUEST, - RemovalPolicy = RemovalPolicy.DESTROY - }); - } -} -``` - -#### DynamoDB Table with Global Secondary Index +### DynamoDB Table ```csharp -var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps +var table = new DynamoDbTableConstruct(this, "UserTable", new DynamoDbTableConstructProps { TableName = "users", - PartitionKey = new Attribute { Name = "userId", Type = AttributeType.STRING }, - SortKey = new Attribute { Name = "itemId", Type = AttributeType.STRING }, - BillingMode = BillingMode.PAY_PER_REQUEST, - RemovalPolicy = RemovalPolicy.DESTROY, - GlobalSecondaryIndexes = - [ - new GlobalSecondaryIndexProps + PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING }, + GlobalSecondaryIndexes = [ + new GlobalSecondaryIndex { - IndexName = "GSI1", - PartitionKey = new Attribute { Name = "itemType", Type = AttributeType.STRING }, - SortKey = new Attribute { Name = "createdAt", Type = AttributeType.STRING } + IndexName = "email-index", + PartitionKey = new AttributeDefinition { AttributeName = "email", AttributeType = AttributeType.STRING }, + ProjectionType = ProjectionType.ALL } ] }); ``` -#### DynamoDB Table with Streams and Lambda Integration - -```csharp -var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps -{ - TableName = "events", - PartitionKey = new Attribute { Name = "aggregateId", Type = AttributeType.STRING }, - SortKey = new Attribute { Name = "eventId", Type = AttributeType.STRING }, - BillingMode = BillingMode.PAY_PER_REQUEST, - RemovalPolicy = RemovalPolicy.DESTROY, - Stream = StreamViewType.NEW_AND_OLD_IMAGES, - TimeToLiveAttribute = "expiresAt" -}); - -// Attach a Lambda function to process stream events -var streamProcessor = new LambdaFunctionConstruct(this, "StreamProcessor", new LambdaFunctionConstructProps -{ - FunctionName = "stream-processor", - FunctionSuffix = "prod", - AssetPath = "./lambda-deployment.zip", - RoleName = "stream-processor-role", - PolicyName = "stream-processor-policy" -}); - -table.AttachStreamLambda(streamProcessor.Function); -``` - -#### DynamoDB Configuration Options - -| Property | Type | Description | Default | -|----------|------|-------------|---------| -| `TableName` | `string` | Name of the DynamoDB table | Required | -| `PartitionKey` | `IAttribute?` | Partition key attribute definition | `null` | -| `SortKey` | `IAttribute?` | Sort key attribute definition | `null` | -| `RemovalPolicy` | `RemovalPolicy` | Stack removal policy | Required | -| `BillingMode` | `BillingMode` | Billing mode (PAY_PER_REQUEST or PROVISIONED) | Required | -| `GlobalSecondaryIndexes` | `GlobalSecondaryIndexProps[]` | Array of GSI configurations | `[]` | -| `Stream` | `StreamViewType?` | Stream view type if streams enabled | `null` | -| `TimeToLiveAttribute` | `string?` | TTL attribute name | `null` | - -### ๐Ÿ”ง Lambda Configuration Options - -| Property | Type | Description | Default | -|----------|------|-------------|---------| -| `FunctionName` | `string` | Base name for the Lambda function | Required | -| `FunctionSuffix` | `string` | Suffix appended to function name | Required | -| `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` | Environment variables | `{}` | -| `IncludeOtelLayer` | `bool` | Enable OpenTelemetry layer | `true` | -| `EnableSnapStart` | `bool` | Enable AWS Lambda SnapStart | `false` | -| `Permissions` | `List` | Lambda invocation permissions | `[]` | - -### ๐Ÿ” OpenTelemetry Integration - -When `IncludeOtelLayer` is enabled (default), the construct automatically: -- Adds the AWS OTEL Collector layer -- Enables X-Ray tracing -- Configures the Lambda for observability - -### ๐Ÿ“‹ Automatic IAM Permissions - -The construct automatically grants these CloudWatch Logs permissions: -- `logs:CreateLogStream` -- `logs:CreateLogGroup` -- `logs:TagResource` -- `logs:PutLogEvents` - -## Testing Support - -The library includes comprehensive testing helpers to make it easy to unit test your CDK stacks that use these constructs. - -### Testing Helpers Overview - -- **`CdkTestHelper`**: Reduces boilerplate for creating test stacks and props (including custom stack types) -- **`LambdaFunctionConstructAssertions`**: Extension methods for asserting Lambda resources -- **`LambdaFunctionConstructPropsBuilder`**: Fluent builder for creating test props -- **`StaticSiteConstructAssertions`**: Extension methods for asserting static site resources -- **`StaticSiteConstructPropsBuilder`**: Fluent builder for creating static site test props - -### Quick Testing Example - -```csharp -using LayeredCraft.Cdk.Constructs; -using LayeredCraft.Cdk.Constructs.Testing; - -[Fact] -public void MyStack_ShouldCreateApiFunction() -{ - // Create test infrastructure with minimal boilerplate - var stack = CdkTestHelper.CreateTestStackMinimal(); - - // Build test props using fluent builder - 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") - .WithOtelEnabled(true) - .Build(); - - // Create the construct - _ = new LambdaFunctionConstruct(stack, "MyApiLambda", props); - - // Create template AFTER adding constructs to the stack - var template = Template.FromStack(stack); - - // Use assertion helpers for clean, readable tests - template.ShouldHaveLambdaFunction("my-api-prod"); - template.ShouldHaveOtelLayer(); - template.ShouldHaveCloudWatchLogsPermissions("my-api-prod"); - template.ShouldHaveEnvironmentVariables(new Dictionary - { - { "TABLE_NAME", "users-table" } - }); - template.ShouldHaveLambdaPermissions(1); // 1 permission = 3 resources (function + version + alias) - template.ShouldHaveVersionAndAlias("live"); - template.ShouldHaveLogGroup("my-api-prod", 14); -} -``` - -### Example: Testing Without OpenTelemetry - -```csharp -[Fact] -public void MyStack_ShouldCreateLegacyFunction() -{ - var stack = CdkTestHelper.CreateTestStackMinimal(); - - var props = CdkTestHelper.CreatePropsBuilder("./legacy-lambda.zip") - .WithFunctionName("legacy-function") - .WithOtelEnabled(false) - .Build(); - - _ = new LambdaFunctionConstruct(stack, "LegacyLambda", props); - - // Create template AFTER adding constructs to the stack - var template = Template.FromStack(stack); - - template.ShouldHaveLambdaFunction("legacy-function-test"); - template.ShouldNotHaveOtelLayer(); - template.ShouldHaveCloudWatchLogsPermissions("legacy-function-test"); -} -``` - -### Example: Testing StaticSiteConstruct - -```csharp -using LayeredCraft.Cdk.Constructs; -using LayeredCraft.Cdk.Constructs.Testing; - -[Fact] -public void MyStack_ShouldCreateStaticSiteWithApiProxy() -{ - // Create test infrastructure - var stack = CdkTestHelper.CreateTestStackMinimal(); - - // Build test props using fluent builder - var props = CdkTestHelper.CreateStaticSitePropsBuilder() - .WithDomainName("myapp.com") - .WithSiteSubDomain("app") - .WithApiDomain("api.myapp.com") - .WithAlternateDomains("myapp.com", "www.myapp.com") - .Build(); - - // Create the construct - _ = new StaticSiteConstruct(stack, "MyAppSite", props); - - // Create template AFTER adding constructs to the stack - var template = Template.FromStack(stack); - - // Use assertion helpers for clean, readable tests - template.ShouldHaveCompleteStaticSite("app.myapp.com", "app.myapp.com", - hasApiProxy: true, domainCount: 3); - template.ShouldHaveApiProxyBehavior("api.myapp.com"); - template.ShouldHaveRoute53Records("app.myapp.com"); - template.ShouldHaveRoute53Records("myapp.com"); - template.ShouldHaveRoute53Records("www.myapp.com"); -} -``` - -### Example: Testing Static Site Scenarios - -```csharp -[Fact] -public void MyStack_ShouldCreateBlogSite() -{ - var stack = CdkTestHelper.CreateTestStackMinimal(); - - var props = CdkTestHelper.CreateStaticSitePropsBuilder() - .ForBlog("myblog.com") - .Build(); - - _ = new StaticSiteConstruct(stack, "BlogSite", props); - - var template = Template.FromStack(stack); - - template.ShouldHaveCompleteStaticSite("www.myblog.com", "www.myblog.com", - hasApiProxy: false, domainCount: 2); - template.ShouldHaveRoute53Records("www.myblog.com"); - template.ShouldHaveRoute53Records("myblog.com"); -} -``` - -### Example: Testing DynamoDbTableConstruct - -```csharp -using LayeredCraft.Cdk.Constructs; -using LayeredCraft.Cdk.Constructs.Testing; - -[Fact] -public void MyStack_ShouldCreateDynamoDbTable() -{ - // Create test infrastructure - var stack = CdkTestHelper.CreateTestStackMinimal(); - - // Build test props using fluent builder - var props = CdkTestHelper.CreateDynamoDbTablePropsBuilder() - .WithTableName("users") - .WithPartitionKey("userId") - .WithSortKey("itemId") - .WithGlobalSecondaryIndex("GSI1", "itemType", AttributeType.STRING, "createdAt") - .WithStream(StreamViewType.NEW_AND_OLD_IMAGES) - .WithTimeToLiveAttribute("expiresAt") - .Build(); - - // Create the construct - _ = new DynamoDbTableConstruct(stack, "UsersTable", props); - - // Create template AFTER adding constructs to the stack - var template = Template.FromStack(stack); - - // Use assertion helpers for clean, readable tests - template.ShouldHaveDynamoTable("users"); - template.ShouldHavePartitionKey("userId", AttributeType.STRING); - template.ShouldHaveSortKey("itemId", AttributeType.STRING); - template.ShouldHaveGlobalSecondaryIndex("GSI1"); - template.ShouldHaveTableStream(StreamViewType.NEW_AND_OLD_IMAGES); - template.ShouldHaveTimeToLiveAttribute("expiresAt"); - template.ShouldHaveTableOutputs("test-stack", "UsersTable"); -} -``` - -### Example: Testing DynamoDB Table Scenarios - -```csharp -[Fact] -public void MyStack_ShouldCreateSessionTable() -{ - var stack = CdkTestHelper.CreateTestStackMinimal(); - - var props = CdkTestHelper.CreateDynamoDbTablePropsBuilder() - .ForSessionTable("user-sessions") - .Build(); - - _ = new DynamoDbTableConstruct(stack, "SessionsTable", props); - - var template = Template.FromStack(stack); - - template.ShouldHaveDynamoTable("user-sessions"); - template.ShouldHavePartitionKey("sessionId", AttributeType.STRING); - template.ShouldHaveTimeToLiveAttribute("expiresAt"); - template.ShouldHaveTableStream(StreamViewType.NEW_AND_OLD_IMAGES); -} -``` - -### Example: Testing Custom Stack Types - -For projects with custom stack implementations (inheriting from `Stack`), you can use the generic methods to create your custom stack types directly: - -```csharp -[Fact] -public void MyCustomStack_ShouldCreateInfrastructure() -{ - // Custom stack props (could be your LightsaberStackProps, InfraStackProps, etc.) - var customProps = new MyCustomStackProps - { - Env = new Environment { Account = "123456789012", Region = "us-east-1" }, - Context = new MyContext { /* your custom context */ }, - Tags = new Dictionary { { "Environment", "test" } } - }; - - // Create your custom stack type directly - no unused base stack needed! - var customStack = CdkTestHelper.CreateTestStackMinimal("test-stack", customProps); - - // Your custom stack is ready to use - var template = Template.FromStack(customStack); - - // Verify your custom stack's infrastructure - template.ShouldHaveLambdaFunction("my-function-test"); - // ... other assertions -} -``` - -**Benefits of Generic Stack Creation:** -- **No Workarounds**: Create custom stack types directly instead of creating unused base stacks -- **Type Safety**: Get strongly-typed stack instances with full IntelliSense support -- **Clean Tests**: Eliminate boilerplate while maintaining flexibility -- **Real-World Ready**: Works with any custom stack implementation -- **Constructor Support**: Supports both public and internal constructors (CDK default) +## Documentation -### Example: Testing with Custom Stack Props Types +๐Ÿ“– **[Complete Documentation](https://layeredcraft.github.io/cdk-constructs/)** -For advanced scenarios where your custom stack expects specific props interfaces (not just `IStackProps`), you can use the dual-generic methods: - -```csharp -[Fact] -public void MyAdvancedStack_ShouldWorkWithCustomProps() -{ - // Custom props interface extending IStackProps - var customProps = new MyCustomStackProps - { - Env = new Environment { Account = "123456789012", Region = "us-east-1" }, - Context = new MyContext { DatabaseUrl = "test-db-url" }, - FeatureFlags = new Dictionary { { "EnableNewFeature", true } } - }; - - // Use dual generics for exact type matching - solves Activator.CreateInstance issues - var stack = CdkTestHelper.CreateTestStackMinimal( - "advanced-test", customProps); - - // Your stack gets the exact props type it expects - var template = Template.FromStack(stack); - - // Test with full type safety - template.ShouldHaveLambdaFunction("advanced-function"); - stack.DatabaseUrl.Should().Be("test-db-url"); // Custom props available - stack.IsNewFeatureEnabled.Should().Be(true); -} -``` - -**When to Use Dual Generics:** -- Your stack constructor expects a specific props interface (e.g., `ILightsaberStackProps`, `IInfraStackProps`) -- You need exact type matching for `Activator.CreateInstance` to work correctly -- You want full IntelliSense support for your custom props throughout the test - -### Supported Constructor Patterns - -The generic methods work with both public and internal constructors using reflection with `BindingFlags`: - -```csharp -// โœ… Works - Public constructor -public class MyStack : Stack -{ - public MyStack(Construct scope, string id, IStackProps props) : base(scope, id, props) { } -} - -// โœ… Works - Internal constructor (CDK default pattern) -public class MyStack : Stack -{ - internal MyStack(Construct scope, string id, IStackProps props) : base(scope, id, props) { } -} - -// โœ… Works - Custom props interface with internal constructor -public class MyStack : Stack -{ - internal MyStack(Construct scope, string id, IMyCustomProps props) : base(scope, id, props) { } -} -``` - -**Important Notes:** -- The helpers use `BindingFlags.NonPublic` to access internal constructors -- This follows common testing library patterns for accessing non-public members -- Works seamlessly with CDK's default internal constructor pattern - -### Test Asset Management - -Create a `TestAssets` folder in your test project and place your deployment packages there: - -``` -YourTestProject/ -โ”œโ”€โ”€ TestAssets/ -โ”‚ โ”œโ”€โ”€ test-lambda.zip # Default Lambda test asset -โ”‚ โ”œโ”€โ”€ my-custom-lambda.zip # Custom Lambda test assets -โ”‚ โ””โ”€โ”€ static-site/ # Static site test assets -โ”‚ โ”œโ”€โ”€ index.html -โ”‚ โ”œโ”€โ”€ styles.css -โ”‚ โ”œโ”€โ”€ about.html -โ”‚ โ””โ”€โ”€ app.js -โ””โ”€โ”€ YourTests.cs -``` - -The testing helpers automatically resolve test asset paths: - -```csharp -// Lambda testing - uses TestAssets/test-lambda.zip by default -var lambdaProps = CdkTestHelper.CreatePropsBuilder().Build(); - -// Static site testing - uses TestAssets/static-site by default -var staticProps = CdkTestHelper.CreateStaticSitePropsBuilder().Build(); - -// Or specify your own test assets -var customLambdaProps = CdkTestHelper.CreatePropsBuilder("./TestAssets/my-custom-lambda.zip").Build(); -var customStaticProps = CdkTestHelper.CreateStaticSitePropsBuilder("./TestAssets/my-site").Build(); - -// Get reliable paths to test assets -var lambdaAssetPath = CdkTestHelper.GetTestAssetPath("TestAssets/my-lambda.zip"); -var staticAssetPath = CdkTestHelper.GetTestAssetPath("TestAssets/my-site"); -``` - -### Available Assertion Methods - -#### Lambda Function Assertions - -| Method | Description | -|--------|-------------| -| `ShouldHaveLambdaFunction(functionName)` | Verify Lambda function exists | -| `ShouldHaveOtelLayer()` | Verify OpenTelemetry layer and tracing are enabled | -| `ShouldNotHaveOtelLayer()` | Verify OpenTelemetry is disabled | -| `ShouldHaveCloudWatchLogsPermissions(functionName)` | Verify log permissions | -| `ShouldHaveEnvironmentVariables(variables)` | Verify environment variables | -| `ShouldHaveLambdaPermissions(count)` | Verify Lambda invoke permissions | -| `ShouldHaveVersionAndAlias(aliasName)` | Verify versioning setup | -| `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 - -| Method | Description | -|--------|-------------| -| `ShouldHaveCompleteStaticSite(bucketName, domainName, hasApiProxy, domainCount)` | Verify complete static site setup | -| `ShouldHaveStaticWebsiteBucket(bucketName)` | Verify S3 bucket with website configuration | -| `ShouldHaveCloudFrontDistribution(domainName)` | Verify CloudFront distribution | -| `ShouldHaveSSLCertificate(domainName)` | Verify SSL certificate | -| `ShouldHaveRoute53Records(domainName)` | Verify Route53 A records | -| `ShouldHaveMultipleRoute53Records(count)` | Verify multiple domain records | -| `ShouldHaveApiProxyBehavior(apiDomain)` | Verify API proxy behavior | -| `ShouldNotHaveApiProxyBehavior()` | Verify no API proxy behavior | -| `ShouldHaveBucketDeployment()` | Verify asset deployment | - -#### DynamoDB Table Assertions - -| Method | Description | -|--------|-------------| -| `ShouldHaveDynamoTable(tableName)` | Verify DynamoDB table exists | -| `ShouldHavePartitionKey(keyName, keyType)` | Verify partition key configuration | -| `ShouldHaveSortKey(keyName, keyType)` | Verify sort key configuration | -| `ShouldHaveGlobalSecondaryIndex(indexName)` | Verify GSI exists | -| `ShouldHaveTableStream(streamType)` | Verify stream configuration | -| `ShouldHaveTimeToLiveAttribute(attributeName)` | Verify TTL attribute | -| `ShouldHaveTableOutputs(stackName, constructId)` | Verify CloudFormation outputs | -| `ShouldHaveGlobalSecondaryIndexCount(count)` | Verify number of GSIs | - -### Props Builder Methods - -#### Lambda Function Props Builder - -| Method | Description | -|--------|-------------| -| `WithFunctionName(name)` | Set function name | -| `WithFunctionSuffix(suffix)` | Set function suffix | -| `WithAssetPath(path)` | Set Lambda deployment package path | -| `WithOtelEnabled(bool)` | Enable/disable OpenTelemetry | -| `WithDynamoDbAccess(tableName)` | Add DynamoDB permissions | -| `WithS3Access(bucketName)` | Add S3 permissions | -| `WithApiGatewayPermission(apiArn)` | Add API Gateway invoke permission | -| `WithAlexaPermission(skillId)` | Add Alexa Skills Kit permission | -| `WithEnvironmentVariable(key, value)` | Add environment variable | -| `WithEnvironmentVariables(variables)` | Add multiple environment variables | -| `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 - -| Method | Description | -|--------|-------------| -| `WithDomainName(domain)` | Set root domain name | -| `WithSiteSubDomain(subdomain)` | Set site subdomain | -| `WithAssetPath(path)` | Set static assets directory path | -| `WithApiDomain(apiDomain)` | Set API domain for proxying | -| `WithoutApiDomain()` | Remove API domain | -| `WithAlternateDomain(domain)` | Add single alternate domain | -| `WithAlternateDomains(domains...)` | Add multiple alternate domains | -| `WithoutAlternateDomains()` | Clear all alternate domains | -| `ForBlog(domain)` | Configure for blog scenario (www + naked domain) | -| `ForDocumentation(domain, apiDomain)` | Configure for docs scenario with API | -| `ForSinglePageApp(domain, apiDomain)` | Configure for SPA scenario with API | - -#### DynamoDB Table Props Builder - -| Method | Description | -|--------|-------------| -| `WithTableName(name)` | Set table name | -| `WithPartitionKey(keyName, keyType)` | Set partition key | -| `WithSortKey(keyName, keyType)` | Set sort key | -| `WithRemovalPolicy(policy)` | Set removal policy | -| `WithBillingMode(mode)` | Set billing mode | -| `WithGlobalSecondaryIndex(indexName, partitionKey, partitionKeyType, sortKey, sortKeyType)` | Add GSI | -| `WithStream(streamViewType)` | Enable streams | -| `WithoutStream()` | Disable streams | -| `WithTimeToLiveAttribute(attributeName)` | Set TTL attribute | -| `WithoutTimeToLiveAttribute()` | Remove TTL attribute | -| `ForUserTable(tableName)` | Configure for user data scenario | -| `ForSessionTable(tableName)` | Configure for session management scenario | -| `ForEventSourcing(tableName)` | Configure for event sourcing scenario | - -## Project Structure - -``` -โ”œโ”€โ”€ src/ -โ”‚ โ””โ”€โ”€ LayeredCraft.Cdk.Constructs/ # Main library package -โ”‚ โ”œโ”€โ”€ LambdaFunctionConstruct.cs # Lambda function construct with IAM and logging -โ”‚ โ”œโ”€โ”€ StaticSiteConstruct.cs # Static site construct with CDN and DNS -โ”‚ โ”œโ”€โ”€ DynamoDbTableConstruct.cs # DynamoDB table construct with GSI and streams -โ”‚ โ”œโ”€โ”€ Extensions/ -โ”‚ โ”‚ โ””โ”€โ”€ StackExtensions.cs # CDK Stack extension methods -โ”‚ โ”œโ”€โ”€ Models/ -โ”‚ โ”‚ โ”œโ”€โ”€ LambdaFunctionConstructProps.cs # Configuration props for Lambda construct -โ”‚ โ”‚ โ”œโ”€โ”€ LambdaPermission.cs # Lambda permission model -โ”‚ โ”‚ โ”œโ”€โ”€ StaticSiteConstructProps.cs # Configuration props for static site construct -โ”‚ โ”‚ โ””โ”€โ”€ DynamoDbTableConstructProps.cs # Configuration props for DynamoDB table construct -โ”‚ โ””โ”€โ”€ Testing/ # Testing helpers for consumers -โ”‚ โ”œโ”€โ”€ CdkTestHelper.cs # Test stack and props creation utilities -โ”‚ โ”œโ”€โ”€ LambdaFunctionConstructAssertions.cs # Extension methods for Lambda assertions -โ”‚ โ”œโ”€โ”€ LambdaFunctionConstructPropsBuilder.cs # Fluent builder for Lambda test props -โ”‚ โ”œโ”€โ”€ StaticSiteConstructAssertions.cs # Extension methods for static site assertions -โ”‚ โ”œโ”€โ”€ StaticSiteConstructPropsBuilder.cs # Fluent builder for static site test props -โ”‚ โ”œโ”€โ”€ DynamoDbTableConstructAssertions.cs # Extension methods for DynamoDB table assertions -โ”‚ โ””โ”€โ”€ DynamoDbTableConstructPropsBuilder.cs # Fluent builder for DynamoDB table test props -โ”œโ”€โ”€ test/ -โ”‚ โ””โ”€โ”€ LayeredCraft.Cdk.Constructs.Tests/ # Test suite -โ”‚ โ”œโ”€โ”€ LambdaFunctionConstructTests.cs # Unit tests for Lambda construct -โ”‚ โ”œโ”€โ”€ StaticSiteConstructTests.cs # Unit tests for static site construct -โ”‚ โ”œโ”€โ”€ DynamoDbTableConstructTests.cs # Unit tests for DynamoDB table construct -โ”‚ โ”œโ”€โ”€ StackExtensionsTests.cs # Unit tests for Stack extension methods -โ”‚ โ”œโ”€โ”€ Testing/ -โ”‚ โ”‚ โ””โ”€โ”€ TestingHelpersTests.cs # Tests for testing helper functionality -โ”‚ โ”œโ”€โ”€ TestKit/ # Test utilities and fixtures -โ”‚ โ”‚ โ”œโ”€โ”€ Attributes/ # Custom AutoFixture attributes -โ”‚ โ”‚ โ”œโ”€โ”€ Customizations/ # Test data customizations -โ”‚ โ”‚ โ””โ”€โ”€ Extensions/ # Test extension methods -โ”‚ โ””โ”€โ”€ TestAssets/ -โ”‚ โ”œโ”€โ”€ test-lambda.zip # Test Lambda deployment package -โ”‚ โ””โ”€โ”€ static-site/ # Test static site assets -โ”‚ โ”œโ”€โ”€ index.html -โ”‚ โ”œโ”€โ”€ styles.css -โ”‚ โ”œโ”€โ”€ about.html -โ”‚ โ””โ”€โ”€ app.js -โ””โ”€โ”€ LayeredCraft.Cdk.Constructs.sln # Solution file -``` +- **[Lambda Function Construct](https://layeredcraft.github.io/cdk-constructs/constructs/lambda-function/)** - Full-featured Lambda functions with OpenTelemetry, IAM, and more +- **[Static Site Construct](https://layeredcraft.github.io/cdk-constructs/constructs/static-site/)** - Complete static website hosting with CloudFront and SSL +- **[DynamoDB Table Construct](https://layeredcraft.github.io/cdk-constructs/constructs/dynamodb-table/)** - Production-ready DynamoDB tables with streams and indexes +- **[Testing Guide](https://layeredcraft.github.io/cdk-constructs/testing/)** - Comprehensive testing utilities and patterns +- **[Examples](https://layeredcraft.github.io/cdk-constructs/examples/)** - Real-world usage examples and patterns ## Requirements -- **.NET 8.0 or .NET 9.0** +- **.NET 8.0** or **.NET 9.0** - **AWS CDK v2** (Amazon.CDK.Lib 2.203.1+) - **AWS CLI** configured with appropriate permissions - **Node.js** (for CDK deployment) -## Runtime Configuration - -The Lambda functions created by this construct use: -- **Runtime**: `PROVIDED_AL2023` (Amazon Linux 2023) -- **Handler**: `bootstrap` (for custom runtimes) -- **Memory**: 1024 MB -- **Timeout**: 6 seconds -- **Log Retention**: 2 weeks +## Contributing -## Development +We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details. -### Building the Project +### Development Setup ```bash -# Build the solution +# Clone the repository +git clone https://github.com/LayeredCraft/cdk-constructs.git +cd cdk-constructs + +# Restore dependencies +dotnet restore + +# Build the project dotnet build # Run tests dotnet run --project test/LayeredCraft.Cdk.Constructs.Tests/ --framework net8.0 - -# Pack for local testing -dotnet pack src/LayeredCraft.Cdk.Constructs/ ``` -### Testing +### Code Style -The project uses xUnit v3 with AutoFixture for comprehensive testing: - -```bash -# Run all tests -cd test/LayeredCraft.Cdk.Constructs.Tests -dotnet run --framework net8.0 - -# Run specific test -dotnet run --framework net8.0 -- --filter "Construct_ShouldCreateLambdaFunction" -``` - -## Contributing - -1. Fork the repository -2. Create a feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m 'Add amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request +- Follow C# coding conventions +- Use meaningful names for variables and methods +- Add XML documentation for public APIs +- Include unit tests for new features +- Run tests before submitting PRs ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +This project is licensed under the [MIT License](LICENSE). ## Support -For issues and questions: -- ๐Ÿ› [Report bugs](https://github.com/LayeredCraft/cdk-constructs/issues) -- ๐Ÿ’ก [Request features](https://github.com/LayeredCraft/cdk-constructs/issues) -- ๐Ÿ“– [Documentation](https://github.com/LayeredCraft/cdk-constructs/wiki) +- **Issues**: [GitHub Issues](https://github.com/LayeredCraft/cdk-constructs/issues) +- **Discussions**: [GitHub Discussions](https://github.com/LayeredCraft/cdk-constructs/discussions) +- **Documentation**: [https://layeredcraft.github.io/cdk-constructs/](https://layeredcraft.github.io/cdk-constructs/) + +## Changelog + +See [CHANGELOG.md](CHANGELOG.md) for details on releases and changes. --- -**Built with โค๏ธ by the LayeredCraft team** +Built with โค๏ธ by the LayeredCraft team \ No newline at end of file diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..4d92a68 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,49 @@ +title: LayeredCraft CDK Constructs +description: Reusable AWS CDK constructs for .NET serverless applications +baseurl: "/cdk-constructs" +url: "https://layeredcraft.github.io" + +# Build settings +markdown: kramdown +theme: architect +plugins: + - jekyll-feed + - jekyll-sitemap + +# Navigation +header_pages: + - index.md + - constructs/lambda-function.md + - constructs/static-site.md + - constructs/dynamodb-table.md + - testing/index.md + - examples/index.md + +# GitHub repository +github_username: LayeredCraft +repository: LayeredCraft/cdk-constructs + +# Exclude from processing +exclude: + - .sass-cache/ + - .jekyll-cache/ + - gemfiles/ + - Gemfile + - Gemfile.lock + - node_modules/ + - vendor/bundle/ + - vendor/cache/ + - vendor/gems/ + - vendor/ruby/ + +# Collections +collections: + constructs: + output: true + permalink: /:collection/:name/ + testing: + output: true + permalink: /:collection/:name/ + examples: + output: true + permalink: /:collection/:name/ \ No newline at end of file diff --git a/docs/constructs/dynamodb-table.md b/docs/constructs/dynamodb-table.md new file mode 100644 index 0000000..217e58d --- /dev/null +++ b/docs/constructs/dynamodb-table.md @@ -0,0 +1,280 @@ +--- +layout: default +title: DynamoDB Table Construct +--- + +# DynamoDB Table Construct + +The `DynamoDbTableConstruct` provides a comprehensive, production-ready DynamoDB table with support for global secondary indexes, streams, TTL, and Lambda integration. + +## Features + +- **Flexible Key Schema**: Support for partition keys, sort keys, and composite keys +- **Global Secondary Indexes**: Multiple GSIs with custom key schemas +- **DynamoDB Streams**: Real-time data processing with Lambda integration +- **TTL Support**: Automatic data expiration +- **CloudFormation Outputs**: Automatic exports for table ARN, name, and stream ARN +- **Lambda Integration**: Built-in method for attaching Lambda functions to streams +- **Billing Modes**: Support for both PAY_PER_REQUEST and PROVISIONED billing + +## Basic Usage + +```csharp +using Amazon.CDK; +using LayeredCraft.Cdk.Constructs; +using LayeredCraft.Cdk.Constructs.Models; + +public class MyStack : Stack +{ + public MyStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) + { + var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps + { + TableName = "users", + PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING } + }); + } +} +``` + +## Configuration Properties + +### Required Properties + +| Property | Type | Description | +|----------|------|-------------| +| `TableName` | `string` | Name of the DynamoDB table | +| `PartitionKey` | `AttributeDefinition` | Primary partition key definition | + +### Optional Properties + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `SortKey` | `AttributeDefinition?` | `null` | Primary sort key definition | +| `GlobalSecondaryIndexes` | `GlobalSecondaryIndex[]` | `[]` | GSI definitions | +| `StreamSpecification` | `StreamViewType?` | `null` | DynamoDB stream configuration | +| `TimeToLiveAttribute` | `string?` | `null` | TTL attribute name | +| `BillingMode` | `BillingMode` | `PAY_PER_REQUEST` | Table billing mode | + +## Advanced Examples + +### Table with Sort Key + +```csharp +var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps +{ + TableName = "user-sessions", + PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING }, + SortKey = new AttributeDefinition { AttributeName = "sessionId", AttributeType = AttributeType.STRING } +}); +``` + +### Table with Global Secondary Index + +```csharp +var gsi = new GlobalSecondaryIndex +{ + IndexName = "email-index", + PartitionKey = new AttributeDefinition { AttributeName = "email", AttributeType = AttributeType.STRING }, + ProjectionType = ProjectionType.ALL +}; + +var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps +{ + TableName = "users", + PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING }, + GlobalSecondaryIndexes = [gsi] +}); +``` + +### Table with DynamoDB Streams + +```csharp +var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps +{ + TableName = "events", + PartitionKey = new AttributeDefinition { AttributeName = "eventId", AttributeType = AttributeType.STRING }, + StreamSpecification = StreamViewType.NEW_AND_OLD_IMAGES +}); +``` + +### Table with TTL + +```csharp +var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps +{ + TableName = "sessions", + PartitionKey = new AttributeDefinition { AttributeName = "sessionId", AttributeType = AttributeType.STRING }, + TimeToLiveAttribute = "expiresAt" // Unix timestamp field +}); +``` + +### Complete Configuration + +```csharp +var emailGsi = new GlobalSecondaryIndex +{ + IndexName = "email-index", + PartitionKey = new AttributeDefinition { AttributeName = "email", AttributeType = AttributeType.STRING }, + ProjectionType = ProjectionType.ALL +}; + +var statusGsi = new GlobalSecondaryIndex +{ + IndexName = "status-index", + PartitionKey = new AttributeDefinition { AttributeName = "status", AttributeType = AttributeType.STRING }, + SortKey = new AttributeDefinition { AttributeName = "createdAt", AttributeType = AttributeType.NUMBER }, + ProjectionType = ProjectionType.KEYS_ONLY +}; + +var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps +{ + TableName = "users", + PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING }, + SortKey = new AttributeDefinition { AttributeName = "createdAt", AttributeType = AttributeType.NUMBER }, + GlobalSecondaryIndexes = [emailGsi, statusGsi], + StreamSpecification = StreamViewType.NEW_AND_OLD_IMAGES, + TimeToLiveAttribute = "expiresAt" +}); +``` + +## Lambda Stream Integration + +The construct provides a convenient method to attach Lambda functions to DynamoDB streams: + +```csharp +var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps +{ + TableName = "events", + PartitionKey = new AttributeDefinition { AttributeName = "eventId", AttributeType = AttributeType.STRING }, + StreamSpecification = StreamViewType.NEW_AND_OLD_IMAGES +}); + +// Create a Lambda function to process stream events +var streamProcessor = new LambdaFunctionConstruct(this, "StreamProcessor", new LambdaFunctionConstructProps +{ + FunctionName = "stream-processor", + FunctionSuffix = "prod", + AssetPath = "./lambda-stream-processor.zip", + RoleName = "stream-processor-role", + PolicyName = "stream-processor-policy" +}); + +// Attach Lambda to the stream +table.AttachStreamLambda(streamProcessor.LambdaFunction); +``` + +## CloudFormation Outputs + +The construct automatically creates CloudFormation outputs: + +```csharp +var table = new DynamoDbTableConstruct(this, "MyTable", props); + +// Outputs created: +// - {StackName}-MyTable-table-arn-output +// - {StackName}-MyTable-table-name-output +// - {StackName}-MyTable-table-stream-arn-output (if streams enabled) +``` + +## Stream View Types + +| View Type | Description | +|-----------|-------------| +| `KEYS_ONLY` | Only key attributes of the modified item | +| `NEW_IMAGE` | Entire item after modification | +| `OLD_IMAGE` | Entire item before modification | +| `NEW_AND_OLD_IMAGES` | Both new and old images of the item | + +## Billing Modes + +### PAY_PER_REQUEST (Default) +- No provisioned capacity required +- Scales automatically based on demand +- Pay only for actual read/write requests + +### PROVISIONED +- Specify read and write capacity units +- More predictable costs for steady workloads +- Requires capacity planning + +```csharp +var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps +{ + TableName = "users", + PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING }, + BillingMode = BillingMode.PROVISIONED, + // Note: Provisioned capacity configuration would be added in future versions +}); +``` + +## Global Secondary Index Configuration + +### Projection Types + +| Projection Type | Description | +|----------------|-------------| +| `ALL` | All attributes projected | +| `KEYS_ONLY` | Only key attributes projected | +| `INCLUDE` | Key attributes plus specified attributes | + +### GSI Example with Projection + +```csharp +var gsi = new GlobalSecondaryIndex +{ + IndexName = "status-index", + PartitionKey = new AttributeDefinition { AttributeName = "status", AttributeType = AttributeType.STRING }, + SortKey = new AttributeDefinition { AttributeName = "updatedAt", AttributeType = AttributeType.NUMBER }, + ProjectionType = ProjectionType.INCLUDE, + NonKeyAttributes = ["name", "email"] // Only with INCLUDE projection +}; +``` + +## Common Use Cases + +### User Management Table +```csharp +var userTable = new DynamoDbTableConstruct(this, "UserTable", new DynamoDbTableConstructProps +{ + TableName = "users", + PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING }, + GlobalSecondaryIndexes = [ + new GlobalSecondaryIndex + { + IndexName = "email-index", + PartitionKey = new AttributeDefinition { AttributeName = "email", AttributeType = AttributeType.STRING }, + ProjectionType = ProjectionType.ALL + } + ] +}); +``` + +### Session Management Table +```csharp +var sessionTable = new DynamoDbTableConstruct(this, "SessionTable", new DynamoDbTableConstructProps +{ + TableName = "sessions", + PartitionKey = new AttributeDefinition { AttributeName = "sessionId", AttributeType = AttributeType.STRING }, + TimeToLiveAttribute = "expiresAt" +}); +``` + +### Event Sourcing Table +```csharp +var eventTable = new DynamoDbTableConstruct(this, "EventTable", new DynamoDbTableConstructProps +{ + TableName = "events", + PartitionKey = new AttributeDefinition { AttributeName = "aggregateId", AttributeType = AttributeType.STRING }, + SortKey = new AttributeDefinition { AttributeName = "timestamp", AttributeType = AttributeType.NUMBER }, + StreamSpecification = StreamViewType.NEW_AND_OLD_IMAGES +}); +``` + +## Testing + +See the [Testing Guide](../testing/) for comprehensive testing utilities and patterns specific to the DynamoDB Table construct. + +## Examples + +For more real-world examples, see the [Examples](../examples/) section. \ No newline at end of file diff --git a/docs/constructs/lambda-function.md b/docs/constructs/lambda-function.md new file mode 100644 index 0000000..357c626 --- /dev/null +++ b/docs/constructs/lambda-function.md @@ -0,0 +1,212 @@ +--- +layout: default +title: Lambda Function Construct +--- + +# Lambda Function Construct + +The `LambdaFunctionConstruct` provides a comprehensive, production-ready Lambda function with integrated OpenTelemetry support, IAM management, and environment configuration. + +## Features + +- **OpenTelemetry Integration**: Built-in AWS OpenTelemetry collector layer +- **IAM Management**: Automatic role and policy creation with CloudWatch Logs permissions +- **Environment Configuration**: Easy environment variable management +- **Function URLs**: Optional HTTP endpoint generation +- **SnapStart Support**: Improved cold start performance for Java runtimes +- **Versioning & Aliases**: Automatic version management with "live" alias +- **Lambda Permissions**: Multi-target permission management (function, version, alias) + +## Basic Usage + +```csharp +using Amazon.CDK; +using LayeredCraft.Cdk.Constructs; +using LayeredCraft.Cdk.Constructs.Models; + +public class MyStack : Stack +{ + public MyStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) + { + var lambda = new LambdaFunctionConstruct(this, "MyLambda", new LambdaFunctionConstructProps + { + FunctionName = "my-api", + FunctionSuffix = "prod", + AssetPath = "./lambda-deployment.zip", + RoleName = "my-api-role", + PolicyName = "my-api-policy" + }); + } +} +``` + +## Configuration Properties + +### Required Properties + +| Property | Type | Description | +|----------|------|-------------| +| `FunctionName` | `string` | Base name of the Lambda function | +| `FunctionSuffix` | `string` | Suffix appended to function name (e.g., "prod", "dev") | +| `AssetPath` | `string` | Path to Lambda deployment package | +| `RoleName` | `string` | Name of the IAM role | +| `PolicyName` | `string` | Name of the IAM policy | + +### Optional Properties + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `MemorySize` | `double` | `1024` | Memory allocation in MB | +| `TimeoutInSeconds` | `double` | `6` | Function timeout in seconds | +| `PolicyStatements` | `PolicyStatement[]` | `[]` | Additional IAM policy statements | +| `EnvironmentVariables` | `IDictionary` | `{}` | Environment variables | +| `IncludeOtelLayer` | `bool` | `true` | Enable OpenTelemetry layer | +| `Permissions` | `List` | `[]` | Lambda invocation permissions | +| `EnableSnapStart` | `bool` | `false` | Enable SnapStart for improved cold starts | +| `GenerateUrl` | `bool` | `false` | Generate Function URL for HTTP access | + +## Advanced Examples + +### Lambda with DynamoDB Access + +```csharp +var dynamoPolicy = new PolicyStatement(new PolicyStatementProps +{ + Actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query"], + Resources = ["arn:aws:dynamodb:us-east-1:123456789012:table/MyTable"], + Effect = Effect.ALLOW +}); + +var lambda = new LambdaFunctionConstruct(this, "MyLambda", new LambdaFunctionConstructProps +{ + FunctionName = "my-api", + FunctionSuffix = "prod", + AssetPath = "./lambda-deployment.zip", + RoleName = "my-api-role", + PolicyName = "my-api-policy", + PolicyStatements = [dynamoPolicy], + EnvironmentVariables = new Dictionary + { + { "TABLE_NAME", "MyTable" }, + { "AWS_REGION", "us-east-1" } + } +}); +``` + +### Lambda with Function URL + +```csharp +var lambda = 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, // Creates HTTP endpoint + MemorySize = 2048, + TimeoutInSeconds = 30 +}); + +// Access the function URL domain +var functionUrl = lambda.LiveAliasFunctionUrlDomain; +``` + +### Lambda with API Gateway Permissions + +```csharp +var apiPermission = new LambdaPermission +{ + Principal = "apigateway.amazonaws.com", + Action = "lambda:InvokeFunction", + SourceArn = "arn:aws:execute-api:us-east-1:123456789012:abcdef123/*" +}; + +var lambda = new LambdaFunctionConstruct(this, "MyLambda", new LambdaFunctionConstructProps +{ + FunctionName = "my-api", + FunctionSuffix = "prod", + AssetPath = "./lambda-deployment.zip", + RoleName = "my-api-role", + PolicyName = "my-api-policy", + Permissions = [apiPermission] +}); +``` + +### Lambda with SnapStart + +```csharp +var lambda = new LambdaFunctionConstruct(this, "MyLambda", new LambdaFunctionConstructProps +{ + FunctionName = "my-api", + FunctionSuffix = "prod", + AssetPath = "./lambda-deployment.zip", + RoleName = "my-api-role", + PolicyName = "my-api-policy", + EnableSnapStart = true // Improves cold start performance +}); +``` + +## Public Properties + +### LambdaFunction +Access the underlying CDK Lambda function for advanced configuration: + +```csharp +var lambda = new LambdaFunctionConstruct(this, "MyLambda", props); +var underlyingFunction = lambda.LambdaFunction; + +// Add additional configuration +underlyingFunction.AddEnvironment("CUSTOM_VAR", "value"); +``` + +### LiveAliasFunctionUrlDomain +Get the domain of the Function URL (if enabled): + +```csharp +var lambda = new LambdaFunctionConstruct(this, "MyLambda", new LambdaFunctionConstructProps +{ + // ... other props + GenerateUrl = true +}); + +var domain = lambda.LiveAliasFunctionUrlDomain; // Returns the domain string +``` + +## Runtime Configuration + +The Lambda functions use the following runtime configuration: + +- **Runtime**: `PROVIDED_AL2023` (Amazon Linux 2023) +- **Handler**: `bootstrap` (for custom runtimes) +- **Architecture**: x86_64 +- **Log Retention**: 2 weeks +- **OpenTelemetry Layer**: AWS managed layer (us-east-1 region) + +## IAM Permissions + +The construct automatically creates: + +1. **CloudWatch Logs Permissions**: + - `logs:CreateLogStream` + - `logs:CreateLogGroup` + - `logs:TagResource` + - `logs:PutLogEvents` + +2. **Custom Policy Statements**: Any additional policies you provide + +3. **Lambda Permissions**: Applied to function, version, and alias + +## Versioning Strategy + +- Creates a new version on every deployment +- Maintains a "live" alias pointing to the latest version +- Versions have `RemovalPolicy.RETAIN` to prevent deletion + +## Testing + +See the [Testing Guide](../testing/) for comprehensive testing utilities and patterns specific to the Lambda Function construct. + +## Examples + +For more real-world examples, see the [Examples](../examples/) section. \ No newline at end of file diff --git a/docs/constructs/static-site.md b/docs/constructs/static-site.md new file mode 100644 index 0000000..debafd5 --- /dev/null +++ b/docs/constructs/static-site.md @@ -0,0 +1,217 @@ +--- +layout: default +title: Static Site Construct +--- + +# Static Site Construct + +The `StaticSiteConstruct` provides complete static website hosting with S3, CloudFront CDN, SSL certificates, Route53 DNS management, and optional API proxying. + +## Features + +- **S3 Website Hosting**: Optimized S3 bucket configuration for static websites +- **CloudFront CDN**: Global content delivery with custom error pages +- **SSL Certificates**: Automatic SSL certificate provisioning and management +- **Route53 DNS**: DNS record management for primary and alternate domains +- **API Proxy Support**: Optional CloudFront behavior for `/api/*` paths +- **Asset Deployment**: Automatic deployment with cache invalidation +- **Custom Error Pages**: 404 and 403 error page handling + +## Basic Usage + +```csharp +using Amazon.CDK; +using LayeredCraft.Cdk.Constructs; +using LayeredCraft.Cdk.Constructs.Models; + +public class MyStack : Stack +{ + public MyStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) + { + var site = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps + { + SiteBucketName = "my-website-bucket", + DomainName = "example.com", + AssetPath = "./website-build" + }); + } +} +``` + +## Configuration Properties + +### Required Properties + +| Property | Type | Description | +|----------|------|-------------| +| `SiteBucketName` | `string` | Name of the S3 bucket for website hosting | +| `DomainName` | `string` | Primary domain name (e.g., "example.com") | +| `AssetPath` | `string` | Path to static website assets | + +### Optional Properties + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `SiteSubDomain` | `string?` | `null` | Subdomain for the site (e.g., "www") | +| `ApiDomain` | `string?` | `null` | API domain for proxy behavior | +| `AlternateDomains` | `string[]` | `[]` | Additional domains to include in certificate | + +## Advanced Examples + +### Website with Subdomain + +```csharp +var site = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps +{ + SiteBucketName = "my-website-bucket", + DomainName = "example.com", + SiteSubDomain = "www", // Creates www.example.com + AssetPath = "./website-build" +}); +``` + +### Website with API Proxy + +```csharp +var site = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps +{ + SiteBucketName = "my-website-bucket", + DomainName = "example.com", + ApiDomain = "api.example.com", // Proxies /api/* to api.example.com + AssetPath = "./website-build" +}); +``` + +### Website with Multiple Domains + +```csharp +var site = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps +{ + SiteBucketName = "my-website-bucket", + DomainName = "example.com", + AlternateDomains = ["www.example.com", "example.org", "www.example.org"], + AssetPath = "./website-build" +}); +``` + +### Complete Configuration + +```csharp +var site = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps +{ + SiteBucketName = "my-website-bucket", + DomainName = "example.com", + SiteSubDomain = "www", + ApiDomain = "api.example.com", + AlternateDomains = ["example.org", "www.example.org"], + AssetPath = "./website-build" +}); +``` + +## Domain Resolution + +The construct automatically resolves domains based on configuration: + +### Primary Domain +- If `SiteSubDomain` is provided: `{SiteSubDomain}.{DomainName}` +- If no subdomain: `{DomainName}` + +### Certificate Domains +The SSL certificate includes: +1. Primary domain (resolved as above) +2. All domains from `AlternateDomains` array +3. API domain (if `ApiDomain` is provided) + +### Route53 Records +A records are created for: +- Primary domain +- All alternate domains + +## CloudFront Configuration + +### Default Behavior +- **Origin**: S3 website endpoint +- **Viewer Protocol**: Redirect HTTP to HTTPS +- **Caching**: Optimized for static assets +- **Index Document**: `index.html` + +### API Proxy Behavior (if enabled) +- **Path Pattern**: `/api/*` +- **Origin**: API domain +- **Viewer Protocol**: HTTPS only +- **Caching**: Disabled for API requests + +### Error Pages +- **404 Errors**: Redirected to `/index.html` (for SPA routing) +- **403 Errors**: Redirected to `/index.html` + +## S3 Bucket Configuration + +The S3 bucket is configured with: +- **Public read access** for website hosting +- **Website hosting** enabled +- **Index document**: `index.html` +- **Error document**: `index.html` (for SPA routing) +- **CORS configuration** for cross-origin requests + +## Asset Deployment + +The construct automatically: +1. Deploys all assets from `AssetPath` to S3 +2. Invalidates CloudFront cache after deployment +3. Maintains proper content types for different file extensions + +## Security Considerations + +- **HTTPS Enforcement**: All traffic is redirected to HTTPS +- **Public Access**: S3 bucket allows public read access (required for website hosting) +- **CORS**: Configured to allow necessary cross-origin requests +- **Certificate Management**: SSL certificates are automatically provisioned and renewed + +## Regional Considerations + +- **SSL Certificates**: Must be in `us-east-1` region for CloudFront +- **S3 Bucket**: Can be in any region +- **Route53**: Global service + +## Common Use Cases + +### Single Page Application (SPA) +```csharp +var spa = new StaticSiteConstruct(this, "SPA", new StaticSiteConstructProps +{ + SiteBucketName = "my-spa-bucket", + DomainName = "myapp.com", + AssetPath = "./build" // React/Vue/Angular build output +}); +``` + +### Blog with API +```csharp +var blog = new StaticSiteConstruct(this, "Blog", new StaticSiteConstructProps +{ + SiteBucketName = "my-blog-bucket", + DomainName = "myblog.com", + SiteSubDomain = "www", + ApiDomain = "api.myblog.com", // For comments, search, etc. + AssetPath = "./site" // Static site generator output +}); +``` + +### Documentation Site +```csharp +var docs = new StaticSiteConstruct(this, "Docs", new StaticSiteConstructProps +{ + SiteBucketName = "my-docs-bucket", + DomainName = "docs.mycompany.com", + AssetPath = "./docs-build" // Documentation generator output +}); +``` + +## Testing + +See the [Testing Guide](../testing/) for comprehensive testing utilities and patterns specific to the Static Site construct. + +## Examples + +For more real-world examples, see the [Examples](../examples/) section. \ No newline at end of file diff --git a/docs/examples/index.md b/docs/examples/index.md new file mode 100644 index 0000000..8a684f8 --- /dev/null +++ b/docs/examples/index.md @@ -0,0 +1,467 @@ +--- +layout: default +title: Examples +--- + +# Examples + +Real-world usage examples and patterns for LayeredCraft CDK Constructs. + +## Complete Application Examples + +### Serverless API with Database + +A complete serverless API with Lambda, DynamoDB, and API Gateway. + +```csharp +using Amazon.CDK; +using Amazon.CDK.AWS.IAM; +using LayeredCraft.Cdk.Constructs; +using LayeredCraft.Cdk.Constructs.Models; + +public class ServerlessApiStack : Stack +{ + public ServerlessApiStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) + { + // Create DynamoDB table for user data + var userTable = new DynamoDbTableConstruct(this, "UserTable", new DynamoDbTableConstructProps + { + TableName = "users", + PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING }, + GlobalSecondaryIndexes = [ + new GlobalSecondaryIndex + { + IndexName = "email-index", + PartitionKey = new AttributeDefinition { AttributeName = "email", AttributeType = AttributeType.STRING }, + ProjectionType = ProjectionType.ALL + } + ] + }); + + // Create Lambda function for API + var apiLambda = new LambdaFunctionConstruct(this, "ApiLambda", new LambdaFunctionConstructProps + { + FunctionName = "user-api", + FunctionSuffix = "prod", + AssetPath = "./api-lambda.zip", + RoleName = "user-api-role", + PolicyName = "user-api-policy", + MemorySize = 1024, + TimeoutInSeconds = 30, + PolicyStatements = [ + new PolicyStatement(new PolicyStatementProps + { + Actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query", "dynamodb:UpdateItem"], + Resources = [userTable.TableArn, $"{userTable.TableArn}/index/*"], + Effect = Effect.ALLOW + }) + ], + EnvironmentVariables = new Dictionary + { + { "TABLE_NAME", userTable.TableName }, + { "EMAIL_INDEX", "email-index" } + }, + GenerateUrl = true // Enable Function URL for direct HTTP access + }); + } +} +``` + +### Static Website with API Backend + +A static website with API backend proxying. + +```csharp +public class WebsiteStack : Stack +{ + public WebsiteStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) + { + // Create API Lambda + var apiLambda = new LambdaFunctionConstruct(this, "ApiLambda", new LambdaFunctionConstructProps + { + FunctionName = "website-api", + FunctionSuffix = "prod", + AssetPath = "./api-lambda.zip", + RoleName = "website-api-role", + PolicyName = "website-api-policy", + GenerateUrl = true + }); + + // Create static website with API proxy + var website = new StaticSiteConstruct(this, "Website", new StaticSiteConstructProps + { + SiteBucketName = "my-website-bucket", + DomainName = "mywebsite.com", + SiteSubDomain = "www", + ApiDomain = apiLambda.LiveAliasFunctionUrlDomain!, // Proxy /api/* to Lambda + AssetPath = "./website-build" + }); + } +} +``` + +### Event-Driven Architecture + +Event sourcing with DynamoDB streams and Lambda processors. + +```csharp +public class EventDrivenStack : Stack +{ + public EventDrivenStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) + { + // Event store table + var eventTable = new DynamoDbTableConstruct(this, "EventTable", new DynamoDbTableConstructProps + { + TableName = "events", + PartitionKey = new AttributeDefinition { AttributeName = "aggregateId", AttributeType = AttributeType.STRING }, + SortKey = new AttributeDefinition { AttributeName = "timestamp", AttributeType = AttributeType.NUMBER }, + StreamSpecification = StreamViewType.NEW_AND_OLD_IMAGES + }); + + // Read model table + var readModelTable = new DynamoDbTableConstruct(this, "ReadModelTable", new DynamoDbTableConstructProps + { + TableName = "user-projections", + PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING } + }); + + // Event processor Lambda + var eventProcessor = new LambdaFunctionConstruct(this, "EventProcessor", new LambdaFunctionConstructProps + { + FunctionName = "event-processor", + FunctionSuffix = "prod", + AssetPath = "./event-processor.zip", + RoleName = "event-processor-role", + PolicyName = "event-processor-policy", + PolicyStatements = [ + new PolicyStatement(new PolicyStatementProps + { + Actions = ["dynamodb:PutItem", "dynamodb:UpdateItem"], + Resources = [readModelTable.TableArn], + Effect = Effect.ALLOW + }) + ], + EnvironmentVariables = new Dictionary + { + { "READ_MODEL_TABLE", readModelTable.TableName } + } + }); + + // Connect stream to processor + eventTable.AttachStreamLambda(eventProcessor.LambdaFunction); + } +} +``` + +## Common Patterns + +### Lambda with Multiple AWS Services + +```csharp +var lambda = new LambdaFunctionConstruct(this, "MultiServiceLambda", new LambdaFunctionConstructProps +{ + FunctionName = "multi-service", + FunctionSuffix = "prod", + AssetPath = "./lambda.zip", + RoleName = "multi-service-role", + PolicyName = "multi-service-policy", + PolicyStatements = [ + // DynamoDB access + new PolicyStatement(new PolicyStatementProps + { + Actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query"], + Resources = ["arn:aws:dynamodb:us-east-1:123456789012:table/MyTable"], + Effect = Effect.ALLOW + }), + // S3 access + new PolicyStatement(new PolicyStatementProps + { + Actions = ["s3:GetObject", "s3:PutObject"], + Resources = ["arn:aws:s3:::my-bucket/*"], + Effect = Effect.ALLOW + }), + // SES access + new PolicyStatement(new PolicyStatementProps + { + Actions = ["ses:SendEmail"], + Resources = ["*"], + Effect = Effect.ALLOW + }) + ], + EnvironmentVariables = new Dictionary + { + { "TABLE_NAME", "MyTable" }, + { "BUCKET_NAME", "my-bucket" }, + { "FROM_EMAIL", "noreply@mycompany.com" } + } +}); +``` + +### Lambda with API Gateway Integration + +```csharp +// Lambda function +var apiLambda = new LambdaFunctionConstruct(this, "ApiLambda", new LambdaFunctionConstructProps +{ + FunctionName = "api-handler", + FunctionSuffix = "prod", + AssetPath = "./api-lambda.zip", + RoleName = "api-handler-role", + PolicyName = "api-handler-policy", + Permissions = [ + new LambdaPermission + { + Principal = "apigateway.amazonaws.com", + Action = "lambda:InvokeFunction", + SourceArn = "arn:aws:execute-api:us-east-1:123456789012:*/*/GET/users" + }, + new LambdaPermission + { + Principal = "apigateway.amazonaws.com", + Action = "lambda:InvokeFunction", + SourceArn = "arn:aws:execute-api:us-east-1:123456789012:*/*/POST/users" + } + ] +}); + +// API Gateway would be created separately using standard CDK constructs +``` + +### Static Site with Multiple Domains + +```csharp +var website = new StaticSiteConstruct(this, "Website", new StaticSiteConstructProps +{ + SiteBucketName = "my-website-bucket", + DomainName = "mycompany.com", + SiteSubDomain = "www", + AlternateDomains = [ + "mycompany.org", + "www.mycompany.org", + "mycompany.net", + "www.mycompany.net" + ], + AssetPath = "./website-build" +}); +``` + +### DynamoDB with Complex Indexing + +```csharp +var table = new DynamoDbTableConstruct(this, "ComplexTable", new DynamoDbTableConstructProps +{ + TableName = "user-activities", + PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING }, + SortKey = new AttributeDefinition { AttributeName = "timestamp", AttributeType = AttributeType.NUMBER }, + GlobalSecondaryIndexes = [ + // Query by activity type + new GlobalSecondaryIndex + { + IndexName = "activity-type-index", + PartitionKey = new AttributeDefinition { AttributeName = "activityType", AttributeType = AttributeType.STRING }, + SortKey = new AttributeDefinition { AttributeName = "timestamp", AttributeType = AttributeType.NUMBER }, + ProjectionType = ProjectionType.ALL + }, + // Query by status + new GlobalSecondaryIndex + { + IndexName = "status-index", + PartitionKey = new AttributeDefinition { AttributeName = "status", AttributeType = AttributeType.STRING }, + SortKey = new AttributeDefinition { AttributeName = "timestamp", AttributeType = AttributeType.NUMBER }, + ProjectionType = ProjectionType.KEYS_ONLY + } + ], + StreamSpecification = StreamViewType.NEW_AND_OLD_IMAGES, + TimeToLiveAttribute = "expiresAt" +}); +``` + +## Testing Examples + +### Complete Test Suite + +```csharp +[Collection("CDK Tests")] +public class ServerlessApiStackTests +{ + [Fact] + public void Should_Create_Complete_Api_Stack() + { + // Arrange + var app = new App(); + var stack = new ServerlessApiStack(app, "test-stack", new StackProps + { + Env = new Amazon.CDK.Environment { Account = "123456789012", Region = "us-east-1" } + }); + + // Act + var template = Template.FromStack(stack); + + // Assert + template.ShouldHaveDynamoTable("users"); + template.ShouldHaveGlobalSecondaryIndex("email-index", "email", AttributeType.STRING); + template.ShouldHaveLambdaFunction("user-api-prod"); + template.ShouldHaveFunctionUrl(); + template.ShouldHaveEnvironmentVariables(new Dictionary + { + { "TABLE_NAME", "users" }, + { "EMAIL_INDEX", "email-index" } + }); + } +} +``` + +### Parameterized Testing + +```csharp +[Collection("CDK Tests")] +public class EnvironmentSpecificTests +{ + [Theory] + [InlineData("dev", 512, 10)] + [InlineData("staging", 1024, 15)] + [InlineData("prod", 2048, 30)] + public void Should_Configure_Lambda_For_Environment(string env, int memory, int timeout) + { + // Arrange + var props = CdkTestHelper.CreatePropsBuilder(AssetPathExtensions.GetTestLambdaZipPath()) + .WithFunctionSuffix(env) + .WithMemorySize(memory) + .WithTimeoutInSeconds(timeout) + .Build(); + + var (app, stack) = CdkTestHelper.CreateTestStack("test-stack"); + + // Act + var construct = new LambdaFunctionConstruct(stack, "test-construct", props); + var template = Template.FromStack(stack); + + // Assert + template.ShouldHaveLambdaFunction($"test-function-{env}"); + template.ShouldHaveMemorySize(memory); + template.ShouldHaveTimeout(timeout); + } +} +``` + +## Best Practices Examples + +### Environment-Specific Configuration + +```csharp +public class EnvironmentConfig +{ + public string Environment { get; set; } + public int LambdaMemory { get; set; } + public int LambdaTimeout { get; set; } + public bool EnableTracing { get; set; } + public string DomainName { get; set; } +} + +public class MyStack : Stack +{ + public MyStack(Construct scope, string id, EnvironmentConfig config, IStackProps props = null) + : base(scope, id, props) + { + var lambda = new LambdaFunctionConstruct(this, "Lambda", new LambdaFunctionConstructProps + { + FunctionName = "my-function", + FunctionSuffix = config.Environment, + AssetPath = "./lambda.zip", + RoleName = $"my-function-{config.Environment}-role", + PolicyName = $"my-function-{config.Environment}-policy", + MemorySize = config.LambdaMemory, + TimeoutInSeconds = config.LambdaTimeout, + IncludeOtelLayer = config.EnableTracing + }); + + if (!string.IsNullOrEmpty(config.DomainName)) + { + var site = new StaticSiteConstruct(this, "Site", new StaticSiteConstructProps + { + SiteBucketName = $"my-site-{config.Environment}", + DomainName = config.DomainName, + AssetPath = "./site-build" + }); + } + } +} +``` + +### Resource Naming Conventions + +```csharp +public class ResourceNaming +{ + private readonly string _environment; + private readonly string _application; + + public ResourceNaming(string application, string environment) + { + _application = application; + _environment = environment; + } + + public string Lambda(string component) => $"{_application}-{component}"; + public string LambdaRole(string component) => $"{_application}-{component}-{_environment}-role"; + public string Table(string entity) => $"{_application}-{entity}-{_environment}"; + public string Bucket(string purpose) => $"{_application}-{purpose}-{_environment}"; +} + +public class MyStack : Stack +{ + public MyStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) + { + var naming = new ResourceNaming("myapp", "prod"); + + var lambda = new LambdaFunctionConstruct(this, "Lambda", new LambdaFunctionConstructProps + { + FunctionName = naming.Lambda("api"), + FunctionSuffix = "prod", + AssetPath = "./lambda.zip", + RoleName = naming.LambdaRole("api"), + PolicyName = naming.LambdaRole("api").Replace("-role", "-policy") + }); + } +} +``` + +## Deployment Examples + +### Multi-Stack Application + +```csharp +public class Program +{ + public static void Main(string[] args) + { + var app = new App(); + var env = new Amazon.CDK.Environment + { + Account = System.Environment.GetEnvironmentVariable("CDK_DEFAULT_ACCOUNT"), + Region = System.Environment.GetEnvironmentVariable("CDK_DEFAULT_REGION") + }; + + // Infrastructure stack + var infrastructure = new InfrastructureStack(app, "MyApp-Infrastructure", new StackProps { Env = env }); + + // API stack (depends on infrastructure) + var api = new ApiStack(app, "MyApp-Api", new ApiStackProps + { + Env = env, + UserTable = infrastructure.UserTable + }); + + // Website stack (depends on API) + var website = new WebsiteStack(app, "MyApp-Website", new WebsiteStackProps + { + Env = env, + ApiUrl = api.ApiUrl + }); + + app.Synth(); + } +} +``` + +For more examples, see the test files in the [repository](https://github.com/LayeredCraft/cdk-constructs/tree/main/test) which demonstrate comprehensive usage patterns. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..79441f8 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,107 @@ +--- +layout: default +title: LayeredCraft CDK Constructs +--- + +# LayeredCraft CDK Constructs + +[![Build Status](https://github.com/LayeredCraft/cdk-constructs/actions/workflows/build.yaml/badge.svg)](https://github.com/LayeredCraft/cdk-constructs/actions/workflows/build.yaml) +[![NuGet](https://img.shields.io/nuget/v/LayeredCraft.Cdk.Constructs.svg)](https://www.nuget.org/packages/LayeredCraft.Cdk.Constructs/) +[![Downloads](https://img.shields.io/nuget/dt/LayeredCraft.Cdk.Constructs.svg)](https://www.nuget.org/packages/LayeredCraft.Cdk.Constructs/) + +A comprehensive library of reusable AWS CDK constructs for .NET projects, designed for serverless applications and static websites. Built with best practices, observability, and cost efficiency in mind. + +## Key Features + +- **๐Ÿš€ Lambda Functions**: Comprehensive Lambda construct with OpenTelemetry support, IAM management, and environment configuration +- **๐ŸŒ Static Sites**: Complete static website hosting with S3, CloudFront, SSL certificates, and Route53 DNS management +- **๐Ÿ“Š DynamoDB Tables**: Full-featured DynamoDB construct with streams, TTL, and global secondary indexes +- **๐Ÿงช Testing Helpers**: Extensive testing utilities with fluent assertions and builders +- **๐Ÿ“ Type Safety**: Full TypeScript-style intellisense and compile-time validation +- **โšก Performance**: Optimized for cold starts with AWS Lambda SnapStart support + +## Installation + +```bash +dotnet add package LayeredCraft.Cdk.Constructs +``` + +## Quick Start + +```csharp +using Amazon.CDK; +using LayeredCraft.Cdk.Constructs; +using LayeredCraft.Cdk.Constructs.Models; + +public class MyStack : Stack +{ + public MyStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) + { + // Create a Lambda function + var lambda = 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 // Creates a Function URL for HTTP access + }); + + // Create a static website + var site = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps + { + SiteBucketName = "my-website-bucket", + DomainName = "example.com", + AssetPath = "./website-build" + }); + } +} +``` + +## Available Constructs + +### [Lambda Function Construct](constructs/lambda-function.md) +Full-featured Lambda functions with: +- OpenTelemetry integration +- IAM roles and policies +- Environment variables +- Function URLs +- SnapStart support +- Versioning and aliases + +### [Static Site Construct](constructs/static-site.md) +Complete static website hosting with: +- S3 website hosting +- CloudFront CDN +- SSL certificates +- Route53 DNS +- API proxy support + +### [DynamoDB Table Construct](constructs/dynamodb-table.md) +Production-ready DynamoDB tables with: +- Global secondary indexes +- DynamoDB streams +- TTL configuration +- Lambda stream integration + +## Documentation + +- **[Testing Guide](testing/)** - Comprehensive testing utilities and patterns +- **[Examples](examples/)** - Real-world usage examples and patterns +- **[API Reference](api/)** - Complete API documentation + +## Requirements + +- **.NET 8.0** or **.NET 9.0** +- **AWS CDK v2** (Amazon.CDK.Lib 2.203.1+) +- **AWS CLI** configured with appropriate permissions +- **Node.js** (for CDK deployment) + +## Contributing + +See the main [README](https://github.com/LayeredCraft/cdk-constructs#contributing) for contribution guidelines. + +## License + +This project is licensed under the [MIT License](https://github.com/LayeredCraft/cdk-constructs/blob/main/LICENSE). \ No newline at end of file diff --git a/docs/testing/index.md b/docs/testing/index.md new file mode 100644 index 0000000..a7c51c8 --- /dev/null +++ b/docs/testing/index.md @@ -0,0 +1,351 @@ +--- +layout: default +title: Testing Guide +--- + +# Testing Guide + +LayeredCraft CDK Constructs includes comprehensive testing utilities to help you write robust tests for your CDK infrastructure. The testing helpers provide fluent assertion methods, builders for test data, and utilities for common testing scenarios. + +## Overview + +The testing framework consists of: + +- **Test Helpers**: Utilities for creating test stacks and props +- **Assertion Methods**: Fluent assertions for CDK resources +- **Props Builders**: Fluent builders for construct properties +- **AutoFixture Integration**: Automatic test data generation with realistic values + +## Getting Started + +### Basic Test Structure + +```csharp +[Collection("CDK Tests")] +public class MyConstructTests +{ + [Fact] + public void Should_Create_Lambda_Function() + { + // Arrange + var (app, stack) = CdkTestHelper.CreateTestStack("test-stack"); + var props = CdkTestHelper.CreatePropsBuilder(AssetPathExtensions.GetTestLambdaZipPath()) + .WithFunctionName("my-function") + .WithMemorySize(512) + .Build(); + + // Act + var construct = new LambdaFunctionConstruct(stack, "test-construct", props); + var template = Template.FromStack(stack); + + // Assert + template.ShouldHaveLambdaFunction("my-function-test"); + template.ShouldHaveMemorySize(512); + } +} +``` + +### Important Test Collection + +All CDK construct tests **MUST** be decorated with `[Collection("CDK Tests")]` to ensure sequential execution: + +```csharp +[Collection("CDK Tests")] +public class LambdaFunctionConstructTests +{ + // Your tests here +} +``` + +This prevents parallel execution which can cause CDK context conflicts. + +## Test Helpers + +### CdkTestHelper + +The main utility class for creating test infrastructure: + +```csharp +// Create test stack with app +var (app, stack) = CdkTestHelper.CreateTestStack("test-stack"); + +// Create minimal test stack (app created internally) +var stack = CdkTestHelper.CreateTestStackMinimal("test-stack"); + +// Create custom stack types +var (app, customStack) = CdkTestHelper.CreateTestStack("test-stack", props); +var customStack = CdkTestHelper.CreateTestStackMinimal("test-stack", props); + +// Get test asset paths +var lambdaPath = CdkTestHelper.GetTestAssetPath("test-lambda.zip"); +var sitePath = CdkTestHelper.GetTestAssetPath("static-site"); +``` + +### Generic Stack Creation + +For testing custom stack types: + +```csharp +// Single generic (for IStackProps) +var (app, stack) = CdkTestHelper.CreateTestStack("stack-name", stackProps); + +// Dual generic (for custom props interfaces) +var (app, stack) = CdkTestHelper.CreateTestStack("stack-name", customProps); +``` + +## Lambda Function Testing + +### Props Builder + +```csharp +var props = CdkTestHelper.CreatePropsBuilder(AssetPathExtensions.GetTestLambdaZipPath()) + .WithFunctionName("my-function") + .WithFunctionSuffix("prod") + .WithMemorySize(1024) + .WithTimeoutInSeconds(30) + .WithEnvironmentVariable("LOG_LEVEL", "info") + .WithDynamoDbAccess("my-table") + .WithS3Access("my-bucket") + .WithApiGatewayPermission("arn:aws:execute-api:us-east-1:123456789012:abcdef123/*") + .WithOtelEnabled(true) + .WithSnapStart(true) + .WithGenerateUrl(true) + .Build(); +``` + +### Assertion Methods + +```csharp +// Basic assertions +template.ShouldHaveLambdaFunction("my-function-prod"); +template.ShouldHaveMemorySize(1024); +template.ShouldHaveTimeout(30); + +// Advanced features +template.ShouldHaveOtelLayer(); +template.ShouldHaveSnapStart(); +template.ShouldHaveFunctionUrl(); +template.ShouldHaveFunctionUrlOutput("test-stack", "test-construct"); + +// IAM and permissions +template.ShouldHaveCloudWatchLogsPermissions("my-function-prod"); +template.ShouldHaveLambdaPermissions(1); // Count of permission objects + +// Environment and configuration +template.ShouldHaveEnvironmentVariables(new Dictionary +{ + { "LOG_LEVEL", "info" } +}); +template.ShouldHaveVersionAndAlias("live"); +template.ShouldHaveLogGroup("my-function-prod", 14); +``` + +## Static Site Testing + +### Props Builder + +```csharp +var props = CdkTestHelper.CreateStaticSitePropsBuilder() + .WithDomainName("example.com") + .WithSiteSubDomain("www") + .WithApiDomain("api.example.com") + .WithAlternateDomains("example.org", "www.example.org") + .WithAssetPath("./static-site") + .Build(); +``` + +### Assertion Methods + +```csharp +// Complete site setup +template.ShouldHaveCompleteStaticSite("my-bucket", "example.com", true, 3); + +// Individual components +template.ShouldHaveStaticWebsiteBucket("my-bucket"); +template.ShouldHaveCloudFrontDistribution("www.example.com"); +template.ShouldHaveSSLCertificate("www.example.com"); +template.ShouldHaveRoute53Records("www.example.com"); +template.ShouldHaveMultipleRoute53Records(3); + +// API proxy +template.ShouldHaveApiProxyBehavior("api.example.com"); +template.ShouldNotHaveApiProxyBehavior(); +``` + +## DynamoDB Table Testing + +### Props Builder + +```csharp +var props = CdkTestHelper.CreateDynamoDbTablePropsBuilder() + .WithTableName("my-table") + .WithPartitionKey("userId", AttributeType.STRING) + .WithSortKey("createdAt", AttributeType.NUMBER) + .WithGlobalSecondaryIndex("email-index", "email", AttributeType.STRING) + .WithStream(StreamViewType.NEW_AND_OLD_IMAGES) + .WithTimeToLiveAttribute("expiresAt") + .Build(); +``` + +### Assertion Methods + +```csharp +// Table structure +template.ShouldHaveDynamoTable("my-table"); +template.ShouldHavePartitionKey("userId", AttributeType.STRING); +template.ShouldHaveSortKey("createdAt", AttributeType.NUMBER); + +// GSI +template.ShouldHaveGlobalSecondaryIndex("email-index", "email", AttributeType.STRING); + +// Streams and TTL +template.ShouldHaveTableStream(StreamViewType.NEW_AND_OLD_IMAGES); +template.ShouldHaveTimeToLiveAttribute("expiresAt"); + +// CloudFormation outputs +template.ShouldHaveTableOutputs("test-stack", "test-construct"); +``` + +## AutoFixture Integration + +### Custom Attributes + +```csharp +[Theory] +[LambdaFunctionConstructAutoData] +public void Should_Create_Lambda_With_Generated_Data(LambdaFunctionConstructProps props) +{ + // props is automatically generated with realistic test data +} + +[Theory] +[LambdaFunctionConstructAutoData(includeOtelLayer: false, includePermissions: true)] +public void Should_Create_Lambda_With_Custom_Settings(LambdaFunctionConstructProps props) +{ + // props generated with specific customizations +} +``` + +### Available Attributes + +- `LambdaFunctionConstructAutoDataAttribute`: Generates Lambda props +- `StaticSiteConstructAutoDataAttribute`: Generates static site props +- `DynamoDbTableConstructAutoDataAttribute`: Generates DynamoDB props + +## Testing Patterns + +### Scenario-Based Testing + +```csharp +[Theory] +[InlineData(true, true)] // OTEL enabled, permissions included +[InlineData(true, false)] // OTEL enabled, no permissions +[InlineData(false, true)] // OTEL disabled, permissions included +[InlineData(false, false)] // OTEL disabled, no permissions +public void Should_Handle_Different_Configurations(bool includeOtel, bool includePermissions) +{ + var props = CdkTestHelper.CreatePropsBuilder(AssetPathExtensions.GetTestLambdaZipPath()) + .WithOtelEnabled(includeOtel); + + if (includePermissions) + { + props = props.WithApiGatewayPermission("arn:aws:execute-api:us-east-1:123456789012:abcdef123/*"); + } + + var builtProps = props.Build(); + + // Test with different configurations +} +``` + +### Custom Stack Testing + +```csharp +[Fact] +public void Should_Create_Custom_Stack() +{ + var props = new MyCustomStackProps + { + Environment = "test", + ApiName = "my-api" + }; + + var stack = CdkTestHelper.CreateTestStackMinimal("test-stack", props); + var template = Template.FromStack(stack); + + // Assert custom stack resources +} +``` + +## Best Practices + +### 1. Use Collection Attribute +Always use `[Collection("CDK Tests")]` for CDK construct tests. + +### 2. Create Templates After Constructs +```csharp +// โœ… Correct +var construct = new LambdaFunctionConstruct(stack, "test", props); +var template = Template.FromStack(stack); + +// โŒ Incorrect +var template = Template.FromStack(stack); +var construct = new LambdaFunctionConstruct(stack, "test", props); +``` + +### 3. Use Builders for Complex Props +```csharp +// โœ… Recommended +var props = CdkTestHelper.CreatePropsBuilder(assetPath) + .WithMemorySize(512) + .WithTimeoutInSeconds(30) + .Build(); + +// โŒ Verbose +var props = new LambdaFunctionConstructProps +{ + FunctionName = "test-function", + FunctionSuffix = "test", + AssetPath = assetPath, + RoleName = "test-role", + PolicyName = "test-policy", + MemorySize = 512, + TimeoutInSeconds = 30 +}; +``` + +### 4. Test Both Positive and Negative Cases +```csharp +template.ShouldHaveOtelLayer(); // When enabled +template.ShouldNotHaveOtelLayer(); // When disabled +``` + +### 5. Use Meaningful Test Names +```csharp +[Fact] +public void Should_Enable_SnapStart_When_Configured() +{ + // Clear what the test is verifying +} +``` + +## Common Issues + +### Template Creation Timing +Create templates AFTER adding constructs to stacks, not before. + +### Parallel Test Execution +CDK tests must run sequentially due to shared context. Use `[Collection("CDK Tests")]`. + +### Asset Path Resolution +Use `AssetPathExtensions.GetTestLambdaZipPath()` for consistent asset paths. + +### Stack Environment +Test stacks are created with us-east-1 environment by default. Override if needed for region-specific testing. + +## Examples + +For complete testing examples, see the test files in the repository: +- `LambdaFunctionConstructTests.cs` +- `StaticSiteConstructTests.cs` +- `DynamoDbTableConstructTests.cs` \ No newline at end of file