Skip to content

Commit 16586b3

Browse files
authored
Add support for DynamoDB Local (#3)
1 parent 47489c0 commit 16586b3

17 files changed

+470
-13
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"Projects": [
3+
{
4+
"Name": "Aspire.Hosting.AWS",
5+
"Type": "Minor",
6+
"ChangelogMessages": [
7+
"Add Amazon DynamoDB Local support"
8+
]
9+
}
10+
]
11+
}

Directory.Packages.props

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,33 @@
44
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
55
<AspireVersion>9.0.0</AspireVersion>
66
</PropertyGroup>
7-
87
<ItemGroup>
98
<PackageVersion Include="Aspire.Hosting" Version="$(AspireVersion)" />
109
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireVersion)" />
10+
<PackageVersion Include="Aspire.Hosting.Testing" Version="$(AspireVersion)" />
1111
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery.Dns" Version="$(AspireVersion)" />
12-
1312
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="$(AspireVersion)" />
14-
1513
<!-- AWS SDK for .NET dependencies -->
16-
<PackageVersion Include="AWSSDK.CloudFormation" Version="3.7.400.36" />
17-
<PackageVersion Include="AWSSDK.SQS" Version="3.7.400.36" />
18-
<PackageVersion Include="AWSSDK.SimpleNotificationService" Version="3.7.400.36" />
19-
<PackageVersion Include="AWSSDK.Core" Version="3.7.400.36" />
14+
<PackageVersion Include="AWSSDK.CloudFormation" Version="3.7.400.47" />
15+
<PackageVersion Include="AWSSDK.Core" Version="3.7.400.47" />
16+
<PackageVersion Include="AWSSDK.DynamoDBv2" Version="3.7.402.11" />
17+
<PackageVersion Include="AWSSDK.SimpleNotificationService" Version="3.7.400.47" />
18+
<PackageVersion Include="AWSSDK.SQS" Version="3.7.400.47" />
2019
<PackageVersion Include="AWSSDK.Extensions.NETCore.Setup" Version="3.7.301" />
2120
<PackageVersion Include="AWS.Messaging" Version="0.9.2" />
2221
<!-- AWS CDK dependencies -->
23-
<PackageVersion Include="Amazon.CDK.Lib" Version="2.160.0" />
24-
22+
<PackageVersion Include="Amazon.CDK.Lib" Version="2.166.0" />
2523
<!-- Open Telemetry -->
2624
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
2725
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
2826
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
2927
<PackageVersion Include="OpenTelemetry.Instrumentation.AWS" Version="1.1.0-beta.6" />
3028
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.9.0" />
3129
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.9.0" />
32-
3330
<!-- Test dependencies -->
3431
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
3532
<PackageVersion Include="xunit" Version="2.9.2" />
3633
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
3734
<PackageVersion Include="JsonSchema.Net" Version="7.2.3" />
3835
</ItemGroup>
39-
</Project>
36+
</Project>

integrations-on-dotnet-aspire-for-aws.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{7F5F26A8
2121
EndProject
2222
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.AWS.UnitTests", "tests\Aspire.Hosting.AWS.UnitTests\Aspire.Hosting.AWS.UnitTests.csproj", "{2CDEDFE3-5B21-455B-AC18-14F89A9D321D}"
2323
EndProject
24+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.AWS.Integ.Tests", "tests\Aspire.Hosting.AWS.Integ.Tests\Aspire.Hosting.AWS.Integ.Tests.csproj", "{85E281A8-944D-4303-9D9A-28B4F5AEF69F}"
25+
EndProject
2426
Global
2527
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2628
Debug|Any CPU = Debug|Any CPU
@@ -51,6 +53,10 @@ Global
5153
{2CDEDFE3-5B21-455B-AC18-14F89A9D321D}.Debug|Any CPU.Build.0 = Debug|Any CPU
5254
{2CDEDFE3-5B21-455B-AC18-14F89A9D321D}.Release|Any CPU.ActiveCfg = Release|Any CPU
5355
{2CDEDFE3-5B21-455B-AC18-14F89A9D321D}.Release|Any CPU.Build.0 = Release|Any CPU
56+
{85E281A8-944D-4303-9D9A-28B4F5AEF69F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
57+
{85E281A8-944D-4303-9D9A-28B4F5AEF69F}.Debug|Any CPU.Build.0 = Debug|Any CPU
58+
{85E281A8-944D-4303-9D9A-28B4F5AEF69F}.Release|Any CPU.ActiveCfg = Release|Any CPU
59+
{85E281A8-944D-4303-9D9A-28B4F5AEF69F}.Release|Any CPU.Build.0 = Release|Any CPU
5460
EndGlobalSection
5561
GlobalSection(SolutionProperties) = preSolution
5662
HideSolutionNode = FALSE
@@ -62,6 +68,7 @@ Global
6268
{B9C202D3-5E96-8F20-9DF2-7F3F5F78F6DB} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
6369
{B539FEDD-415D-A6BF-7813-265B9ED31888} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
6470
{2CDEDFE3-5B21-455B-AC18-14F89A9D321D} = {7F5F26A8-F41A-4B16-83F0-8A11EE396FAC}
71+
{85E281A8-944D-4303-9D9A-28B4F5AEF69F} = {7F5F26A8-F41A-4B16-83F0-8A11EE396FAC}
6572
EndGlobalSection
6673
GlobalSection(ExtensibilityGlobals) = postSolution
6774
SolutionGuid = {FBA55172-92F1-4495-A082-E0ABE4F4AF09}

playground/AWS/AWS.AppHost/AWS.AppHost.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,10 @@
1515
<ProjectReference Include="..\Frontend\Frontend.csproj" />
1616
</ItemGroup>
1717

18+
<ItemGroup>
19+
<None Update="app-resources.template">
20+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
21+
</None>
22+
</ItemGroup>
23+
1824
</Project>

playground/AWS/AWS.AppHost/Program.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
using Amazon;
3+
using Aspire.Hosting.AWS.DynamoDB;
34

45
var builder = DistributedApplication.CreateBuilder(args);
56

67
// Setup a configuration for the AWS .NET SDK.
78
var awsConfig = builder.AddAWSSDKConfig()
8-
.WithProfile("default")
99
.WithRegion(RegionEndpoint.USWest2);
1010

1111
// Provision application level resources like SQS queues and SNS topics defined in the CloudFormation template file app-resources.template.
@@ -14,6 +14,9 @@
1414
// Add the SDK configuration so the AppHost knows what account/region to provision the resources.
1515
.WithReference(awsConfig);
1616

17+
// Add a DynamoDB Local instance
18+
var localDynamoDB = builder.AddAWSDynamoDBLocal("DynamoDBLocal");
19+
1720
// To add outputs of a CloudFormation stack that was created outside of AppHost use the AddAWSCloudFormationStack method.
1821
// then attach the CloudFormation resource to a project using the WithReference method.
1922
//var awsExistingResource = builder.AddAWSCloudFormationStack("ExistingStackName")
@@ -28,6 +31,7 @@
2831
// The prefix is configurable by the optional configSection parameter.
2932
.WithReference(awsResources)
3033
// Demonstrating binding a single output variable to environment variable in the project.
31-
.WithEnvironment("ChatTopicArnEnv", awsResources.GetOutput("ChatTopicArn"));
34+
.WithEnvironment("ChatTopicArnEnv", awsResources.GetOutput("ChatTopicArn"))
35+
.WithReference(localDynamoDB);
3236

3337
builder.Build().Run();

playground/AWS/Frontend/Components/Layout/NavMenu.razor

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@
2525
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Message Publisher
2626
</NavLink>
2727
</div>
28+
29+
<div class="nav-item px-3">
30+
<NavLink class="nav-link" href="dynamodb-local-test">
31+
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> DynamoDB Local
32+
</NavLink>
33+
</div>
2834
</nav>
2935
</div>
3036

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
@page "/dynamodb-local-test"
2+
@using Amazon.DynamoDBv2
3+
@using Amazon.DynamoDBv2.Model
4+
5+
@inject IAmazonDynamoDB ddbClient
6+
@inject ILogger<DynamoDBLocalTest> logger
7+
8+
9+
<h3>Test DynamoDB Local Integration</h3>
10+
11+
<p>The IAmazonDynamoDB service client is configured to make requests to: <strong>@ServiceUrl</strong></p>
12+
13+
<p>Tables in Local DynamoDB Instance</p>
14+
<ul>
15+
@foreach(var name in TableNames)
16+
{
17+
<li>@name</li>
18+
}
19+
</ul>
20+
21+
@code {
22+
23+
public string? ServiceUrl { get; set; }
24+
25+
public List<string> TableNames { get; set; } = new List<string>();
26+
27+
protected override async Task OnInitializedAsync()
28+
{
29+
var listTablesRequest = new ListTablesRequest();
30+
ServiceUrl = ddbClient.DetermineServiceOperationEndpoint(listTablesRequest).URL;
31+
32+
33+
// Create a table so something comes back in the ListTables call
34+
var createRequest = new CreateTableRequest
35+
{
36+
TableName = "LocalDDBTable",
37+
BillingMode = BillingMode.PAY_PER_REQUEST,
38+
KeySchema = new List<KeySchemaElement>
39+
{
40+
new KeySchemaElement{AttributeName = "Id", KeyType = KeyType.HASH}
41+
},
42+
AttributeDefinitions = new List<AttributeDefinition>
43+
{
44+
new AttributeDefinition{AttributeName = "Id", AttributeType = ScalarAttributeType.S}
45+
}
46+
};
47+
48+
try
49+
{
50+
await ddbClient.CreateTableAsync(createRequest);
51+
logger.LogInformation("Table {tableName} created", createRequest.TableName);
52+
}
53+
catch (ResourceInUseException)
54+
{
55+
logger.LogWarning("CreateTable failed because table already exists: {tableName}", createRequest.TableName);
56+
}
57+
58+
59+
TableNames = (await ddbClient.ListTablesAsync(listTablesRequest)).TableNames;
60+
}
61+
}

playground/AWS/Frontend/Frontend.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
<ItemGroup>
1010
<ProjectReference Include="..\AWS.ServiceDefaults\AWS.ServiceDefaults.csproj" />
11+
<PackageReference Include="AWSSDK.DynamoDBv2" />
1112
<PackageReference Include="AWSSDK.SQS" />
1213
<PackageReference Include="AWSSDK.SimpleNotificationService" />
1314
<PackageReference Include="AWSSDK.Extensions.NETCore.Setup" />

playground/AWS/Frontend/Program.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22

3+
using Amazon.DynamoDBv2;
34
using Amazon.SimpleNotificationService;
45
using Amazon.SQS;
56
using Frontend.Components;
@@ -8,6 +9,7 @@
89

910
builder.AddServiceDefaults();
1011

12+
builder.Services.AddAWSService<IAmazonDynamoDB>();
1113
builder.Services.AddAWSService<IAmazonSQS>();
1214
builder.Services.AddAWSService<IAmazonSimpleNotificationService>();
1315

@@ -41,4 +43,42 @@
4143
app.MapRazorComponents<App>()
4244
.AddInteractiveServerRenderMode();
4345

46+
app.MapGet("/healthcheck/dynamodb", (HttpContext ctx) =>
47+
{
48+
var ddbClient = app.Services.GetRequiredService<IAmazonDynamoDB>();
49+
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AWS_ENDPOINT_URL_DYNAMODB")))
50+
{
51+
return Results.BadRequest("The AWS_ENDPOINT_URL_DYNAMODB is not set");
52+
}
53+
if (!ddbClient.Config.ServiceURL.StartsWith(Environment.GetEnvironmentVariable("AWS_ENDPOINT_URL_DYNAMODB")!))
54+
{
55+
return Results.BadRequest("The DynamoDB service client is not configured for DyanamoDB local");
56+
}
57+
58+
return Results.Ok("Success");
59+
});
60+
61+
62+
app.MapGet("/healthcheck/cloudformation", (HttpContext ctx) =>
63+
{
64+
// Confirm the WithEnvironment behavior
65+
if (builder.Configuration["ChatTopicArnEnv"] == null)
66+
{
67+
return Results.BadRequest("Missing ChatTopicArnEnv");
68+
}
69+
70+
// Confirm the WithReference behavior
71+
if (builder.Configuration["AWS:Resources:ChatTopicArn"] == null)
72+
{
73+
return Results.BadRequest("Missing ChatTopicArn");
74+
}
75+
if (builder.Configuration["AWS:Resources:ChatMessagesQueueUrl"] == null)
76+
{
77+
return Results.BadRequest("Missing ChatTopicArn");
78+
}
79+
80+
return Results.Ok("Success");
81+
});
82+
83+
4484
app.Run();
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
3+
using System.Diagnostics;
4+
5+
namespace Aspire.Hosting.AWS.DynamoDB;
6+
7+
/// <summary>
8+
/// Options that can be set for configuring the instance of DynamoDB local.
9+
/// </summary>
10+
[DebuggerDisplay("Type = {GetType().Name,nq}, Registry = {Registry}, Image = {Image}, Tag = {Tag}, LocalStorageDirectory = {LocalStorageDirectory}, SharedDb = {SharedDb}, DisableDynamoDBLocalTelemetry = {DisableDynamoDBLocalTelemetry}, DelayTransientStatuses = {DelayTransientStatuses}")]
11+
public class DynamoDBLocalOptions
12+
{
13+
/// <summary>
14+
/// The registry of the container image. THe default is public.ecr.aws.
15+
/// </summary>
16+
public string Registry { get; set; } = "public.ecr.aws";
17+
18+
/// <summary>
19+
/// The container image to run for DynamoDB local. The default is aws-dynamodb-local/aws-dynamodb-local.
20+
/// </summary>
21+
public string Image { get; set; } = "aws-dynamodb-local/aws-dynamodb-local";
22+
23+
/// <summary>
24+
/// The container image tag. The default is latest.
25+
/// </summary>
26+
public string Tag { get; set; } = "latest";
27+
28+
/// <summary>
29+
/// If set to true DynamoDB local uses a single database file instead of separate files for each credential and Region.
30+
/// </summary>
31+
public bool SharedDb { get; set; }
32+
33+
/// <summary>
34+
/// If set to true DynamoDB runs in memory instead of using a database file. DynamoDB local will run faster
35+
/// using InMemory mode but all data will be lost when the container ends and the data stored in DynamoDB
36+
/// local can not exceed the available memory for the container.
37+
/// </summary>
38+
public bool InMemory { get; set; }
39+
40+
/// <summary>
41+
/// Directory on host machine to create the DynamoDB local database files. If this property is set the data
42+
/// written to DynamoDB local will persist between AppHost invocations.
43+
/// </summary>
44+
public string? LocalStorageDirectory { get; set; }
45+
46+
/// <summary>
47+
/// If set to true disabled DynamoDB local's telemetry by setting the DDB_LOCAL_TELEMETRY environment variable
48+
/// </summary>
49+
public bool DisableDynamoDBLocalTelemetry { get; set; }
50+
51+
/// <summary>
52+
/// If set to true causes DynamoDB local to introduce delays for certain operations. DynamoDB local can perform
53+
/// some tasks almost instantaneously, such as create/update/delete operations on tables and indexes. However, the DynamoDB
54+
/// service requires more time for these tasks. Setting this parameter helps DynamoDB running on your computer
55+
/// simulate the behavior of the DynamoDB web service more closely.
56+
/// </summary>
57+
public bool DelayTransientStatuses { get; set; }
58+
59+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
3+
using Aspire.Hosting.ApplicationModel;
4+
5+
namespace Aspire.Hosting.AWS.DynamoDB;
6+
7+
/// <summary>
8+
/// Represents a DynamoDB local resource. This is a dev only resources and will not be written to the project's manifest.
9+
/// </summary>
10+
internal sealed class DynamoDBLocalResource(string name, DynamoDBLocalOptions options) : ContainerResource(name), IDynamoDBLocalResource
11+
{
12+
internal const int DynamoDBInternalPort = 8000;
13+
internal const string InternalStorageMountPoint = "/storage";
14+
15+
internal DynamoDBLocalOptions Options { get; } = options;
16+
17+
/// <summary>
18+
/// Create the list of command line arguments that must be used when running the DynamoDB local container image.
19+
/// </summary>
20+
/// <returns></returns>
21+
internal string[] CreateContainerImageArguments()
22+
{
23+
var arguments = new List<string>
24+
{
25+
"-Djava.library.path=./DynamoDBLocal_lib",
26+
"-jar",
27+
"DynamoDBLocal.jar"
28+
};
29+
30+
if (Options.SharedDb)
31+
arguments.Add("-sharedDb");
32+
33+
if (Options.InMemory)
34+
arguments.Add("-inMemory");
35+
36+
if (Options.DisableDynamoDBLocalTelemetry)
37+
arguments.Add("-disableTelemetry");
38+
39+
if (!string.IsNullOrEmpty(Options.LocalStorageDirectory))
40+
{
41+
arguments.Add("-dbPath");
42+
arguments.Add(InternalStorageMountPoint);
43+
}
44+
45+
if (Options.DelayTransientStatuses)
46+
arguments.Add("-delayTransientStatuses");
47+
48+
return arguments.ToArray();
49+
}
50+
}

0 commit comments

Comments
 (0)