diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 2966729..f1399e9 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -1,9 +1,6 @@ name: Deploy to Azure on: workflow_dispatch: - push: - branches: - - main permissions: id-token: write contents: read diff --git a/assets/web.png b/assets/web.png index 6f861cc..30a4c54 100644 Binary files a/assets/web.png and b/assets/web.png differ diff --git a/azure.yaml b/azure.yaml index 50dce19..6bbb45a 100644 --- a/azure.yaml +++ b/azure.yaml @@ -17,13 +17,13 @@ hooks: postprovision: windows: run: | - dotnet user-secrets set "AZURE_COSMOS_DB_NOSQL_ENDPOINT" "$env:AZURE_COSMOS_DB_NOSQL_ENDPOINT" --project ./src/web/Microsoft.Samples.Cosmos.NoSQL.Quickstart.Web.csproj + dotnet user-secrets set "CONFIGURATION:AZURECOSMOSDB:ENDPOINT" "$env:CONFIGURATION__AZURECOSMOSDB__ENDPOINT" --project ./src/web/Microsoft.Samples.Cosmos.NoSQL.Quickstart.Web.csproj shell: pwsh continueOnError: false interactive: true posix: run: | - dotnet user-secrets set "AZURE_COSMOS_DB_NOSQL_ENDPOINT" "$AZURE_COSMOS_DB_NOSQL_ENDPOINT" --project ./src/web/Microsoft.Samples.Cosmos.NoSQL.Quickstart.Web.csproj + dotnet user-secrets set "CONFIGURATION:AZURECOSMOSDB:ENDPOINT" "$CONFIGURATION__AZURECOSMOSDB__ENDPOINT" --project ./src/web/Microsoft.Samples.Cosmos.NoSQL.Quickstart.Web.csproj shell: sh continueOnError: false interactive: true diff --git a/infra/abbreviations.json b/infra/abbreviations.json deleted file mode 100644 index 81c842b..0000000 --- a/infra/abbreviations.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "logAnalyticsWorkspace": "log-analytics", - "containerRegistry": "containerreg", - "containerAppsEnv": "container-env", - "containerAppsApp": "container-app", - "cosmosDbAccount": "cosmos-db-nosql", - "userAssignedIdentity": "managed-identity" -} \ No newline at end of file diff --git a/infra/app/database.bicep b/infra/app/database.bicep deleted file mode 100644 index 297349a..0000000 --- a/infra/app/database.bicep +++ /dev/null @@ -1,67 +0,0 @@ -metadata description = 'Create database accounts.' - -param accountName string -param location string = resourceGroup().location -param tags object = {} - -@description('Id of the service principals to assign database and application roles.') -param appPrincipalId string - -@description('Id of the user principals to assign database and application roles.') -param userPrincipalId string = '' - -module cosmosDbAccount 'br/public:avm/res/document-db/database-account:0.6.1' = { - name: 'cosmos-db-account' - params: { - name: accountName - location: location - locations: [ - { - failoverPriority: 0 - locationName: location - isZoneRedundant: false - } - ] - tags: tags - disableKeyBasedMetadataWriteAccess: true - disableLocalAuth: true - capabilitiesToAdd: [ - 'EnableServerless' - ] - sqlRoleDefinitions: [ - { - name: 'nosql-data-plane-contributor' - dataAction: [ - 'Microsoft.DocumentDB/databaseAccounts/readMetadata' - 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*' - 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*' - ] - } - ] - sqlRoleAssignmentsPrincipalIds: union( - [ - appPrincipalId - ], - !empty(userPrincipalId) - ? [ - userPrincipalId - ] - : [] - ) - sqlDatabases: [ - { - name: 'cosmicworks' - containers: [ - { - name: 'products' - paths: [ - '/category' - ] - } - ] - } - ] - } -} - -output endpoint string = cosmosDbAccount.outputs.endpoint diff --git a/infra/app/identity.bicep b/infra/app/identity.bicep deleted file mode 100644 index c4c6b0b..0000000 --- a/infra/app/identity.bicep +++ /dev/null @@ -1,18 +0,0 @@ -metadata description = 'Create identity resources.' - -param identityName string -param location string = resourceGroup().location -param tags object = {} - -module userAssignedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.0' = { - name: 'user-assigned-identity' - params: { - name: identityName - location: location - tags: tags - } -} - -output resourceId string = userAssignedIdentity.outputs.resourceId -output principalId string = userAssignedIdentity.outputs.principalId -output clientId string = userAssignedIdentity.outputs.clientId diff --git a/infra/app/registry.bicep b/infra/app/registry.bicep deleted file mode 100644 index 4013b70..0000000 --- a/infra/app/registry.bicep +++ /dev/null @@ -1,35 +0,0 @@ -metadata description = 'Create container registries.' - -param registryName string -param location string = resourceGroup().location -param tags object = {} - -@description('Id of the user principals to assign database and application roles.') -param userPrincipalId string = '' - -module containerRegistry 'br/public:avm/res/container-registry/registry:0.5.1' = { - name: 'container-registry' - params: { - name: registryName - location: location - tags: tags - acrAdminUserEnabled: false - anonymousPullEnabled: true - publicNetworkAccess: 'Enabled' - acrSku: 'Standard' - } -} - -module registryUserAssignment 'br/public:avm/ptn/authorization/resource-role-assignment:0.1.1' = if (!empty(userPrincipalId)) { - name: 'container-registry-role-assignment-push-user' - params: { - principalId: userPrincipalId - resourceId: containerRegistry.outputs.resourceId - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - '8311e382-0749-4cb8-b61a-304f252e45ec' // AcrPush built-in role - ) - } -} - -output endpoint string = containerRegistry.outputs.loginServer diff --git a/infra/app/web.bicep b/infra/app/web.bicep deleted file mode 100644 index 7e697c2..0000000 --- a/infra/app/web.bicep +++ /dev/null @@ -1,95 +0,0 @@ -metadata description = 'Create web application resources.' - -param workspaceName string -param envName string -param appName string -param serviceTag string -param location string = resourceGroup().location -param tags object = {} - -@description('Endpoint for Azure Cosmos DB for Table account.') -param databaseAccountEndpoint string - -@description('Client ID of the service principal to assign database and application roles.') -param appClientId string - -@description('Resource ID of the service principal to assign database and application roles.') -param appResourceId string - -module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.7.0' = { - name: 'log-analytics-workspace' - params: { - name: workspaceName - location: location - tags: tags - } -} - -module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.8.0' = { - name: 'container-apps-env' - params: { - name: envName - location: location - tags: tags - logAnalyticsWorkspaceResourceId: logAnalyticsWorkspace.outputs.resourceId - zoneRedundant: false - } -} - -module containerAppsApp 'br/public:avm/res/app/container-app:0.9.0' = { - name: 'container-apps-app' - params: { - name: appName - environmentResourceId: containerAppsEnvironment.outputs.resourceId - location: location - tags: union(tags, { 'azd-service-name': serviceTag }) - ingressTargetPort: 8080 - ingressExternal: true - ingressTransport: 'auto' - stickySessionsAffinity: 'sticky' - corsPolicy: { - allowCredentials: true - allowedOrigins: [ - '*' - ] - } - managedIdentities: { - systemAssigned: false - userAssignedResourceIds: [ - appResourceId - ] - } - secrets: { - secureList: [ - { - name: 'azure-cosmos-db-nosql-endpoint' - value: databaseAccountEndpoint - } - { - name: 'user-assigned-managed-identity-client-id' - value: appClientId - } - ] - } - containers: [ - { - image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' - name: 'web-front-end' - resources: { - cpu: '1' - memory: '2Gi' - } - env: [ - { - name: 'AZURE_COSMOS_DB_NOSQL_ENDPOINT' - secretRef: 'azure-cosmos-db-nosql-endpoint' - } - { - name: 'AZURE_CLIENT_ID' - secretRef: 'user-assigned-managed-identity-client-id' - } - ] - } - ] - } -} diff --git a/infra/main.bicep b/infra/main.bicep index ec3caf2..cd98fed 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1,3 +1,5 @@ +metadata description = 'Provisions resources for a web application that uses Azure SDK for .NET to connect to Azure Cosmos DB for NoSQL.' + targetScope = 'resourceGroup' @minLength(1) @@ -10,68 +12,201 @@ param environmentName string param location string @description('Id of the principal to assign database and application roles.') -param principalId string = '' - -// Optional parameters -param logWorkspaceName string = '' -param cosmosDbAccountName string = '' -param containerRegistryName string = '' -param containerAppsEnvName string = '' -param containerAppsAppName string = '' +param deploymentUserPrincipalId string = '' // serviceName is used as value for the tag (azd-service-name) azd uses to identify deployment host param serviceName string = 'web' -var abbreviations = loadJsonContent('abbreviations.json') var resourceToken = toLower(uniqueString(resourceGroup().id, environmentName, location)) var tags = { 'azd-env-name': environmentName repo: 'https://github.com/azure-samples/cosmos-db-nosql-dotnet-quickstart' } -module identity 'app/identity.bicep' = { - name: 'identity' +module managedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.0' = { + name: 'user-assigned-identity' + params: { + name: 'managed-identity-${resourceToken}' + location: location + tags: tags + } +} + +var databaseName = 'cosmicworks' +var containerName = 'products' + +module cosmosDbAccount 'br/public:avm/res/document-db/database-account:0.8.1' = { + name: 'cosmos-db-account' params: { - identityName: '${abbreviations.userAssignedIdentity}-${resourceToken}' + name: 'cosmos-db-nosql-${resourceToken}' location: location + locations: [ + { + failoverPriority: 0 + locationName: location + isZoneRedundant: false + } + ] tags: tags + disableKeyBasedMetadataWriteAccess: true + disableLocalAuth: true + networkRestrictions: { + publicNetworkAccess: 'Enabled' + ipRules: [] + virtualNetworkRules: [] + } + capabilitiesToAdd: [ + 'EnableServerless' + ] + sqlRoleDefinitions: [ + { + name: 'nosql-data-plane-contributor' + dataAction: [ + 'Microsoft.DocumentDB/databaseAccounts/readMetadata' + 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*' + 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*' + ] + } + ] + sqlRoleAssignmentsPrincipalIds: union( + [ + managedIdentity.outputs.principalId + ], + !empty(deploymentUserPrincipalId) ? [deploymentUserPrincipalId] : [] + ) + sqlDatabases: [ + { + name: databaseName + containers: [ + { + name: containerName + paths: [ + '/category' + ] + } + ] + } + ] } } -module database 'app/database.bicep' = { - name: 'database' +module containerRegistry 'br/public:avm/res/container-registry/registry:0.5.1' = { + name: 'container-registry' params: { - accountName: !empty(cosmosDbAccountName) ? cosmosDbAccountName : '${abbreviations.cosmosDbAccount}-${resourceToken}' + name: 'containerreg${resourceToken}' location: location tags: tags - appPrincipalId: identity.outputs.principalId - userPrincipalId: !empty(principalId) ? principalId : null + acrAdminUserEnabled: false + anonymousPullEnabled: true + publicNetworkAccess: 'Enabled' + acrSku: 'Standard' + } +} + +var containerRegistryRole = subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '8311e382-0749-4cb8-b61a-304f252e45ec' +) // AcrPush built-in role + +module registryUserAssignment 'br/public:avm/ptn/authorization/resource-role-assignment:0.1.1' = if (!empty(deploymentUserPrincipalId)) { + name: 'container-registry-role-assignment-push-user' + params: { + principalId: deploymentUserPrincipalId + resourceId: containerRegistry.outputs.resourceId + roleDefinitionId: containerRegistryRole } } -module registry 'app/registry.bicep' = { - name: 'registry' +module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.7.0' = { + name: 'log-analytics-workspace' params: { - registryName: !empty(containerRegistryName) ? containerRegistryName : '${abbreviations.containerRegistry}${resourceToken}' + name: 'log-analytics-${resourceToken}' location: location tags: tags } } -module web 'app/web.bicep' = { - name: serviceName +module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.8.0' = { + name: 'container-apps-env' params: { - workspaceName: !empty(logWorkspaceName) ? logWorkspaceName : '${abbreviations.logAnalyticsWorkspace}-${resourceToken}' - envName: !empty(containerAppsEnvName) ? containerAppsEnvName : '${abbreviations.containerAppsEnv}-${resourceToken}' - appName: !empty(containerAppsAppName) ? containerAppsAppName : '${abbreviations.containerAppsApp}-${resourceToken}' + name: 'container-env-${resourceToken}' location: location tags: tags - serviceTag: serviceName - appResourceId: identity.outputs.resourceId - appClientId: identity.outputs.clientId - databaseAccountEndpoint: database.outputs.endpoint + logAnalyticsWorkspaceResourceId: logAnalyticsWorkspace.outputs.resourceId + zoneRedundant: false + } +} + +module containerAppsApp 'br/public:avm/res/app/container-app:0.9.0' = { + name: 'container-apps-app' + params: { + name: 'container-app-${resourceToken}' + environmentResourceId: containerAppsEnvironment.outputs.resourceId + location: location + tags: union(tags, { 'azd-service-name': serviceName }) + ingressTargetPort: 8080 + ingressExternal: true + ingressTransport: 'auto' + stickySessionsAffinity: 'sticky' + corsPolicy: { + allowCredentials: true + allowedOrigins: [ + '*' + ] + } + managedIdentities: { + systemAssigned: false + userAssignedResourceIds: [ + managedIdentity.outputs.resourceId + ] + } + secrets: { + secureList: [ + { + name: 'azure-cosmos-db-nosql-endpoint' + value: cosmosDbAccount.outputs.endpoint + } + { + name: 'user-assigned-managed-identity-client-id' + value: managedIdentity.outputs.clientId + } + ] + } + containers: [ + { + image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' + name: 'web-front-end' + resources: { + cpu: '0.25' + memory: '.5Gi' + } + env: [ + { + name: 'CONFIGURATION__AZURECOSMOSDB__ENDPOINT' + secretRef: 'azure-cosmos-db-nosql-endpoint' + } + { + name: 'CONFIGURATION__AZURECOSMOSDB__DATABASENAME' + value: databaseName + } + { + name: 'CONFIGURATION__AZURECOSMOSDB__CONTAINERNAME' + value: containerName + } + { + name: 'AZURE_CLIENT_ID' + secretRef: 'user-assigned-managed-identity-client-id' + } + ] + } + ] } } -output AZURE_COSMOS_DB_NOSQL_ENDPOINT string = database.outputs.endpoint -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = registry.outputs.endpoint +// Azure Cosmos DB for Table outputs +output CONFIGURATION__AZURECOSMOSDB__ENDPOINT string = cosmosDbAccount.outputs.endpoint +output CONFIGURATION__AZURECOSMOSDB__DATABASENAME string = databaseName +output CONFIGURATION__AZURECOSMOSDB__CONTAINERNAME string = containerName + +// Azure Container Registry outputs +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer diff --git a/infra/main.bicepparam b/infra/main.bicepparam new file mode 100644 index 0000000..c0af6f2 --- /dev/null +++ b/infra/main.bicepparam @@ -0,0 +1,5 @@ +using './main.bicep' + +param environmentName = readEnvironmentVariable('AZURE_ENV_NAME', 'development') +param location = readEnvironmentVariable('AZURE_LOCATION', 'westus') +param deploymentUserPrincipalId = readEnvironmentVariable('AZURE_PRINCIPAL_ID', '') diff --git a/infra/main.parameters.json b/infra/main.parameters.json deleted file mode 100644 index 67ad852..0000000 --- a/infra/main.parameters.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "environmentName": { - "value": "${AZURE_ENV_NAME}" - }, - "location": { - "value": "${AZURE_LOCATION}" - }, - "principalId": { - "value": "${AZURE_PRINCIPAL_ID}" - } - } -} \ No newline at end of file diff --git a/src/models/Settings/Configuration.cs b/src/models/Settings/Configuration.cs new file mode 100644 index 0000000..57d09aa --- /dev/null +++ b/src/models/Settings/Configuration.cs @@ -0,0 +1,15 @@ +namespace Microsoft.Samples.Cosmos.NoSQL.Quickstart.Models.Settings; + +public record Configuration +{ + public required AzureCosmosDB AzureCosmosDB { get; init; } +} + +public record AzureCosmosDB +{ + public required string Endpoint { get; init; } + + public required string DatabaseName { get; init; } + + public required string ContainerName { get; init; } +} \ No newline at end of file diff --git a/src/services/DemoService.cs b/src/services/DemoService.cs index 28a60bc..b0e39c4 100644 --- a/src/services/DemoService.cs +++ b/src/services/DemoService.cs @@ -1,21 +1,30 @@ +using System.Configuration; using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.Options; using Microsoft.Samples.Cosmos.NoSQL.Quickstart.Models; using Microsoft.Samples.Cosmos.NoSQL.Quickstart.Services.Interfaces; +using Settings = Microsoft.Samples.Cosmos.NoSQL.Quickstart.Models.Settings; + namespace Microsoft.Samples.Cosmos.NoSQL.Quickstart.Services; -public sealed class DemoService(CosmosClient client) : IDemoService +public sealed class DemoService( + CosmosClient client, + IOptions configurationOptions +) : IDemoService { + private readonly Settings.Configuration configuration = configurationOptions.Value; + public string GetEndpoint() => $"{client.Endpoint}"; public async Task RunAsync(Func writeOutputAync) { - Database database = client.GetDatabase("cosmicworks"); + Database database = client.GetDatabase(configuration.AzureCosmosDB.DatabaseName); database = await database.ReadAsync(); await writeOutputAync($"Get database:\t{database.Id}"); - Container container = database.GetContainer("products"); + Container container = database.GetContainer(configuration.AzureCosmosDB.ContainerName); container = await container.ReadContainerAsync(); await writeOutputAync($"Get container:\t{container.Id}"); diff --git a/src/services/Microsoft.Samples.Cosmos.NoSQL.Quickstart.Services.csproj b/src/services/Microsoft.Samples.Cosmos.NoSQL.Quickstart.Services.csproj index 3a8972d..3f1a720 100644 --- a/src/services/Microsoft.Samples.Cosmos.NoSQL.Quickstart.Services.csproj +++ b/src/services/Microsoft.Samples.Cosmos.NoSQL.Quickstart.Services.csproj @@ -5,8 +5,9 @@ enable - - + + + diff --git a/src/web/Microsoft.Samples.Cosmos.NoSQL.Quickstart.Web.csproj b/src/web/Microsoft.Samples.Cosmos.NoSQL.Quickstart.Web.csproj index 952768b..814ed0a 100644 --- a/src/web/Microsoft.Samples.Cosmos.NoSQL.Quickstart.Web.csproj +++ b/src/web/Microsoft.Samples.Cosmos.NoSQL.Quickstart.Web.csproj @@ -6,9 +6,9 @@ f6167579-5a7c-405e-bdae-cf20a79d6b9d - - - + + + diff --git a/src/web/Program.cs b/src/web/Program.cs index 6cf4d54..37340ad 100644 --- a/src/web/Program.cs +++ b/src/web/Program.cs @@ -1,18 +1,26 @@ using Azure.Identity; using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.Options; using Microsoft.Samples.Cosmos.NoSQL.Quickstart.Services; using Microsoft.Samples.Cosmos.NoSQL.Quickstart.Services.Interfaces; using Microsoft.Samples.Cosmos.NoSQL.Quickstart.Web.Components; +using Settings = Microsoft.Samples.Cosmos.NoSQL.Quickstart.Models.Settings; + var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorComponents().AddInteractiveServerComponents(); -builder.Services.AddSingleton((_) => +builder.Services.AddOptions().Bind(builder.Configuration.GetSection(nameof(Settings.Configuration))); + +builder.Services.AddSingleton((serviceProvider) => { + IOptions configurationOptions = serviceProvider.GetRequiredService>(); + Settings.Configuration configuration = configurationOptions.Value; + // CosmosClient client = new( - accountEndpoint: builder.Configuration["AZURE_COSMOS_DB_NOSQL_ENDPOINT"]!, + accountEndpoint: configuration.AzureCosmosDB.Endpoint, tokenCredential: new DefaultAzureCredential() ); // diff --git a/src/web/appsettings.json b/src/web/appsettings.json index c5f5d9e..945b1bb 100644 --- a/src/web/appsettings.json +++ b/src/web/appsettings.json @@ -6,5 +6,11 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" -} + "AllowedHosts": "*", + "Configuration": { + "AzureCosmosDB": { + "DatabaseName": "cosmicworks", + "ContainerName": "products" + } + } +} \ No newline at end of file