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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<ItemGroup>
<AspireProjectOrPackageReference Include="Aspire.Azure.Storage.Blobs" />
<AspireProjectOrPackageReference Include="Aspire.Azure.Storage.Queues" />
<AspireProjectOrPackageReference Include="Aspire.Microsoft.Data.SqlClient" />
<ProjectReference Include="..\..\Playground.ServiceDefaults\Playground.ServiceDefaults.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Azure.Storage.Blobs;
using Azure.Storage.Queues;
using Microsoft.Data.SqlClient;

var builder = WebApplication.CreateBuilder(args);

Expand All @@ -12,6 +13,8 @@

builder.AddKeyedAzureQueue("myqueue");

builder.AddSqlServerClient("sqldb");

var app = builder.Build();

app.MapDefaultEndpoints();
Expand All @@ -30,6 +33,38 @@
return blobNames;
});

app.MapGet("/sql", async (SqlConnection connection) =>
{
await connection.OpenAsync();

// Ensure the Items table exists
await using var createCmd = connection.CreateCommand();
createCmd.CommandText = """
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Items')
CREATE TABLE Items (Id INT IDENTITY(1,1) PRIMARY KEY, Name NVARCHAR(256) NOT NULL, CreatedAt DATETIME2 DEFAULT GETUTCDATE())
""";
await createCmd.ExecuteNonQueryAsync();

// Insert a new item
var itemName = $"Item-{Guid.NewGuid():N}";
await using var insertCmd = connection.CreateCommand();
insertCmd.CommandText = "INSERT INTO Items (Name) OUTPUT INSERTED.Id, INSERTED.Name, INSERTED.CreatedAt VALUES (@name)";
insertCmd.Parameters.Add(new SqlParameter("@name", itemName));

await using var reader = await insertCmd.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return Results.Ok(new
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
CreatedAt = reader.GetDateTime(2)
});
}

return Results.StatusCode(500);
});

app.Run();

static async Task ReadBlobsAsync(BlobContainerClient containerClient, List<string> output)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<AspireProjectOrPackageReference Include="Aspire.Hosting.Azure" />
<AspireProjectOrPackageReference Include="Aspire.Hosting.Azure.AppContainers" />
<AspireProjectOrPackageReference Include="Aspire.Hosting.Azure.Network" />
<AspireProjectOrPackageReference Include="Aspire.Hosting.Azure.Sql" />
<AspireProjectOrPackageReference Include="Aspire.Hosting.Azure.Storage" />
<AspireProjectOrPackageReference Include="Aspire.Hosting.AppHost" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,15 @@
privateEndpointsSubnet.AddPrivateEndpoint(blobs);
privateEndpointsSubnet.AddPrivateEndpoint(queues);

var sqlServer = builder.AddAzureSqlServer("sql")
.RunAsContainer(c => c.WithLifetime(ContainerLifetime.Persistent));
privateEndpointsSubnet.AddPrivateEndpoint(sqlServer);

var db = sqlServer.AddDatabase("sqldb");

builder.AddProject<Projects.AzureVirtualNetworkEndToEnd_ApiService>("api")
.WithExternalHttpEndpoints()
.WithReference(db).WaitFor(db)
.WithReference(mycontainer).WaitFor(mycontainer)
.WithReference(myqueue).WaitFor(myqueue);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ param api_identity_outputs_id string

param api_containerport string

param sql_outputs_sqlserverfqdn string

param storage_outputs_blobendpoint string

param storage_outputs_queueendpoint string
Expand Down Expand Up @@ -63,6 +65,30 @@ resource api 'Microsoft.App/containerApps@2025-02-02-preview' = {
name: 'HTTP_PORTS'
value: api_containerport
}
{
name: 'ConnectionStrings__sqldb'
value: 'Server=tcp:${sql_outputs_sqlserverfqdn},1433;Encrypt=True;Authentication="Active Directory Default";Database=sqldb'
}
{
name: 'SQLDB_HOST'
value: sql_outputs_sqlserverfqdn
}
{
name: 'SQLDB_PORT'
value: '1433'
}
{
name: 'SQLDB_URI'
value: 'mssql://${sql_outputs_sqlserverfqdn}:1433/sqldb'
}
{
name: 'SQLDB_JDBCCONNECTIONSTRING'
value: 'jdbc:sqlserver://${sql_outputs_sqlserverfqdn}:1433;database=sqldb;encrypt=true;trustServerCertificate=false'
}
{
name: 'SQLDB_DATABASENAME'
value: 'sqldb'
}
{
name: 'ConnectionStrings__mycontainer'
value: 'Endpoint=${storage_outputs_blobendpoint};ContainerName=mycontainer'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location

param sql_outputs_name string

param sql_outputs_sqlserveradminname string

param vnet_outputs_sql_aci_subnet_id string

param sql_store_outputs_name string

param principalId string

param principalName string

param private_endpoints_sql_pe_outputs_name string

param private_endpoints_files_pe_outputs_name string

resource sql 'Microsoft.Sql/servers@2023-08-01' existing = {
name: sql_outputs_name
}

resource sqlServerAdmin 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' existing = {
name: sql_outputs_sqlserveradminname
}

resource sql_store 'Microsoft.Storage/storageAccounts@2024-01-01' existing = {
name: sql_store_outputs_name
}

resource mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' existing = {
name: principalName
}

resource private_endpoints_sql_pe 'Microsoft.Network/privateEndpoints@2025-05-01' existing = {
name: private_endpoints_sql_pe_outputs_name
}

resource private_endpoints_files_pe 'Microsoft.Network/privateEndpoints@2025-05-01' existing = {
name: private_endpoints_files_pe_outputs_name
}

resource script_sql_sqldb 'Microsoft.Resources/deploymentScripts@2023-08-01' = {
name: take('script-${uniqueString('sql', principalName, 'sqldb', resourceGroup().id)}', 24)
location: location
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${sqlServerAdmin.id}': { }
}
}
kind: 'AzurePowerShell'
properties: {
azPowerShellVersion: '14.0'
retentionInterval: 'PT1H'
containerSettings: {
subnetIds: [
{
id: vnet_outputs_sql_aci_subnet_id
}
]
}
environmentVariables: [
{
name: 'DBNAME'
value: 'sqldb'
}
{
name: 'DBSERVER'
value: sql.properties.fullyQualifiedDomainName
}
{
name: 'PRINCIPALTYPE'
value: 'ServicePrincipal'
}
{
name: 'PRINCIPALNAME'
value: principalName
}
{
name: 'ID'
value: mi.properties.clientId
}
]
scriptContent: '\$sqlServerFqdn = "\$env:DBSERVER"\r\n\$sqlDatabaseName = "\$env:DBNAME"\r\n\$principalName = "\$env:PRINCIPALNAME"\r\n\$id = "\$env:ID"\r\n\r\n# Install SqlServer module - using specific version to avoid breaking changes in 22.4.5.1 (see https://github.com/dotnet/aspire/issues/9926)\r\nInstall-Module -Name SqlServer -RequiredVersion 22.3.0 -Force -AllowClobber -Scope CurrentUser\r\nImport-Module SqlServer\r\n\r\n\$sqlCmd = @"\r\nDECLARE @name SYSNAME = \'\$principalName\';\r\nDECLARE @id UNIQUEIDENTIFIER = \'\$id\';\r\n\r\n-- Convert the guid to the right type\r\nDECLARE @castId NVARCHAR(MAX) = CONVERT(VARCHAR(MAX), CONVERT (VARBINARY(16), @id), 1);\r\n\r\n-- Construct command: CREATE USER [@name] WITH SID = @castId, TYPE = E;\r\nDECLARE @cmd NVARCHAR(MAX) = N\'CREATE USER [\' + @name + \'] WITH SID = \' + @castId + \', TYPE = E;\'\r\nEXEC (@cmd);\r\n\r\n-- Assign roles to the new user\r\nDECLARE @role1 NVARCHAR(MAX) = N\'ALTER ROLE db_owner ADD MEMBER [\' + @name + \']\';\r\nEXEC (@role1);\r\n\r\n"@\r\n# Note: the string terminator must not have whitespace before it, therefore it is not indented.\r\n\r\nWrite-Host \$sqlCmd\r\n\r\n\$connectionString = "Server=tcp:\${sqlServerFqdn},1433;Initial Catalog=\${sqlDatabaseName};Authentication=Active Directory Default;"\r\n\r\n\$maxRetries = 5\r\n\$retryDelay = 60\r\n\$attempt = 0\r\n\$success = \$false\r\n\r\nwhile (-not \$success -and \$attempt -lt \$maxRetries) {\r\n \$attempt++\r\n Write-Host "Attempt \$attempt of \$maxRetries..."\r\n try {\r\n Invoke-Sqlcmd -ConnectionString \$connectionString -Query \$sqlCmd\r\n \$success = \$true\r\n Write-Host "SQL command succeeded on attempt \$attempt."\r\n } catch {\r\n Write-Host "Attempt \$attempt failed: \$_"\r\n if (\$attempt -lt \$maxRetries) {\r\n Write-Host "Retrying in \$retryDelay seconds..."\r\n Start-Sleep -Seconds \$retryDelay\r\n } else {\r\n throw\r\n }\r\n }\r\n}'
storageAccountSettings: {
storageAccountName: sql_store_outputs_name
}
}
dependsOn: [
private_endpoints_sql_pe
private_endpoints_files_pe
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"params": {
"nat_outputs_id": "{nat.outputs.id}",
"container_apps_nsg_outputs_id": "{container-apps-nsg.outputs.id}",
"private_endpoints_nsg_outputs_id": "{private-endpoints-nsg.outputs.id}"
"private_endpoints_nsg_outputs_id": "{private-endpoints-nsg.outputs.id}",
"sql_nsg_outputs_id": "{sql-nsg.outputs.id}"
}
},
"container-apps-nsg": {
Expand Down Expand Up @@ -95,6 +96,46 @@
"storage_outputs_id": "{storage.outputs.id}"
}
},
"sql": {
"type": "azure.bicep.v0",
"connectionString": "Server=tcp:{sql.outputs.sqlServerFqdn},1433;Encrypt=True;Authentication=\u0022Active Directory Default\u0022",
"path": "sql.module.bicep"
},
"privatelink-database-windows-net": {
"type": "azure.bicep.v0",
"path": "privatelink-database-windows-net.module.bicep",
"params": {
"vnet_outputs_id": "{vnet.outputs.id}"
}
},
"private-endpoints-sql-pe": {
"type": "azure.bicep.v0",
"path": "private-endpoints-sql-pe.module.bicep",
"params": {
"privatelink_database_windows_net_outputs_name": "{privatelink-database-windows-net.outputs.name}",
"vnet_outputs_private_endpoints_id": "{vnet.outputs.private_endpoints_Id}",
"sql_outputs_id": "{sql.outputs.id}"
}
},
"sql-store": {
"type": "azure.bicep.v0",
"path": "sql-store.module.bicep"
},
"sql-admin-identity": {
"type": "azure.bicep.v0",
"path": "sql-admin-identity.module.bicep",
"params": {
"sql_outputs_sqlserveradminname": "{sql.outputs.sqlServerAdminName}"
}
},
"sql-nsg": {
"type": "azure.bicep.v0",
"path": "sql-nsg.module.bicep"
},
"sqldb": {
"type": "value.v0",
"connectionString": "{sql.connectionString};Database=sqldb"
},
"api": {
"type": "project.v1",
"path": "../AzureVirtualNetworkEndToEnd.ApiService/AzureVirtualNetworkEndToEnd.ApiService.csproj",
Expand All @@ -109,6 +150,7 @@
"api_containerimage": "{api.containerImage}",
"api_identity_outputs_id": "{api-identity.outputs.id}",
"api_containerport": "{api.containerPort}",
"sql_outputs_sqlserverfqdn": "{sql.outputs.sqlServerFqdn}",
"storage_outputs_blobendpoint": "{storage.outputs.blobEndpoint}",
"storage_outputs_queueendpoint": "{storage.outputs.queueEndpoint}",
"api_identity_outputs_clientid": "{api-identity.outputs.clientId}"
Expand All @@ -118,6 +160,12 @@
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
"HTTP_PORTS": "{api.bindings.http.targetPort}",
"ConnectionStrings__sqldb": "{sqldb.connectionString}",
"SQLDB_HOST": "{sql.outputs.sqlServerFqdn}",
"SQLDB_PORT": "1433",
"SQLDB_URI": "mssql://{sql.outputs.sqlServerFqdn}:1433/sqldb",
"SQLDB_JDBCCONNECTIONSTRING": "jdbc:sqlserver://{sql.outputs.sqlServerFqdn}:1433;database=sqldb;encrypt=true;trustServerCertificate=false",
"SQLDB_DATABASENAME": "sqldb",
"ConnectionStrings__mycontainer": "{mycontainer.connectionString}",
"MYCONTAINER_URI": "{storage.outputs.blobEndpoint}",
"MYCONTAINER_BLOBCONTAINERNAME": "mycontainer",
Expand All @@ -140,17 +188,55 @@
}
}
},
"privatelink-file-core-windows-net": {
"type": "azure.bicep.v0",
"path": "privatelink-file-core-windows-net.module.bicep",
"params": {
"vnet_outputs_id": "{vnet.outputs.id}"
}
},
"private-endpoints-files-pe": {
"type": "azure.bicep.v0",
"path": "private-endpoints-files-pe.module.bicep",
"params": {
"privatelink_file_core_windows_net_outputs_name": "{privatelink-file-core-windows-net.outputs.name}",
"vnet_outputs_private_endpoints_id": "{vnet.outputs.private_endpoints_Id}",
"sql_store_outputs_id": "{sql-store.outputs.id}"
}
},
"api-identity": {
"type": "azure.bicep.v0",
"path": "api-identity.module.bicep"
},
"api-roles-sql": {
"type": "azure.bicep.v0",
"path": "api-roles-sql.module.bicep",
"params": {
"sql_outputs_name": "{sql.outputs.name}",
"sql_outputs_sqlserveradminname": "{sql.outputs.sqlServerAdminName}",
"vnet_outputs_sql_aci_subnet_id": "{vnet.outputs.sql_aci_subnet_Id}",
"sql_store_outputs_name": "{sql-store.outputs.name}",
"principalId": "{api-identity.outputs.principalId}",
"principalName": "{api-identity.outputs.principalName}",
"private_endpoints_sql_pe_outputs_name": "{private-endpoints-sql-pe.outputs.name}",
"private_endpoints_files_pe_outputs_name": "{private-endpoints-files-pe.outputs.name}"
}
},
"api-roles-storage": {
"type": "azure.bicep.v0",
"path": "api-roles-storage.module.bicep",
"params": {
"storage_outputs_name": "{storage.outputs.name}",
"principalId": "{api-identity.outputs.principalId}"
}
},
"sql-admin-identity-roles-sql-store": {
"type": "azure.bicep.v0",
"path": "sql-admin-identity-roles-sql-store.module.bicep",
"params": {
"sql_store_outputs_name": "{sql-store.outputs.name}",
"principalId": "{sql-admin-identity.outputs.principalId}"
}
}
}
}
Loading
Loading