diff --git a/templates/todo/api/python/todo/models.py b/templates/todo/api/python/todo/models.py index 7710395dc9d..e6933bafb15 100644 --- a/templates/todo/api/python/todo/models.py +++ b/templates/todo/api/python/todo/models.py @@ -20,11 +20,12 @@ def __init__(self, *args, **kwargs): credential = DefaultAzureCredential() keyvault_client = SecretClient(self.AZURE_KEY_VAULT_ENDPOINT, credential) for secret in keyvault_client.list_properties_of_secrets(): - setattr( - self, - keyvault_name_as_attr(secret.name), - keyvault_client.get_secret(secret.name).value, - ) + if secret.name == "AZURE-COSMOS-CONNECTION-STRING": + setattr( + self, + keyvault_name_as_attr(secret.name), + keyvault_client.get_secret(secret.name).value, + ) AZURE_COSMOS_CONNECTION_STRING: str = "" AZURE_COSMOS_DATABASE_NAME: str = "Todo" diff --git a/templates/todo/common/infra/bicep/app/apim-api-settings.bicep b/templates/todo/common/infra/bicep/app/apim-api-settings.bicep new file mode 100644 index 00000000000..3b3f342861c --- /dev/null +++ b/templates/todo/common/infra/bicep/app/apim-api-settings.bicep @@ -0,0 +1,99 @@ +@description('Resource name for the existing apim service') +param name string + +@description('Resource name to uniquely identify this API within the API Management service instance') +@minLength(1) +param apiName string + +@description('Relative URL uniquely identifying this API and all of its resource paths within the API Management service instance. It is appended to the API endpoint base URL specified during the service instance creation to form a public URL for this API.') +@minLength(1) +param apiPath string + +@description('Resource name for the existing applicationInsights service') +param applicationInsightsName string + +@description('Resource name for backend Web App or Function App') +param apiAppName string = '' + +// Necessary due to https://github.com/Azure/bicep/issues/9594 +// placeholderName is never deployed, it is merely used to make the child name validation pass +var appNameForBicep = !empty(apiAppName) ? apiAppName : 'placeholderName' + +resource apiDiagnostics 'Microsoft.ApiManagement/service/apis/diagnostics@2021-12-01-preview' = { + name: 'applicationinsights' + parent: apimService::restApi + properties: { + alwaysLog: 'allErrors' + backend: { + request: { + body: { + bytes: 1024 + } + } + response: { + body: { + bytes: 1024 + } + } + } + frontend: { + request: { + body: { + bytes: 1024 + } + } + response: { + body: { + bytes: 1024 + } + } + } + httpCorrelationProtocol: 'W3C' + logClientIp: true + loggerId: apimLogger.id + metrics: true + sampling: { + percentage: 100 + samplingType: 'fixed' + } + verbosity: 'verbose' + } +} + +resource apiAppProperties 'Microsoft.Web/sites/config@2022-03-01' = if (!empty(apiAppName)) { + name: '${appNameForBicep}/web' + kind: 'string' + properties: { + apiManagementConfig: { + id: '${apimService.id}/apis/${apiName}' + } + } +} + +resource apimLogger 'Microsoft.ApiManagement/service/loggers@2021-12-01-preview' = if (!empty(applicationInsightsName)) { + name: 'app-insights-logger' + parent: apimService + properties: { + credentials: { + instrumentationKey: applicationInsights.properties.InstrumentationKey + } + description: 'Logger to Azure Application Insights' + isBuffered: false + loggerType: 'applicationInsights' + resourceId: applicationInsights.id + } +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { + name: applicationInsightsName +} + +resource apimService 'Microsoft.ApiManagement/service@2021-08-01' existing = { + name: name + + resource restApi 'apis@2021-12-01-preview' existing = { + name: apiName + } +} + +output SERVICE_API_URI string = '${apimService.properties.gatewayUrl}/${apiPath}' diff --git a/templates/todo/common/infra/bicep/app/applicationinsights-dashboard.bicep b/templates/todo/common/infra/bicep/app/applicationinsights-dashboard.bicep new file mode 100644 index 00000000000..d082e668ed9 --- /dev/null +++ b/templates/todo/common/infra/bicep/app/applicationinsights-dashboard.bicep @@ -0,0 +1,1236 @@ +metadata description = 'Creates a dashboard for an Application Insights instance.' +param name string +param applicationInsightsName string +param location string = resourceGroup().location +param tags object = {} + +// 2020-09-01-preview because that is the latest valid version +resource applicationInsightsDashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = { + name: name + location: location + tags: tags + properties: { + lenses: [ + { + order: 0 + parts: [ + { + position: { + x: 0 + y: 0 + colSpan: 2 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'id' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/AspNetOverviewPinnedPart' + asset: { + idInputName: 'id' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'overview' + } + } + { + position: { + x: 2 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/ProactiveDetectionAsyncPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'ProactiveDetection' + } + } + { + position: { + x: 3 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'ResourceId' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/QuickPulseButtonSmallPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 4 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + endTime: null + createdTime: '2018-05-04T01:20:33.345Z' + isInitialTime: true + grain: 1 + useDashboardTimeRange: false + } + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/AvailabilityNavButtonPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 5 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + endTime: null + createdTime: '2018-05-08T18:47:35.237Z' + isInitialTime: true + grain: 1 + useDashboardTimeRange: false + } + } + { + name: 'ConfigurationId' + value: '78ce933e-e864-4b05-a27b-71fd55a6afad' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/AppMapButtonPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 0 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Usage' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 3 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + endTime: null + createdTime: '2018-05-04T01:22:35.782Z' + isInitialTime: true + grain: 1 + useDashboardTimeRange: false + } + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/UsageUsersOverviewPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 4 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Reliability' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 7 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ResourceId' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + { + name: 'DataModel' + value: { + version: '1.0.0' + timeContext: { + durationMs: 86400000 + createdTime: '2018-05-04T23:42:40.072Z' + isInitialTime: false + grain: 1 + useDashboardTimeRange: false + } + } + isOptional: true + } + { + name: 'ConfigurationId' + value: '8a02f7bf-ac0f-40e1-afe9-f0e72cfee77f' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/CuratedBladeFailuresPinnedPart' + isAdapter: true + asset: { + idInputName: 'ResourceId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'failures' + } + } + { + position: { + x: 8 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Responsiveness\r\n' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 11 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ResourceId' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + { + name: 'DataModel' + value: { + version: '1.0.0' + timeContext: { + durationMs: 86400000 + createdTime: '2018-05-04T23:43:37.804Z' + isInitialTime: false + grain: 1 + useDashboardTimeRange: false + } + } + isOptional: true + } + { + name: 'ConfigurationId' + value: '2a8ede4f-2bee-4b9c-aed9-2db0e8a01865' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/CuratedBladePerformancePinnedPart' + isAdapter: true + asset: { + idInputName: 'ResourceId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'performance' + } + } + { + position: { + x: 12 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Browser' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 15 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'MetricsExplorerJsonDefinitionId' + value: 'BrowserPerformanceTimelineMetrics' + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + createdTime: '2018-05-08T12:16:27.534Z' + isInitialTime: false + grain: 1 + useDashboardTimeRange: false + } + } + { + name: 'CurrentFilter' + value: { + eventTypes: [ + 4 + 1 + 3 + 5 + 2 + 6 + 13 + ] + typeFacets: {} + isPermissive: false + } + } + { + name: 'id' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/MetricsExplorerBladePinnedPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'browser' + } + } + { + position: { + x: 0 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'sessions/count' + aggregationType: 5 + namespace: 'microsoft.insights/components/kusto' + metricVisualization: { + displayName: 'Sessions' + color: '#47BDF5' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'users/count' + aggregationType: 5 + namespace: 'microsoft.insights/components/kusto' + metricVisualization: { + displayName: 'Users' + color: '#7E58FF' + } + } + ] + title: 'Unique sessions and users' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'segmentationUsers' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 4 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'requests/failed' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Failed requests' + color: '#EC008C' + } + } + ] + title: 'Failed requests' + visualization: { + chartType: 3 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'failures' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 8 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'requests/duration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Server response time' + color: '#00BCF2' + } + } + ] + title: 'Server response time' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'performance' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 12 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/networkDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Page load network connect time' + color: '#7E58FF' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/processingDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Client processing time' + color: '#44F1C8' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/sendDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Send request time' + color: '#EB9371' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/receiveDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Receiving response time' + color: '#0672F1' + } + } + ] + title: 'Average page load time breakdown' + visualization: { + chartType: 3 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 0 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'availabilityResults/availabilityPercentage' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Availability' + color: '#47BDF5' + } + } + ] + title: 'Average availability' + visualization: { + chartType: 3 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'availability' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 4 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'exceptions/server' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Server exceptions' + color: '#47BDF5' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'dependencies/failed' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Dependency failures' + color: '#7E58FF' + } + } + ] + title: 'Server exceptions and Dependency failures' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 8 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/processorCpuPercentage' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Processor time' + color: '#47BDF5' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/processCpuPercentage' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Process CPU' + color: '#7E58FF' + } + } + ] + title: 'Average processor and process CPU utilization' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 12 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'exceptions/browser' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Browser exceptions' + color: '#47BDF5' + } + } + ] + title: 'Browser exceptions' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 0 + y: 8 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'availabilityResults/count' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Availability test results count' + color: '#47BDF5' + } + } + ] + title: 'Availability test results count' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 4 + y: 8 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/processIOBytesPerSecond' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Process IO rate' + color: '#47BDF5' + } + } + ] + title: 'Average process I/O rate' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 8 + y: 8 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/memoryAvailableBytes' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Available memory' + color: '#47BDF5' + } + } + ] + title: 'Average available memory' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + ] + } + ] + } +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { + name: applicationInsightsName +} diff --git a/templates/todo/common/infra/bicep/app/sql-deployment-script.bicep b/templates/todo/common/infra/bicep/app/sql-deployment-script.bicep new file mode 100644 index 00000000000..fcbb9ea7081 --- /dev/null +++ b/templates/todo/common/infra/bicep/app/sql-deployment-script.bicep @@ -0,0 +1,77 @@ +param location string = resourceGroup().location + +@description('Application user name') +param appUser string = 'appUser' + +@description('SQL Server administrator name') +param sqlAdmin string = 'sqlAdmin' + +@description('The name for sql database ') +param sqlDatabaseName string + +@description('Resource name for sql service') +param sqlServiceName string + +@secure() +@description('SQL Server administrator password') +param sqlAdminPassword string + +@secure() +@description('Application user password') +param appUserPassword string + +module deploymentScript 'br/public:avm/res/resources/deployment-script:0.1.3' = { + name: 'deployment-script' + params: { + kind: 'AzureCLI' + name: 'deployment-script' + azCliVersion: '2.37.0' + location: location + retentionInterval: 'PT1H' + timeout: 'PT5M' + cleanupPreference: 'OnSuccess' + environmentVariables:{ + secureList: [ + { + name: 'APPUSERNAME' + value: appUser + } + { + name: 'APPUSERPASSWORD' + secureValue: appUserPassword + } + { + name: 'DBNAME' + value: !empty(sqlDatabaseName) ? sqlDatabaseName : 'Todo' + } + { + name: 'DBSERVER' + value: '${sqlServiceName}${environment().suffixes.sqlServerHostname}' + } + { + name: 'SQLCMDPASSWORD' + secureValue: sqlAdminPassword + } + { + name: 'SQLADMIN' + value: sqlAdmin + } + ] + } + scriptContent: ''' +wget https://github.com/microsoft/go-sqlcmd/releases/download/v0.8.1/sqlcmd-v0.8.1-linux-x64.tar.bz2 +tar x -f sqlcmd-v0.8.1-linux-x64.tar.bz2 -C . + +cat < ./initDb.sql +drop user if exists ${APPUSERNAME} +go +create user ${APPUSERNAME} with password = '${APPUSERPASSWORD}' +go +alter role db_owner add member ${APPUSERNAME} +go +SCRIPT_END + +./sqlcmd -S ${DBSERVER} -d ${DBNAME} -U ${SQLADMIN} -i ./initDb.sql + ''' + } +} diff --git a/templates/todo/projects/csharp-cosmos-sql/.repo/bicep/infra/main.bicep b/templates/todo/projects/csharp-cosmos-sql/.repo/bicep/infra/main.bicep index 85d3f204650..c2e2c275ea0 100644 --- a/templates/todo/projects/csharp-cosmos-sql/.repo/bicep/infra/main.bicep +++ b/templates/todo/projects/csharp-cosmos-sql/.repo/bicep/infra/main.bicep @@ -24,19 +24,19 @@ param logAnalyticsName string = '' param resourceGroupName string = '' param webServiceName string = '' param apimServiceName string = '' +param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' @description('Flag to use Azure API Management to mediate the calls between the Web frontend and the backend API') param useAPIM bool = false -@description('API Management SKU to use if APIM is enabled') -param apimSku string = 'Consumption' - @description('Id of the user or app to assign application roles') param principalId string = '' var abbrs = loadJsonContent('../../../../../../common/infra/bicep/abbreviations.json') var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) var tags = { 'azd-env-name': environmentName } +var webUri = 'https://${web.outputs.defaultHostname}' +var apiUri = 'https://${api.outputs.defaultHostname}' // Organize resources in a resource group resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { @@ -46,46 +46,79 @@ resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { } // The application frontend -module web '../../../../../common/infra/bicep/app/web-appservice.bicep' = { +module web 'br/public:avm/res/web/site:0.3.4' = { name: 'web' scope: rg params: { + kind: 'app' name: !empty(webServiceName) ? webServiceName : '${abbrs.webSitesAppService}web-${resourceToken}' + serverFarmResourceId: appServicePlan.outputs.resourceId + tags: union(tags, { 'azd-service-name': 'web' }) location: location - tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName - appServicePlanId: appServicePlan.outputs.id + appInsightResourceId: applicationInsights.outputs.resourceId + siteConfig: { + appCommandLine: 'pm2 serve /home/site/wwwroot --no-daemon --spa' + linuxFxVersion: 'node|20-lts' + alwaysOn: true + } } } // The application backend -module api '../../../../../common/infra/bicep/app/api-appservice-dotnet.bicep' = { +module api 'br/public:avm/res/web/site:0.3.4' = { name: 'api' scope: rg params: { + kind: 'app' name: !empty(apiServiceName) ? apiServiceName : '${abbrs.webSitesAppService}api-${resourceToken}' + serverFarmResourceId: appServicePlan.outputs.resourceId + tags: union(tags, { 'azd-service-name': 'api' }) location: location - tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName - appServicePlanId: appServicePlan.outputs.id - keyVaultName: keyVault.outputs.name - allowedOrigins: [ web.outputs.SERVICE_WEB_URI ] - appSettings: { - AZURE_COSMOS_CONNECTION_STRING_KEY: cosmos.outputs.connectionStringKey - AZURE_COSMOS_DATABASE_NAME: cosmos.outputs.databaseName + appInsightResourceId: applicationInsights.outputs.resourceId + managedIdentities: { + systemAssigned: true + } + siteConfig: { + cors: { + allowedOrigins: [ 'https://portal.azure.com', 'https://ms.portal.azure.com' , webUri ] + } + alwaysOn: true + linuxFxVersion: 'dotnetcore|8.0' + appCommandLine: '' + } + appSettingsKeyValuePairs: { + AZURE_KEY_VAULT_ENDPOINT: keyVault.outputs.uri + AZURE_COSMOS_CONNECTION_STRING_KEY: connectionStringKey + AZURE_COSMOS_DATABASE_NAME: !empty(cosmosDatabaseName) ? cosmosDatabaseName: 'Todo' AZURE_COSMOS_ENDPOINT: cosmos.outputs.endpoint - API_ALLOW_ORIGINS: web.outputs.SERVICE_WEB_URI + API_ALLOW_ORIGINS: webUri + SCM_DO_BUILD_DURING_DEPLOYMENT: 'False' + ENABLE_ORYX_BUILD: 'True' } } } // Give the API access to KeyVault -module apiKeyVaultAccess '../../../../../../common/infra/bicep/core/security/keyvault-access.bicep' = { - name: 'api-keyvault-access' +module accesskeyvault 'br/public:avm/res/key-vault/vault:0.5.1' = { + name: 'accesskeyvault' scope: rg params: { - keyVaultName: keyVault.outputs.name - principalId: api.outputs.SERVICE_API_IDENTITY_PRINCIPAL_ID + name: keyVault.outputs.name + enableRbacAuthorization: false + accessPolicies: [ + { + objectId: principalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + { + objectId: api.outputs.systemAssignedMIPrincipalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + ] } } @@ -96,7 +129,7 @@ module apiCosmosSqlRoleAssign '../../../../../../common/infra/bicep/core/databas params: { accountName: cosmos.outputs.accountName roleDefinitionId: cosmos.outputs.roleDefinitionId - principalId: api.outputs.SERVICE_API_IDENTITY_PRINCIPAL_ID + principalId: api.outputs.systemAssignedMIPrincipalId } } @@ -125,85 +158,121 @@ module cosmos '../../../../../common/infra/bicep/app/cosmos-sql-db.bicep' = { } // Create an App Service Plan to group applications under the same payment plan and SKU -module appServicePlan '../../../../../../common/infra/bicep/core/host/appserviceplan.bicep' = { +module appServicePlan 'br/public:avm/res/web/serverfarm:0.1.1' = { name: 'appserviceplan' scope: rg params: { name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}' - location: location - tags: tags sku: { name: 'B3' + tier: 'Basic' } + location: location + tags: tags + reserved: true + kind: 'Linux' } } -// Store secrets in a keyvault -module keyVault '../../../../../../common/infra/bicep/core/security/keyvault.bicep' = { +// Create a keyvault to store secrets +module keyVault 'br/public:avm/res/key-vault/vault:0.5.1' = { name: 'keyvault' scope: rg params: { name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' location: location tags: tags - principalId: principalId + enableRbacAuthorization: false } } -// Monitor application with Azure Monitor -module monitoring '../../../../../../common/infra/bicep/core/monitor/monitoring.bicep' = { - name: 'monitoring' +// Monitor application with Azure loganalytics +module loganalytics 'br/public:avm/res/operational-insights/workspace:0.3.4' = { + name: 'loganalytics' scope: rg params: { + name: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' location: location - tags: tags - logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' - applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' - applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + } +} + +// Monitor application with Azure applicationInsights +module applicationInsights 'br/public:avm/res/insights/component:0.3.0' = { + name: 'applicationInsights' + scope: rg + params: { + name: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + workspaceResourceId: loganalytics.outputs.resourceId + location: location + } +} + +// Monitor application with Azure applicationInsightsDashboard +module applicationInsightsDashboard '../../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep' = { + name: 'application-insights-dashboard' + scope: rg + params: { + name: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + location: location + applicationInsightsName: applicationInsights.outputs.name } } // Creates Azure API Management (APIM) service to mediate the requests between the frontend and the backend API -module apim '../../../../../../common/infra/bicep/core/gateway/apim.bicep' = if (useAPIM) { +module apim 'br/public:avm/res/api-management/service:0.1.6' = if (useAPIM) { name: 'apim-deployment' scope: rg params: { name: !empty(apimServiceName) ? apimServiceName : '${abbrs.apiManagementService}${resourceToken}' - sku: apimSku + publisherEmail: 'noreply@microsoft.com' + publisherName: 'n/a' location: location tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName + apis: [ + { + name: 'todo-api' + path: 'todo' + displayName: 'Simple Todo API' + apiDescription: 'This is a simple Todo API' + serviceUrl: apiUri + subscriptionRequired: false + value: loadTextContent('../../../../../api/common/openapi.yaml') + policies: [ + { + value: replace(loadTextContent('../../../../../../common/infra/shared/gateway/apim/apim-api-policy.xml'), '{origin}', webUri) + format: 'rawxml' + } + ] + } + ] } } // Configures the API in the Azure API Management (APIM) service -module apimApi '../../../../../common/infra/bicep/app/apim-api.bicep' = if (useAPIM) { - name: 'apim-api-deployment' +module apimsettings '../../../../../common/infra/bicep/app/apim-api-settings.bicep' = if (useAPIM) { + name: 'apim-api-settings' scope: rg params: { - name: useAPIM ? apim.outputs.apimServiceName : '' + apiAppName: api.outputs.name apiName: 'todo-api' - apiDisplayName: 'Simple Todo API' - apiDescription: 'This is a simple Todo API' + name: useAPIM ? apim.outputs.name : '' apiPath: 'todo' - webFrontendUrl: web.outputs.SERVICE_WEB_URI - apiBackendUrl: api.outputs.SERVICE_API_URI - apiAppName: api.outputs.SERVICE_API_NAME + applicationInsightsName: applicationInsights.outputs.name } } // Data outputs output AZURE_COSMOS_ENDPOINT string = cosmos.outputs.endpoint -output AZURE_COSMOS_CONNECTION_STRING_KEY string = cosmos.outputs.connectionStringKey -output AZURE_COSMOS_DATABASE_NAME string = cosmos.outputs.databaseName +output AZURE_COSMOS_CONNECTION_STRING_KEY string = connectionStringKey +output AZURE_COSMOS_DATABASE_NAME string = !empty(cosmosDatabaseName) ? cosmosDatabaseName: 'Todo' // App outputs -output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString -output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint +output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsights.outputs.connectionString +output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.uri output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId -output API_BASE_URL string = useAPIM ? apimApi.outputs.SERVICE_API_URI : api.outputs.SERVICE_API_URI -output REACT_APP_WEB_BASE_URL string = web.outputs.SERVICE_WEB_URI +output API_BASE_URL string = useAPIM ? 'https://${apim.outputs.name}.azure-api.net/todo' : apiUri +output REACT_APP_WEB_BASE_URL string = webUri output USE_APIM bool = useAPIM -output SERVICE_API_ENDPOINTS array = useAPIM ? [ apimApi.outputs.SERVICE_API_URI, api.outputs.SERVICE_API_URI ]: [] +output SERVICE_API_ENDPOINTS array = useAPIM ? [ 'https://${apim.outputs.name}.azure-api.net/todo', apiUri ]: [] diff --git a/templates/todo/projects/csharp-cosmos-sql/.repo/bicep/repo.yaml b/templates/todo/projects/csharp-cosmos-sql/.repo/bicep/repo.yaml index 052a5d9b186..52b3f09367b 100644 --- a/templates/todo/projects/csharp-cosmos-sql/.repo/bicep/repo.yaml +++ b/templates/todo/projects/csharp-cosmos-sql/.repo/bicep/repo.yaml @@ -79,6 +79,16 @@ repo: to: ../../src/api/wwwroot/openapi.yaml patterns: - "apim-api.bicep" + + - from: ../../../api/common/openapi.yaml + to: ../src/api/wwwroot/openapi.yaml + patterns: + - "**/main.bicep" + + - from: ../../../common/infra/shared/gateway/apim/apim-api-policy.xml + to: ./app/apim-api-policy.xml + patterns: + - "**/main.bicep" assets: # Common assets @@ -105,6 +115,12 @@ repo: - from: ../../../../common/infra/bicep/app/cosmos-sql-db.bicep to: ./infra/app/db.bicep + - from: ../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep + to: ./infra/app/applicationinsights-dashboard.bicep + + - from: ../../../../common/infra/bicep/app/apim-api-settings.bicep + to: ./infra/app/apim-api-settings.bicep + - from: ./../../ to: ./ ignore: diff --git a/templates/todo/projects/csharp-sql-swa-func/.repo/bicep/infra/main.bicep b/templates/todo/projects/csharp-sql-swa-func/.repo/bicep/infra/main.bicep index bc24c85bfb0..8c4a0ca0b5e 100644 --- a/templates/todo/projects/csharp-sql-swa-func/.repo/bicep/infra/main.bicep +++ b/templates/todo/projects/csharp-sql-swa-func/.repo/bicep/infra/main.bicep @@ -25,13 +25,13 @@ param sqlDatabaseName string = '' param sqlServerName string = '' param webServiceName string = '' param apimServiceName string = '' +param appUser string = 'appUser' +param sqlAdmin string = 'sqlAdmin' +param connectionStringKey string = 'AZURE-SQL-CONNECTION-STRING' @description('Flag to use Azure API Management to mediate the calls between the Web frontend and the backend API') param useAPIM bool = false -@description('API Management SKU to use if APIM is enabled') -param apimSku string = 'Consumption' - @description('Id of the user or app to assign application roles') param principalId string = '' @@ -46,6 +46,8 @@ param appUserPassword string var abbrs = loadJsonContent('../../../../../../common/infra/bicep/abbreviations.json') var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) var tags = { 'azd-env-name': environmentName } +var webUri = 'https://${web.outputs.defaultHostname}' +var apiUri = 'https://${api.outputs.defaultHostname}' // Organize resources in a resource group resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { @@ -55,150 +57,263 @@ resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { } // The application frontend -module web '../../../../../common/infra/bicep/app/web-staticwebapp.bicep' = { - name: 'web' +module web 'br/public:avm/res/web/static-site:0.3.0' = { + name: 'staticWeb' scope: rg params: { name: !empty(webServiceName) ? webServiceName : '${abbrs.webStaticSites}web-${resourceToken}' location: location - tags: tags + provider: 'Custom' + tags: union(tags, { 'azd-service-name': 'web' }) } } // The application backend -module api '../../../../../common/infra/bicep/app/api-functions-dotnet-isolated.bicep' = { +module api 'br/public:avm/res/web/site:0.3.4' = { name: 'api' scope: rg params: { + kind: 'functionapp' name: !empty(apiServiceName) ? apiServiceName : '${abbrs.webSitesFunctions}api-${resourceToken}' + serverFarmResourceId: appServicePlan.outputs.resourceId + tags: union(tags, { 'azd-service-name': 'api' }) location: location - tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName - appServicePlanId: appServicePlan.outputs.id - keyVaultName: keyVault.outputs.name - storageAccountName: storage.outputs.name - allowedOrigins: [ web.outputs.SERVICE_WEB_URI ] - appSettings: { - AZURE_SQL_CONNECTION_STRING_KEY: sqlServer.outputs.connectionStringKey + appInsightResourceId: applicationInsights.outputs.resourceId + managedIdentities: { + systemAssigned: true + } + clientAffinityEnabled: false + siteConfig: { + cors: { + allowedOrigins: [ 'https://portal.azure.com', 'https://ms.portal.azure.com' , webUri ] + } + linuxFxVersion: 'dotnet-isolated|8.0' + use32BitWorkerProcess: false } + appSettingsKeyValuePairs: { + AZURE_KEY_VAULT_ENDPOINT: keyVault.outputs.uri + AZURE_SQL_CONNECTION_STRING_KEY: connectionStringKey + API_ALLOW_ORIGINS: webUri + FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' + SCM_DO_BUILD_DURING_DEPLOYMENT: 'False' + ENABLE_ORYX_BUILD: 'True' + } + storageAccountResourceId: storage.outputs.resourceId } } // Give the API access to KeyVault -module apiKeyVaultAccess '../../../../../../common/infra/bicep/core/security/keyvault-access.bicep' = { - name: 'api-keyvault-access' +module accesskeyvault 'br/public:avm/res/key-vault/vault:0.5.1' = { + name: 'accesskeyvault' scope: rg params: { - keyVaultName: keyVault.outputs.name - principalId: api.outputs.SERVICE_API_IDENTITY_PRINCIPAL_ID + name: keyVault.outputs.name + enableRbacAuthorization: false + accessPolicies: [ + { + objectId: principalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + { + objectId: api.outputs.systemAssignedMIPrincipalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + ] + secrets:{ + secureList: [ + { + name: 'sqlAdmin' + value: sqlAdminPassword + } + { + name: 'appUser' + value: appUserPassword + } + { + name: connectionStringKey + value: 'Server=${sqlService.outputs.name}${environment().suffixes.sqlServerHostname}; Database=${!empty(sqlDatabaseName) ? sqlDatabaseName : 'Todo'}; User=${appUser}; Password=${appUserPassword}' + } + ] + } } } // The application database -module sqlServer '../../../../../common/infra/bicep/app/sqlserver.bicep' = { - name: 'sql' +module sqlService 'br/public:avm/res/sql/server:0.2.0' = { + name: 'sqlservice' scope: rg params: { name: !empty(sqlServerName) ? sqlServerName : '${abbrs.sqlServers}${resourceToken}' - databaseName: sqlDatabaseName + administratorLogin: sqlAdmin + administratorLoginPassword: sqlAdminPassword location: location tags: tags - sqlAdminPassword: sqlAdminPassword + publicNetworkAccess: 'Enabled' + databases: [ + { + name: !empty(sqlDatabaseName) ? sqlDatabaseName : 'Todo' + } + ] + firewallRules:[ + { + name: 'Azure Services' + startIpAddress: '0.0.0.1' + endIpAddress: '255.255.255.254' + } + ] + } +} + +//Add appuser to database owner +module sqldeploymentscript '../../../../../common/infra/bicep/app/sql-deployment-script.bicep' = { + name: 'sqldeploymentscript' + scope: rg + params: { + location: location appUserPassword: appUserPassword - keyVaultName: keyVault.outputs.name + sqlAdminPassword: sqlAdminPassword + sqlDatabaseName: !empty(sqlDatabaseName) ? sqlDatabaseName : 'Todo' + sqlServiceName: sqlService.outputs.name } } // Create an App Service Plan to group applications under the same payment plan and SKU -module appServicePlan '../../../../../../common/infra/bicep/core/host/appserviceplan.bicep' = { +module appServicePlan 'br/public:avm/res/web/serverfarm:0.1.1' = { name: 'appserviceplan' scope: rg params: { name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}' - location: location - tags: tags sku: { name: 'Y1' tier: 'Dynamic' } + location: location + tags: tags + reserved: true + kind: 'Linux' } } // Backing storage for Azure functions backend API -module storage '../../../../../../common/infra/bicep/core/storage/storage-account.bicep' = { +module storage 'br/public:avm/res/storage/storage-account:0.8.3' = { name: 'storage' scope: rg params: { name: !empty(storageAccountName) ? storageAccountName : '${abbrs.storageStorageAccounts}${resourceToken}' + allowBlobPublicAccess: true + dnsEndpointType: 'Standard' + publicNetworkAccess:'Enabled' + networkAcls:{ + bypass: 'AzureServices' + defaultAction: 'Allow' + } location: location tags: tags } } -// Store secrets in a keyvault -module keyVault '../../../../../../common/infra/bicep/core/security/keyvault.bicep' = { +// Create a keyvault to store secrets +module keyVault 'br/public:avm/res/key-vault/vault:0.5.1' = { name: 'keyvault' scope: rg params: { name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' location: location tags: tags - principalId: principalId + enableRbacAuthorization: false } } -// Monitor application with Azure Monitor -module monitoring '../../../../../../common/infra/bicep/core/monitor/monitoring.bicep' = { - name: 'monitoring' +// Monitor application with Azure loganalytics +module loganalytics 'br/public:avm/res/operational-insights/workspace:0.3.4' = { + name: 'loganalytics' scope: rg params: { + name: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' location: location - tags: tags - logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' - applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' - applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + } +} + +// Monitor application with Azure applicationInsights +module applicationInsights 'br/public:avm/res/insights/component:0.3.0' = { + name: 'applicationInsights' + scope: rg + params: { + name: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + workspaceResourceId: loganalytics.outputs.resourceId + location: location + } +} + +// Monitor application with Azure applicationInsightsDashboard +module applicationInsightsDashboard '../../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep' = { + name: 'application-insights-dashboard' + scope: rg + params: { + name: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + location: location + applicationInsightsName: applicationInsights.outputs.name } } // Creates Azure API Management (APIM) service to mediate the requests between the frontend and the backend API -module apim '../../../../../../common/infra/bicep/core/gateway/apim.bicep' = if (useAPIM) { +module apim 'br/public:avm/res/api-management/service:0.1.3' = if (useAPIM) { name: 'apim-deployment' scope: rg params: { name: !empty(apimServiceName) ? apimServiceName : '${abbrs.apiManagementService}${resourceToken}' - sku: apimSku + publisherEmail: 'noreply@microsoft.com' + publisherName: 'n/a' location: location tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName + apis: [ + { + name: 'todo-api' + path: 'todo' + displayName: 'Simple Todo API' + apiDescription: 'This is a simple Todo API' + serviceUrl: apiUri + subscriptionRequired: false + value: loadTextContent('../../../../../api/common/openapi.yaml') + policies: [ + { + value: replace(loadTextContent('../../../../../../common/infra/shared/gateway/apim/apim-api-policy.xml'), '{origin}', webUri) + format: 'rawxml' + } + ] + } + ] } } // Configures the API in the Azure API Management (APIM) service -module apimApi '../../../../../common/infra/bicep/app/apim-api.bicep' = if (useAPIM) { - name: 'apim-api-deployment' +module apimsettings '../../../../../common/infra/bicep/app/apim-api-settings.bicep' = if (useAPIM) { + name: 'apim-api-settings' scope: rg params: { - name: useAPIM ? apim.outputs.apimServiceName : '' + apiAppName: api.outputs.name apiName: 'todo-api' - apiDisplayName: 'Simple Todo API' - apiDescription: 'This is a simple Todo API' + name: useAPIM ? apim.outputs.name : '' apiPath: 'todo' - webFrontendUrl: web.outputs.SERVICE_WEB_URI - apiBackendUrl: api.outputs.SERVICE_API_URI - apiAppName: api.outputs.SERVICE_API_NAME + applicationInsightsName: applicationInsights.outputs.name } } // Data outputs -output AZURE_SQL_CONNECTION_STRING_KEY string = sqlServer.outputs.connectionStringKey +output AZURE_SQL_CONNECTION_STRING_KEY string = connectionStringKey // App outputs -output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString -output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint +output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsights.outputs.connectionString +output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.uri output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId -output API_BASE_URL string = useAPIM ? apimApi.outputs.SERVICE_API_URI : api.outputs.SERVICE_API_URI -output REACT_APP_WEB_BASE_URL string = web.outputs.SERVICE_WEB_URI +output API_BASE_URL string = useAPIM ? 'https://${apim.outputs.name}.azure-api.net/todo' : apiUri +output REACT_APP_WEB_BASE_URL string = webUri output USE_APIM bool = useAPIM -output SERVICE_API_ENDPOINTS array = useAPIM ? [ apimApi.outputs.SERVICE_API_URI, api.outputs.SERVICE_API_URI ]: [] +output SERVICE_API_ENDPOINTS array = useAPIM ? [ 'https://${apim.outputs.name}.azure-api.net/todo', apiUri ]: [] diff --git a/templates/todo/projects/csharp-sql-swa-func/.repo/bicep/repo.yaml b/templates/todo/projects/csharp-sql-swa-func/.repo/bicep/repo.yaml index f3151e6472f..688bdc7e4cf 100644 --- a/templates/todo/projects/csharp-sql-swa-func/.repo/bicep/repo.yaml +++ b/templates/todo/projects/csharp-sql-swa-func/.repo/bicep/repo.yaml @@ -79,6 +79,16 @@ repo: to: ../../src/api/openapi.yaml patterns: - "apim-api.bicep" + + - from: ../../../api/common/openapi.yaml + to: ../src/api/openapi.yaml + patterns: + - "**/main.bicep" + + - from: ../../../common/infra/shared/gateway/apim/apim-api-policy.xml + to: ./app/apim-api-policy.xml + patterns: + - "**/main.bicep" assets: # # Common assets @@ -105,6 +115,15 @@ repo: - from: ../../../../common/infra/bicep/app/sqlserver.bicep to: ./infra/app/db.bicep + - from: ../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep + to: ./infra/app/applicationinsights-dashboard.bicep + + - from: ../../../../common/infra/bicep/app/apim-api-settings.bicep + to: ./infra/app/apim-api-settings.bicep + + - from: ../../../../common/infra/bicep/app/sql-deployment-script.bicep + to: ./infra/app/sql-deployment-script.bicep + - from: ./../../ to: ./ ignore: diff --git a/templates/todo/projects/csharp-sql/.repo/bicep/infra/main.bicep b/templates/todo/projects/csharp-sql/.repo/bicep/infra/main.bicep index c2966cb83e2..0790342f634 100644 --- a/templates/todo/projects/csharp-sql/.repo/bicep/infra/main.bicep +++ b/templates/todo/projects/csharp-sql/.repo/bicep/infra/main.bicep @@ -24,13 +24,11 @@ param sqlServerName string = '' param sqlDatabaseName string = '' param webServiceName string = '' param apimServiceName string = '' +param connectionStringKey string = 'AZURE-SQL-CONNECTION-STRING' @description('Flag to use Azure API Management to mediate the calls between the Web frontend and the backend API') param useAPIM bool = false -@description('API Management SKU to use if APIM is enabled') -param apimSku string = 'Consumption' - @description('Id of the user or app to assign application roles') param principalId string = '' @@ -41,10 +39,13 @@ param sqlAdminPassword string @secure() @description('Application user password') param appUserPassword string - +param appUser string = 'appUser' +param sqlAdmin string = 'sqlAdmin' var abbrs = loadJsonContent('../../../../../../common/infra/bicep/abbreviations.json') var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) var tags = { 'azd-env-name': environmentName } +var webUri = 'https://${web.outputs.defaultHostname}' +var apiUri = 'https://${api.outputs.defaultHostname}' // Organize resources in a resource group resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { @@ -54,139 +55,248 @@ resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { } // The application frontend -module web '../../../../../common/infra/bicep/app/web-appservice.bicep' = { +module web 'br/public:avm/res/web/site:0.3.2' = { name: 'web' scope: rg params: { + kind: 'app' name: !empty(webServiceName) ? webServiceName : '${abbrs.webSitesAppService}web-${resourceToken}' + serverFarmResourceId: appServicePlan.outputs.resourceId + tags: union(tags, { 'azd-service-name': 'web' }) location: location - tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName - appServicePlanId: appServicePlan.outputs.id + appInsightResourceId: applicationInsights.outputs.resourceId + siteConfig: { + appCommandLine: 'pm2 serve /home/site/wwwroot --no-daemon --spa' + linuxFxVersion: 'node|20-lts' + alwaysOn: true + } } } // The application backend -module api '../../../../../common/infra/bicep/app/api-appservice-dotnet.bicep' = { +module api 'br/public:avm/res/web/site:0.2.0' = { name: 'api' scope: rg params: { + kind: 'app' name: !empty(apiServiceName) ? apiServiceName : '${abbrs.webSitesAppService}api-${resourceToken}' + serverFarmResourceId: appServicePlan.outputs.resourceId + tags: union(tags, { 'azd-service-name': 'api' }) location: location - tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName - appServicePlanId: appServicePlan.outputs.id - keyVaultName: keyVault.outputs.name - allowedOrigins: [ web.outputs.SERVICE_WEB_URI ] - appSettings: { - AZURE_SQL_CONNECTION_STRING_KEY: sqlServer.outputs.connectionStringKey + appInsightResourceId: applicationInsights.outputs.resourceId + managedIdentities: { + systemAssigned: true + } + siteConfig: { + cors: { + allowedOrigins: [ 'https://portal.azure.com', 'https://ms.portal.azure.com' , webUri ] + } + linuxFxVersion: 'dotnetcore|8.0' + alwaysOn: true + appCommandLine: '' + } + appSettingsKeyValuePairs: { + AZURE_SQL_CONNECTION_STRING_KEY: connectionStringKey + AZURE_KEY_VAULT_ENDPOINT: keyVault.outputs.uri + SCM_DO_BUILD_DURING_DEPLOYMENT: 'False' + ENABLE_ORYX_BUILD: 'True' } } } // Give the API access to KeyVault -module apiKeyVaultAccess '../../../../../../common/infra/bicep/core/security/keyvault-access.bicep' = { - name: 'api-keyvault-access' +module accessKeyVault 'br/public:avm/res/key-vault/vault:0.3.5' = { + name: 'accesskeyvault' scope: rg params: { - keyVaultName: keyVault.outputs.name - principalId: api.outputs.SERVICE_API_IDENTITY_PRINCIPAL_ID + name: keyVault.outputs.name + enableRbacAuthorization: false + accessPolicies: [ + { + objectId: principalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + { + objectId: api.outputs.systemAssignedMIPrincipalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + ] + secrets:{ + secureList: [ + { + name: 'sqlAdmin' + value: sqlAdminPassword + } + { + name: 'appUser' + value: appUserPassword + } + { + name: connectionStringKey + value: 'Server=${sqlService.outputs.name}${environment().suffixes.sqlServerHostname}; Database=${!empty(sqlDatabaseName) ? sqlDatabaseName : 'Todo'}; User=${appUser}; Password=${appUserPassword}' + } + ] + } } } // The application database -module sqlServer '../../../../../common/infra/bicep/app/sqlserver.bicep' = { - name: 'sql' +module sqlService 'br/public:avm/res/sql/server:0.2.0' = { + name: 'sqlservice' scope: rg params: { name: !empty(sqlServerName) ? sqlServerName : '${abbrs.sqlServers}${resourceToken}' - databaseName: sqlDatabaseName + administratorLogin: sqlAdmin + administratorLoginPassword: sqlAdminPassword location: location tags: tags - sqlAdminPassword: sqlAdminPassword + publicNetworkAccess: 'Enabled' + databases: [ + { + name: !empty(sqlDatabaseName) ? sqlDatabaseName : 'Todo' + } + ] + firewallRules:[ + { + name: 'Azure Services' + startIpAddress: '0.0.0.1' + endIpAddress: '255.255.255.254' + } + ] + } +} + +//Add appuser to database owner +module sqldeploymentscript '../../../../../common/infra/bicep/app/sql-deployment-script.bicep' = { + name: 'sqldeploymentscript' + scope: rg + params: { + location: location appUserPassword: appUserPassword - keyVaultName: keyVault.outputs.name + sqlAdminPassword: sqlAdminPassword + sqlDatabaseName: !empty(sqlDatabaseName) ? sqlDatabaseName : 'Todo' + sqlServiceName: sqlService.outputs.name } } // Create an App Service Plan to group applications under the same payment plan and SKU -module appServicePlan '../../../../../../common/infra/bicep/core/host/appserviceplan.bicep' = { +module appServicePlan 'br/public:avm/res/web/serverfarm:0.1.0' = { name: 'appserviceplan' scope: rg params: { name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}' - location: location - tags: tags sku: { name: 'B3' + tier: 'Basic' } + location: location + tags: tags + reserved: true + kind: 'Linux' } } -// Store secrets in a keyvault -module keyVault '../../../../../../common/infra/bicep/core/security/keyvault.bicep' = { +// Create a keyvault to store secrets +module keyVault 'br/public:avm/res/key-vault/vault:0.3.5' = { name: 'keyvault' scope: rg params: { name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' location: location tags: tags - principalId: principalId + enableRbacAuthorization: false } } -// Monitor application with Azure Monitor -module monitoring '../../../../../../common/infra/bicep/core/monitor/monitoring.bicep' = { - name: 'monitoring' +// Monitor application with Azure loganalytics +module loganalytics 'br/public:avm/res/operational-insights/workspace:0.3.4' = { + name: 'loganalytics' scope: rg params: { + name: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' location: location - tags: tags - logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' - applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' - applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + } +} + +// Monitor application with Azure applicationInsights +module applicationInsights 'br/public:avm/res/insights/component:0.3.0' = { + name: 'applicationInsights' + scope: rg + params: { + name: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + workspaceResourceId: loganalytics.outputs.resourceId + location: location + } +} + +// Monitor application with Azure applicationInsightsDashboard +module applicationInsightsDashboard '../../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep' = { + name: 'application-insights-dashboard' + scope: rg + params: { + name: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + location: location + applicationInsightsName: applicationInsights.outputs.name } } // Creates Azure API Management (APIM) service to mediate the requests between the frontend and the backend API -module apim '../../../../../../common/infra/bicep/core/gateway/apim.bicep' = if (useAPIM) { +module apim 'br/public:avm/res/api-management/service:0.1.3' = if (useAPIM) { name: 'apim-deployment' scope: rg params: { name: !empty(apimServiceName) ? apimServiceName : '${abbrs.apiManagementService}${resourceToken}' - sku: apimSku + publisherEmail: 'noreply@microsoft.com' + publisherName: 'n/a' location: location tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName + apis: [ + { + name: 'todo-api' + path: 'todo' + displayName: 'Simple Todo API' + apiDescription: 'This is a simple Todo API' + serviceUrl: apiUri + subscriptionRequired: false + value: loadTextContent('../../../../../api/common/openapi.yaml') + policies: [ + { + value: replace(loadTextContent('../../../../../../common/infra/shared/gateway/apim/apim-api-policy.xml'), '{origin}', webUri) + format: 'rawxml' + } + ] + } + ] } } // Configures the API in the Azure API Management (APIM) service -module apimApi '../../../../../common/infra/bicep/app/apim-api.bicep' = if (useAPIM) { - name: 'apim-api-deployment' +module apimsettings '../../../../../common/infra/bicep/app/apim-api-settings.bicep' = if (useAPIM) { + name: 'apim-api-settings' scope: rg params: { - name: useAPIM ? apim.outputs.apimServiceName : '' + apiAppName: api.outputs.name apiName: 'todo-api' - apiDisplayName: 'Simple Todo API' - apiDescription: 'This is a simple Todo API' + name: useAPIM ? apim.outputs.name : '' apiPath: 'todo' - webFrontendUrl: web.outputs.SERVICE_WEB_URI - apiBackendUrl: api.outputs.SERVICE_API_URI - apiAppName: api.outputs.SERVICE_API_NAME + applicationInsightsName: applicationInsights.outputs.name } } // Data outputs -output AZURE_SQL_CONNECTION_STRING_KEY string = sqlServer.outputs.connectionStringKey +output AZURE_SQL_CONNECTION_STRING_KEY string = connectionStringKey // App outputs -output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString -output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint +output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsights.outputs.connectionString +output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.uri output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId -output API_BASE_URL string = useAPIM ? apimApi.outputs.SERVICE_API_URI : api.outputs.SERVICE_API_URI -output REACT_APP_WEB_BASE_URL string = web.outputs.SERVICE_WEB_URI +output API_BASE_URL string = useAPIM ? 'https://${apim.outputs.name}.azure-api.net/todo' : apiUri +output REACT_APP_WEB_BASE_URL string = webUri output USE_APIM bool = useAPIM -output SERVICE_API_ENDPOINTS array = useAPIM ? [ apimApi.outputs.SERVICE_API_URI, api.outputs.SERVICE_API_URI ]: [] +output SERVICE_API_ENDPOINTS array = useAPIM ? [ 'https://${apim.outputs.name}.azure-api.net/todo', apiUri ]: [] diff --git a/templates/todo/projects/csharp-sql/.repo/bicep/repo.yaml b/templates/todo/projects/csharp-sql/.repo/bicep/repo.yaml index cac0ccde64a..49bcad057b8 100644 --- a/templates/todo/projects/csharp-sql/.repo/bicep/repo.yaml +++ b/templates/todo/projects/csharp-sql/.repo/bicep/repo.yaml @@ -80,6 +80,16 @@ repo: to: ../../src/api/wwwroot/openapi.yaml patterns: - "apim-api.bicep" + + - from: ../../../api/common/openapi.yaml + to: ../src/api/wwwroot/openapi.yaml + patterns: + - "**/main.bicep" + + - from: ../../../common/infra/shared/gateway/apim/apim-api-policy.xml + to: ./app/apim-api-policy.xml + patterns: + - "**/main.bicep" assets: # Common assets @@ -106,6 +116,15 @@ repo: - from: ../../../../common/infra/bicep/app/sqlserver.bicep to: ./infra/app/db.bicep + - from: ../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep + to: ./infra/app/applicationinsights-dashboard.bicep + + - from: ../../../../common/infra/bicep/app/apim-api-settings.bicep + to: ./infra/app/apim-api-settings.bicep + + - from: ../../../../common/infra/bicep/app/sql-deployment-script.bicep + to: ./infra/app/sql-deployment-script.bicep + - from: ./../../ to: ./ ignore: diff --git a/templates/todo/projects/java-mongo-aca/.repo/bicep/infra/main.bicep b/templates/todo/projects/java-mongo-aca/.repo/bicep/infra/main.bicep index d857a3eee17..29b8d8c2016 100644 --- a/templates/todo/projects/java-mongo-aca/.repo/bicep/infra/main.bicep +++ b/templates/todo/projects/java-mongo-aca/.repo/bicep/infra/main.bicep @@ -25,8 +25,41 @@ param logAnalyticsName string = '' param resourceGroupName string = '' param webContainerAppName string = '' param apimServiceName string = '' -param apiAppExists bool = false -param webAppExists bool = false +param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' +param collections array = [ + { + name: 'TodoList' + id: 'TodoList' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } + { + name: 'TodoItem' + id: 'TodoItem' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } +] @description('Flag to use Azure API Management to mediate the calls between the Web frontend and the backend API') param useAPIM bool = false @@ -34,20 +67,17 @@ param useAPIM bool = false @description('Hostname suffix for container registry. Set when deploying to sovereign clouds') param containerRegistryHostSuffix string = 'azurecr.io' -@description('API Management SKU to use if APIM is enabled') -param apimSku string = 'Consumption' - @description('Id of the user or app to assign application roles') param principalId string = '' -@description('The base URL used by the web service for sending API requests') -param webApiBaseUrl string = '' - var abbrs = loadJsonContent('../../../../../../common/infra/bicep/abbreviations.json') var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) var tags = { 'azd-env-name': environmentName } var apiContainerAppNameOrDefault = '${abbrs.appContainerApps}web-${resourceToken}' -var corsAcaUrl = 'https://${apiContainerAppNameOrDefault}.${containerApps.outputs.defaultDomain}' +var corsAcaUrl = 'https://${apiContainerAppNameOrDefault}.${containerAppsEnvironment.outputs.defaultDomain}' +var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') +var webUri = 'https://${web.outputs.fqdn}' +var apiUri = 'https://${api.outputs.fqdn}' // Organize resources in a resource group resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { @@ -56,147 +86,299 @@ resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { tags: tags } -// Container apps host (including container registry) -module containerApps '../../../../../../common/infra/bicep/core/host/container-apps.bicep' = { - name: 'container-apps' +// Container registry +module containerRegistry 'br/public:avm/res/container-registry/registry:0.1.1' = { + name: 'registry' + scope: rg + params: { + name: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}' + location: location + acrAdminUserEnabled: true + tags: tags + publicNetworkAccess: 'Enabled' + roleAssignments:[ + { + principalId: webIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: acrPullRole + } + { + principalId: apiIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: acrPullRole + } + ] + } +} + +//Container apps environment +module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.4.5' = { + name: 'containerAppsEnvironment' scope: rg params: { - name: 'app' + logAnalyticsWorkspaceResourceId: loganalytics.outputs.resourceId + name: !empty(containerAppsEnvironmentName) ? containerAppsEnvironmentName : '${abbrs.appManagedEnvironments}${resourceToken}' location: location + zoneRedundant: false tags: tags - containerAppsEnvironmentName: !empty(containerAppsEnvironmentName) ? containerAppsEnvironmentName : '${abbrs.appManagedEnvironments}${resourceToken}' - containerRegistryName: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}' - // Work around Azure/azure-dev#3157 (the root cause of which is Azure/acr#723) by explicitly enabling the admin user to allow users which - // don't have the `Owner` role granted (and instead are classic administrators) to access the registry to push even if AAD authentication fails. - // - // This addresses the following error during deploy: - // - // failed getting ACR token: POST https://.azurecr.io/oauth2/exchange 401 Unauthorized - containerRegistryAdminUserEnabled: true - logAnalyticsWorkspaceName: monitoring.outputs.logAnalyticsWorkspaceName - applicationInsightsName: monitoring.outputs.applicationInsightsName + } +} + +//the managed identity for web frontend +module webIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { + name: 'webIdentity' + scope: rg + params: { + name: '${abbrs.managedIdentityUserAssignedIdentities}web-${resourceToken}' + location: location } } // Web frontend -module web '../../../../../common/infra/bicep/app/web-container-app.bicep' = { +module web 'br/public:avm/res/app/container-app:0.2.0' = { name: 'web' scope: rg params: { name: !empty(webContainerAppName) ? webContainerAppName : '${abbrs.appContainerApps}web-${resourceToken}' + containers: [ + { + image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' + name: 'simple-hello-world-container' + resources: { + cpu: json('0.5') + memory: '1.0Gi' + } + } + ] + managedIdentities:{ + systemAssigned: false + userAssignedResourceIds: [webIdentity.outputs.resourceId] + } + registries:[ + { + server: '${containerRegistry.outputs.name}.${containerRegistryHostSuffix}' + identity: webIdentity.outputs.resourceId + } + ] + dapr: { + enabled: true + appId: 'main' + appProtocol: 'http' + appPort: 80 + } + environmentId: containerAppsEnvironment.outputs.resourceId + location: location + tags: union(tags, { 'azd-service-name': 'web' }) + } +} + +//the managed identity for api backend +module apiIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { + name: 'apiIdentity' + scope: rg + params: { + name: '${abbrs.managedIdentityUserAssignedIdentities}api-${resourceToken}' location: location - tags: tags - identityName: '${abbrs.managedIdentityUserAssignedIdentities}web-${resourceToken}' - containerAppsEnvironmentName: containerApps.outputs.environmentName - containerRegistryName: containerApps.outputs.registryName - containerRegistryHostSuffix: containerRegistryHostSuffix - exists: webAppExists } } // Api backend -module api '../../../../../common/infra/bicep/app/api-container-app.bicep' = { +module api 'br/public:avm/res/app/container-app:0.2.0' = { name: 'api' scope: rg params: { name: !empty(apiContainerAppName) ? apiContainerAppName : '${abbrs.appContainerApps}api-${resourceToken}' + containers: [ + { + image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' + name: 'simple-hello-world-container' + resources: { + cpu: json('1.0') + memory: '2.0Gi' + } + env: [ + { + name: 'AZURE_CLIENT_ID' + value: apiIdentity.outputs.clientId + } + { + name: 'AZURE_KEY_VAULT_ENDPOINT' + value: keyVault.outputs.uri + } + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: applicationInsights.outputs.connectionString + } + { + name: 'API_ALLOW_ORIGINS' + value: corsAcaUrl + } + ] + } + ] + managedIdentities:{ + systemAssigned: false + userAssignedResourceIds: [apiIdentity.outputs.resourceId] + } + registries:[ + { + server: '${containerRegistry.outputs.name}.${containerRegistryHostSuffix}' + identity: apiIdentity.outputs.resourceId + } + ] + environmentId: containerAppsEnvironment.outputs.resourceId + ingressTargetPort: 3100 location: location - tags: tags - identityName: '${abbrs.managedIdentityUserAssignedIdentities}api-${resourceToken}' - applicationInsightsName: monitoring.outputs.applicationInsightsName - containerAppsEnvironmentName: containerApps.outputs.environmentName - containerRegistryName: containerApps.outputs.registryName - containerRegistryHostSuffix: containerRegistryHostSuffix - keyVaultName: keyVault.outputs.name - corsAcaUrl: corsAcaUrl - exists: apiAppExists + tags: union(tags, { 'azd-service-name': 'api' }) } } // The application database -module cosmos '../../../../../common/infra/bicep/app/cosmos-mongo-db.bicep' = { +module cosmos 'br/public:avm/res/document-db/database-account:0.4.0' = { name: 'cosmos' scope: rg params: { - accountName: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' - databaseName: cosmosDatabaseName + locations: [ + { + failoverPriority: 0 + isZoneRedundant: false + locationName: location + } + ] + name: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' location: location - tags: tags - keyVaultName: keyVault.outputs.name + mongodbDatabases: [ + { + name: 'Todo' + tags: tags + collections: collections + } + ] + secretsKeyVault: { + keyVaultName: keyVault.outputs.name + primaryWriteConnectionStringSecretName: connectionStringKey + } } } -// Store secrets in a keyvault -module keyVault '../../../../../../common/infra/bicep/core/security/keyvault.bicep' = { +// Create a keyvault to store secrets +module keyVault 'br/public:avm/res/key-vault/vault:0.5.1' = { name: 'keyvault' scope: rg params: { name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' location: location tags: tags - principalId: principalId + enableRbacAuthorization: false + accessPolicies: [ + { + objectId: principalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + { + objectId: apiIdentity.outputs.principalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + ] } } -// Monitor application with Azure Monitor -module monitoring '../../../../../../common/infra/bicep/core/monitor/monitoring.bicep' = { - name: 'monitoring' +// Monitor application with Azure loganalytics +module loganalytics 'br/public:avm/res/operational-insights/workspace:0.3.4' = { + name: 'loganalytics' scope: rg params: { + name: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' location: location - tags: tags - logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' - applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' - applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + } +} + +// Monitor application with Azure applicationInsights +module applicationInsights 'br/public:avm/res/insights/component:0.3.0' = { + name: 'applicationInsights' + scope: rg + params: { + name: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + workspaceResourceId: loganalytics.outputs.resourceId + location: location + } +} + +//Monitor application with Azure applicationInsightsDashboard +module applicationInsightsDashboard '../../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep' = { + name: 'application-insights-dashboard' + scope: rg + params: { + name: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + location: location + applicationInsightsName: applicationInsights.outputs.name } } // Creates Azure API Management (APIM) service to mediate the requests between the frontend and the backend API -module apim '../../../../../../common/infra/bicep/core/gateway/apim.bicep' = if (useAPIM) { +module apim 'br/public:avm/res/api-management/service:0.1.3' = if (useAPIM) { name: 'apim-deployment' scope: rg params: { name: !empty(apimServiceName) ? apimServiceName : '${abbrs.apiManagementService}${resourceToken}' - sku: apimSku + publisherEmail: 'noreply@microsoft.com' + publisherName: 'n/a' location: location tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName + apis: [ + { + name: 'todo-api' + path: 'todo' + displayName: 'Simple Todo API' + apiDescription: 'This is a simple Todo API' + serviceUrl: apiUri + subscriptionRequired: false + value: loadTextContent('../../../../../api/common/openapi.yaml') + policies: [ + { + value: replace(loadTextContent('../../../../../../common/infra/shared/gateway/apim/apim-api-policy.xml'), '{origin}', webUri) + format: 'rawxml' + } + ] + } + ] } } // Configures the API in the Azure API Management (APIM) service -module apimApi '../../../../../common/infra/bicep/app/apim-api.bicep' = if (useAPIM) { - name: 'apim-api-deployment' +module apimsettings '../../../../../common/infra/bicep/app/apim-api-settings.bicep' = if (useAPIM) { + name: 'apim-api-settings' scope: rg params: { - name: useAPIM ? apim.outputs.apimServiceName : '' apiName: 'todo-api' - apiDisplayName: 'Simple Todo API' - apiDescription: 'This is a simple Todo API' + name: useAPIM ? apim.outputs.name : '' apiPath: 'todo' - webFrontendUrl: web.outputs.SERVICE_WEB_URI - apiBackendUrl: api.outputs.SERVICE_API_URI + applicationInsightsName: applicationInsights.outputs.name } } // Data outputs -output AZURE_COSMOS_CONNECTION_STRING_KEY string = cosmos.outputs.connectionStringKey -output AZURE_COSMOS_DATABASE_NAME string = cosmos.outputs.databaseName +output AZURE_COSMOS_CONNECTION_STRING_KEY string = connectionStringKey +output AZURE_COSMOS_DATABASE_NAME string = !empty(cosmosDatabaseName) ? cosmosDatabaseName: 'Todo' // App outputs output API_CORS_ACA_URL string = corsAcaUrl -output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString -output APPLICATIONINSIGHTS_NAME string = monitoring.outputs.applicationInsightsName -output AZURE_CONTAINER_ENVIRONMENT_NAME string = containerApps.outputs.environmentName -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerApps.outputs.registryLoginServer -output AZURE_CONTAINER_REGISTRY_NAME string = containerApps.outputs.registryName -output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint +output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsights.outputs.connectionString +output APPLICATIONINSIGHTS_NAME string = applicationInsights.outputs.name +output AZURE_CONTAINER_ENVIRONMENT_NAME string = containerAppsEnvironment.outputs.name +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer +output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.outputs.name +output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.uri output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId -output API_BASE_URL string = useAPIM ? apimApi.outputs.SERVICE_API_URI : api.outputs.SERVICE_API_URI -output REACT_APP_WEB_BASE_URL string = web.outputs.SERVICE_WEB_URI -output SERVICE_API_NAME string = api.outputs.SERVICE_API_NAME -output SERVICE_WEB_NAME string = web.outputs.SERVICE_WEB_NAME +output API_BASE_URL string = useAPIM ? 'https://${apim.outputs.name}.azure-api.net/todo' : apiUri +output REACT_APP_WEB_BASE_URL string = webUri +output SERVICE_API_NAME string = api.outputs.name +output SERVICE_WEB_NAME string = web.outputs.name output USE_APIM bool = useAPIM -output SERVICE_API_ENDPOINTS array = useAPIM ? [ apimApi.outputs.SERVICE_API_URI, api.outputs.SERVICE_API_URI ] : [] +output SERVICE_API_ENDPOINTS array = useAPIM ? [ 'https://${apim.outputs.name}.azure-api.net/todo', apiUri ] : [] diff --git a/templates/todo/projects/java-mongo-aca/.repo/bicep/repo.yaml b/templates/todo/projects/java-mongo-aca/.repo/bicep/repo.yaml index 94c70dadf1c..6a352cc1978 100644 --- a/templates/todo/projects/java-mongo-aca/.repo/bicep/repo.yaml +++ b/templates/todo/projects/java-mongo-aca/.repo/bicep/repo.yaml @@ -87,6 +87,16 @@ repo: patterns: - "apim-api.bicep" + - from: ../../../api/common/openapi.yaml + to: ../src/api/openapi.yaml + patterns: + - "**/main.bicep" + + - from: ../../../common/infra/shared/gateway/apim/apim-api-policy.xml + to: ./app/apim-api-policy.xml + patterns: + - "**/main.bicep" + assets: # Common assets @@ -109,6 +119,12 @@ repo: - from: ../../../../common/infra/bicep/app/cosmos-mongo-db.bicep to: ./infra/app/db.bicep + - from: ../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep + to: ./infra/app/applicationinsights-dashboard.bicep + + - from: ../../../../common/infra/bicep/app/apim-api-settings.bicep + to: ./infra/app/apim-api-settings.bicep + - from: ./../../ to: ./ ignore: diff --git a/templates/todo/projects/java-mongo/.repo/bicep/infra/main.bicep b/templates/todo/projects/java-mongo/.repo/bicep/infra/main.bicep index 66f648cce5e..2a9244e5063 100644 --- a/templates/todo/projects/java-mongo/.repo/bicep/infra/main.bicep +++ b/templates/todo/projects/java-mongo/.repo/bicep/infra/main.bicep @@ -24,19 +24,53 @@ param logAnalyticsName string = '' param resourceGroupName string = '' param webServiceName string = '' param apimServiceName string = '' +param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' +param collections array = [ + { + name: 'TodoList' + id: 'TodoList' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } + { + name: 'TodoItem' + id: 'TodoItem' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } +] @description('Flag to use Azure API Management to mediate the calls between the Web frontend and the backend API') param useAPIM bool = false -@description('API Management SKU to use if APIM is enabled') -param apimSku string = 'Consumption' - @description('Id of the user or app to assign application roles') param principalId string = '' var abbrs = loadJsonContent('../../../../../../common/infra/bicep/abbreviations.json') var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) var tags = { 'azd-env-name': environmentName } +var webUri = 'https://${web.outputs.defaultHostname}' +var apiUri = 'https://${api.outputs.defaultHostname}' // Organize resources in a resource group resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { @@ -46,141 +80,230 @@ resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { } // The application frontend -module web '../../../../../common/infra/bicep/app/web-appservice.bicep' = { +module web 'br/public:avm/res/web/site:0.2.0' = { name: 'web' scope: rg params: { + kind: 'app' name: !empty(webServiceName) ? webServiceName : '${abbrs.webSitesAppService}web-${resourceToken}' + serverFarmResourceId: appServicePlan.outputs.resourceId + tags: union(tags, { 'azd-service-name': 'web' }) location: location - tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName - appServicePlanId: appServicePlan.outputs.id + appInsightResourceId: applicationInsights.outputs.resourceId + siteConfig: { + appCommandLine: 'pm2 serve /home/site/wwwroot --no-daemon --spa' + linuxFxVersion: 'node|20-lts' + alwaysOn: true + } } } // The application backend -module api '../../../../../common/infra/bicep/app/api-appservice-java.bicep' = { +module api 'br/public:avm/res/web/site:0.2.0' = { name: 'api' scope: rg params: { + kind: 'app' name: !empty(apiServiceName) ? apiServiceName : '${abbrs.webSitesAppService}api-${resourceToken}' + serverFarmResourceId: appServicePlan.outputs.resourceId + tags: union(tags, { 'azd-service-name': 'api' }) location: location - tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName - appServicePlanId: appServicePlan.outputs.id - keyVaultName: keyVault.outputs.name - allowedOrigins: [ web.outputs.SERVICE_WEB_URI ] - appSettings: { - AZURE_COSMOS_CONNECTION_STRING_KEY: cosmos.outputs.connectionStringKey - AZURE_COSMOS_DATABASE_NAME: cosmos.outputs.databaseName - AZURE_COSMOS_ENDPOINT: cosmos.outputs.endpoint - API_ALLOW_ORIGINS: web.outputs.SERVICE_WEB_URI + appInsightResourceId: applicationInsights.outputs.resourceId + managedIdentities: { + systemAssigned: true + } + siteConfig: { + cors: { + allowedOrigins: [ 'https://portal.azure.com', 'https://ms.portal.azure.com' , webUri ] + } + linuxFxVersion: 'java|17-java17' + alwaysOn: true + appCommandLine: '' + } + appSettingsKeyValuePairs: { + AZURE_KEY_VAULT_ENDPOINT: keyVault.outputs.uri + AZURE_COSMOS_CONNECTION_STRING_KEY: connectionStringKey + AZURE_COSMOS_DATABASE_NAME: !empty(cosmosDatabaseName) ? cosmosDatabaseName: 'Todo' + AZURE_COSMOS_ENDPOINT: 'https://${cosmos.outputs.name}.mongo.cosmos.azure.com:443/' + API_ALLOW_ORIGINS: webUri + SCM_DO_BUILD_DURING_DEPLOYMENT: 'True' + ENABLE_ORYX_BUILD: 'True' + JAVA_OPTS: join( + concat( + [], + ['-Djdk.attach.allowAttachSelf=true']), + ' ') } } } // Give the API access to KeyVault -module apiKeyVaultAccess '../../../../../../common/infra/bicep/core/security/keyvault-access.bicep' = { - name: 'api-keyvault-access' +module accesskeyvault 'br/public:avm/res/key-vault/vault:0.3.5' = { + name: 'accesskeyvault' scope: rg params: { - keyVaultName: keyVault.outputs.name - principalId: api.outputs.SERVICE_API_IDENTITY_PRINCIPAL_ID + name: keyVault.outputs.name + enableRbacAuthorization: false + accessPolicies: [ + { + objectId: principalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + { + objectId: api.outputs.systemAssignedMIPrincipalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + ] } } // The application database -module cosmos '../../../../../common/infra/bicep/app/cosmos-mongo-db.bicep' = { +module cosmos 'br/public:avm/res/document-db/database-account:0.4.0' = { name: 'cosmos' scope: rg params: { - accountName: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' - databaseName: cosmosDatabaseName + locations: [ + { + failoverPriority: 0 + isZoneRedundant: false + locationName: location + } + ] + name: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' location: location - tags: tags - keyVaultName: keyVault.outputs.name + mongodbDatabases: [ + { + name: 'Todo' + tags: tags + collections: collections + } + ] + secretsKeyVault: { + keyVaultName: keyVault.outputs.name + primaryWriteConnectionStringSecretName: connectionStringKey + } } } // Create an App Service Plan to group applications under the same payment plan and SKU -module appServicePlan '../../../../../../common/infra/bicep/core/host/appserviceplan.bicep' = { +module appServicePlan 'br/public:avm/res/web/serverfarm:0.1.0' = { name: 'appserviceplan' scope: rg params: { name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}' - location: location - tags: tags sku: { name: 'B3' + tier: 'Basic' } + location: location + tags: tags + reserved: true + kind: 'Linux' } } -// Store secrets in a keyvault -module keyVault '../../../../../../common/infra/bicep/core/security/keyvault.bicep' = { +// Create a keyvault to store secrets +module keyVault 'br/public:avm/res/key-vault/vault:0.3.5' = { name: 'keyvault' scope: rg params: { name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' location: location tags: tags - principalId: principalId + enableRbacAuthorization: false } } -// Monitor application with Azure Monitor -module monitoring '../../../../../../common/infra/bicep/core/monitor/monitoring.bicep' = { - name: 'monitoring' +// Monitor application with Azure loganalytics +module loganalytics 'br/public:avm/res/operational-insights/workspace:0.3.4' = { + name: 'loganalytics' scope: rg params: { + name: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' location: location - tags: tags - logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' - applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' - applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + } +} + +// Monitor application with Azure applicationInsights +module applicationInsights 'br/public:avm/res/insights/component:0.3.0' = { + name: 'applicationInsights' + scope: rg + params: { + name: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + workspaceResourceId: loganalytics.outputs.resourceId + location: location + } +} + +// Monitor application with Azure applicationInsightsDashboard +module applicationInsightsDashboard '../../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep' = { + name: 'application-insights-dashboard' + scope: rg + params: { + name: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + location: location + applicationInsightsName: applicationInsights.outputs.name } } // Creates Azure API Management (APIM) service to mediate the requests between the frontend and the backend API -module apim '../../../../../../common/infra/bicep/core/gateway/apim.bicep' = if (useAPIM) { +module apim 'br/public:avm/res/api-management/service:0.1.3' = if (useAPIM) { name: 'apim-deployment' scope: rg params: { name: !empty(apimServiceName) ? apimServiceName : '${abbrs.apiManagementService}${resourceToken}' - sku: apimSku + publisherEmail: 'noreply@microsoft.com' + publisherName: 'n/a' location: location tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName + apis: [ + { + name: 'todo-api' + path: 'todo' + displayName: 'Simple Todo API' + apiDescription: 'This is a simple Todo API' + serviceUrl: apiUri + subscriptionRequired: false + value: loadTextContent('../../../../../api/common/openapi.yaml') + policies: [ + { + value: replace(loadTextContent('../../../../../../common/infra/shared/gateway/apim/apim-api-policy.xml'), '{origin}', webUri) + format: 'rawxml' + } + ] + } + ] } } // Configures the API in the Azure API Management (APIM) service -module apimApi '../../../../../common/infra/bicep/app/apim-api.bicep' = if (useAPIM) { - name: 'apim-api-deployment' +module apimsettings '../../../../../common/infra/bicep/app/apim-api-settings.bicep' = if (useAPIM) { + name: 'apim-api-settings' scope: rg params: { - name: useAPIM ? apim.outputs.apimServiceName : '' + apiAppName: api.outputs.name apiName: 'todo-api' - apiDisplayName: 'Simple Todo API' - apiDescription: 'This is a simple Todo API' + name: useAPIM ? apim.outputs.name : '' apiPath: 'todo' - webFrontendUrl: web.outputs.SERVICE_WEB_URI - apiBackendUrl: api.outputs.SERVICE_API_URI - apiAppName: api.outputs.SERVICE_API_NAME + applicationInsightsName: applicationInsights.outputs.name } } // Data outputs -output AZURE_COSMOS_CONNECTION_STRING_KEY string = cosmos.outputs.connectionStringKey -output AZURE_COSMOS_DATABASE_NAME string = cosmos.outputs.databaseName +output AZURE_COSMOS_CONNECTION_STRING_KEY string = connectionStringKey +output AZURE_COSMOS_DATABASE_NAME string = !empty(cosmosDatabaseName) ? cosmosDatabaseName: 'Todo' // App outputs -output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString -output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint +output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsights.outputs.connectionString +output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.uri output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId -output API_BASE_URL string = useAPIM ? apimApi.outputs.SERVICE_API_URI : api.outputs.SERVICE_API_URI -output REACT_APP_WEB_BASE_URL string = web.outputs.SERVICE_WEB_URI +output API_BASE_URL string = useAPIM ? 'https://${apim.outputs.name}.azure-api.net/todo' : apiUri +output REACT_APP_WEB_BASE_URL string = webUri output USE_APIM bool = useAPIM -output SERVICE_API_ENDPOINTS array = useAPIM ? [ apimApi.outputs.SERVICE_API_URI, api.outputs.SERVICE_API_URI ]: [] +output SERVICE_API_ENDPOINTS array = useAPIM ? [ 'https://${apim.outputs.name}.azure-api.net/todo', apiUri ]: [] diff --git a/templates/todo/projects/java-mongo/.repo/bicep/repo.yaml b/templates/todo/projects/java-mongo/.repo/bicep/repo.yaml index 5e72dc52794..94d486625cb 100644 --- a/templates/todo/projects/java-mongo/.repo/bicep/repo.yaml +++ b/templates/todo/projects/java-mongo/.repo/bicep/repo.yaml @@ -80,6 +80,16 @@ repo: patterns: - "apim-api.bicep" + - from: ../../../api/common/openapi.yaml + to: ../src/api/openapi.yaml + patterns: + - "**/main.bicep" + + - from: ../../../common/infra/shared/gateway/apim/apim-api-policy.xml + to: ./app/apim-api-policy.xml + patterns: + - "**/main.bicep" + assets: # Common assets @@ -102,6 +112,12 @@ repo: - from: ../../../../common/infra/bicep/app/cosmos-mongo-db.bicep to: ./infra/app/db.bicep + - from: ../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep + to: ./infra/app/applicationinsights-dashboard.bicep + + - from: ../../../../common/infra/bicep/app/apim-api-settings.bicep + to: ./infra/app/apim-api-settings.bicep + - from: ./../../ to: ./ ignore: diff --git a/templates/todo/projects/nodejs-mongo-aca/.repo/bicep/infra/main.bicep b/templates/todo/projects/nodejs-mongo-aca/.repo/bicep/infra/main.bicep index b2f2c491e8c..29b8d8c2016 100644 --- a/templates/todo/projects/nodejs-mongo-aca/.repo/bicep/infra/main.bicep +++ b/templates/todo/projects/nodejs-mongo-aca/.repo/bicep/infra/main.bicep @@ -25,29 +25,59 @@ param logAnalyticsName string = '' param resourceGroupName string = '' param webContainerAppName string = '' param apimServiceName string = '' -param apiAppExists bool = false -param webAppExists bool = false +param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' +param collections array = [ + { + name: 'TodoList' + id: 'TodoList' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } + { + name: 'TodoItem' + id: 'TodoItem' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } +] @description('Flag to use Azure API Management to mediate the calls between the Web frontend and the backend API') param useAPIM bool = false -@description('API Management SKU to use if APIM is enabled') -param apimSku string = 'Consumption' - @description('Hostname suffix for container registry. Set when deploying to sovereign clouds') param containerRegistryHostSuffix string = 'azurecr.io' @description('Id of the user or app to assign application roles') param principalId string = '' -@description('The base URL used by the web service for sending API requests') -param webApiBaseUrl string = '' - var abbrs = loadJsonContent('../../../../../../common/infra/bicep/abbreviations.json') var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) var tags = { 'azd-env-name': environmentName } var apiContainerAppNameOrDefault = '${abbrs.appContainerApps}web-${resourceToken}' -var corsAcaUrl = 'https://${apiContainerAppNameOrDefault}.${containerApps.outputs.defaultDomain}' +var corsAcaUrl = 'https://${apiContainerAppNameOrDefault}.${containerAppsEnvironment.outputs.defaultDomain}' +var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') +var webUri = 'https://${web.outputs.fqdn}' +var apiUri = 'https://${api.outputs.fqdn}' // Organize resources in a resource group resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { @@ -56,147 +86,299 @@ resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { tags: tags } -// Container apps host (including container registry) -module containerApps '../../../../../../common/infra/bicep/core/host/container-apps.bicep' = { - name: 'container-apps' +// Container registry +module containerRegistry 'br/public:avm/res/container-registry/registry:0.1.1' = { + name: 'registry' + scope: rg + params: { + name: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}' + location: location + acrAdminUserEnabled: true + tags: tags + publicNetworkAccess: 'Enabled' + roleAssignments:[ + { + principalId: webIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: acrPullRole + } + { + principalId: apiIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: acrPullRole + } + ] + } +} + +//Container apps environment +module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.4.5' = { + name: 'containerAppsEnvironment' scope: rg params: { - name: 'app' + logAnalyticsWorkspaceResourceId: loganalytics.outputs.resourceId + name: !empty(containerAppsEnvironmentName) ? containerAppsEnvironmentName : '${abbrs.appManagedEnvironments}${resourceToken}' location: location + zoneRedundant: false tags: tags - containerAppsEnvironmentName: !empty(containerAppsEnvironmentName) ? containerAppsEnvironmentName : '${abbrs.appManagedEnvironments}${resourceToken}' - containerRegistryName: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}' - // Work around Azure/azure-dev#3157 (the root cause of which is Azure/acr#723) by explicitly enabling the admin user to allow users which - // don't have the `Owner` role granted (and instead are classic administrators) to access the registry to push even if AAD authentication fails. - // - // This addresses the following error during deploy: - // - // failed getting ACR token: POST https://.azurecr.io/oauth2/exchange 401 Unauthorized - containerRegistryAdminUserEnabled: true - logAnalyticsWorkspaceName: monitoring.outputs.logAnalyticsWorkspaceName - applicationInsightsName: monitoring.outputs.applicationInsightsName + } +} + +//the managed identity for web frontend +module webIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { + name: 'webIdentity' + scope: rg + params: { + name: '${abbrs.managedIdentityUserAssignedIdentities}web-${resourceToken}' + location: location } } // Web frontend -module web '../../../../../common/infra/bicep/app/web-container-app.bicep' = { +module web 'br/public:avm/res/app/container-app:0.2.0' = { name: 'web' scope: rg params: { name: !empty(webContainerAppName) ? webContainerAppName : '${abbrs.appContainerApps}web-${resourceToken}' + containers: [ + { + image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' + name: 'simple-hello-world-container' + resources: { + cpu: json('0.5') + memory: '1.0Gi' + } + } + ] + managedIdentities:{ + systemAssigned: false + userAssignedResourceIds: [webIdentity.outputs.resourceId] + } + registries:[ + { + server: '${containerRegistry.outputs.name}.${containerRegistryHostSuffix}' + identity: webIdentity.outputs.resourceId + } + ] + dapr: { + enabled: true + appId: 'main' + appProtocol: 'http' + appPort: 80 + } + environmentId: containerAppsEnvironment.outputs.resourceId + location: location + tags: union(tags, { 'azd-service-name': 'web' }) + } +} + +//the managed identity for api backend +module apiIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { + name: 'apiIdentity' + scope: rg + params: { + name: '${abbrs.managedIdentityUserAssignedIdentities}api-${resourceToken}' location: location - tags: tags - identityName: '${abbrs.managedIdentityUserAssignedIdentities}web-${resourceToken}' - containerAppsEnvironmentName: containerApps.outputs.environmentName - containerRegistryName: containerApps.outputs.registryName - containerRegistryHostSuffix: containerRegistryHostSuffix - exists: webAppExists } } // Api backend -module api '../../../../../common/infra/bicep/app/api-container-app.bicep' = { +module api 'br/public:avm/res/app/container-app:0.2.0' = { name: 'api' scope: rg params: { name: !empty(apiContainerAppName) ? apiContainerAppName : '${abbrs.appContainerApps}api-${resourceToken}' + containers: [ + { + image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' + name: 'simple-hello-world-container' + resources: { + cpu: json('1.0') + memory: '2.0Gi' + } + env: [ + { + name: 'AZURE_CLIENT_ID' + value: apiIdentity.outputs.clientId + } + { + name: 'AZURE_KEY_VAULT_ENDPOINT' + value: keyVault.outputs.uri + } + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: applicationInsights.outputs.connectionString + } + { + name: 'API_ALLOW_ORIGINS' + value: corsAcaUrl + } + ] + } + ] + managedIdentities:{ + systemAssigned: false + userAssignedResourceIds: [apiIdentity.outputs.resourceId] + } + registries:[ + { + server: '${containerRegistry.outputs.name}.${containerRegistryHostSuffix}' + identity: apiIdentity.outputs.resourceId + } + ] + environmentId: containerAppsEnvironment.outputs.resourceId + ingressTargetPort: 3100 location: location - tags: tags - identityName: '${abbrs.managedIdentityUserAssignedIdentities}api-${resourceToken}' - applicationInsightsName: monitoring.outputs.applicationInsightsName - containerAppsEnvironmentName: containerApps.outputs.environmentName - containerRegistryName: containerApps.outputs.registryName - containerRegistryHostSuffix: containerRegistryHostSuffix - keyVaultName: keyVault.outputs.name - corsAcaUrl: corsAcaUrl - exists: apiAppExists + tags: union(tags, { 'azd-service-name': 'api' }) } } // The application database -module cosmos '../../../../../common/infra/bicep/app/cosmos-mongo-db.bicep' = { +module cosmos 'br/public:avm/res/document-db/database-account:0.4.0' = { name: 'cosmos' scope: rg params: { - accountName: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' - databaseName: cosmosDatabaseName + locations: [ + { + failoverPriority: 0 + isZoneRedundant: false + locationName: location + } + ] + name: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' location: location - tags: tags - keyVaultName: keyVault.outputs.name + mongodbDatabases: [ + { + name: 'Todo' + tags: tags + collections: collections + } + ] + secretsKeyVault: { + keyVaultName: keyVault.outputs.name + primaryWriteConnectionStringSecretName: connectionStringKey + } } } -// Store secrets in a keyvault -module keyVault '../../../../../../common/infra/bicep/core/security/keyvault.bicep' = { +// Create a keyvault to store secrets +module keyVault 'br/public:avm/res/key-vault/vault:0.5.1' = { name: 'keyvault' scope: rg params: { name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' location: location tags: tags - principalId: principalId + enableRbacAuthorization: false + accessPolicies: [ + { + objectId: principalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + { + objectId: apiIdentity.outputs.principalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + ] } } -// Monitor application with Azure Monitor -module monitoring '../../../../../../common/infra/bicep/core/monitor/monitoring.bicep' = { - name: 'monitoring' +// Monitor application with Azure loganalytics +module loganalytics 'br/public:avm/res/operational-insights/workspace:0.3.4' = { + name: 'loganalytics' scope: rg params: { + name: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' location: location - tags: tags - logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' - applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' - applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + } +} + +// Monitor application with Azure applicationInsights +module applicationInsights 'br/public:avm/res/insights/component:0.3.0' = { + name: 'applicationInsights' + scope: rg + params: { + name: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + workspaceResourceId: loganalytics.outputs.resourceId + location: location + } +} + +//Monitor application with Azure applicationInsightsDashboard +module applicationInsightsDashboard '../../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep' = { + name: 'application-insights-dashboard' + scope: rg + params: { + name: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + location: location + applicationInsightsName: applicationInsights.outputs.name } } // Creates Azure API Management (APIM) service to mediate the requests between the frontend and the backend API -module apim '../../../../../../common/infra/bicep/core/gateway/apim.bicep' = if (useAPIM) { +module apim 'br/public:avm/res/api-management/service:0.1.3' = if (useAPIM) { name: 'apim-deployment' scope: rg params: { name: !empty(apimServiceName) ? apimServiceName : '${abbrs.apiManagementService}${resourceToken}' - sku: apimSku + publisherEmail: 'noreply@microsoft.com' + publisherName: 'n/a' location: location tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName + apis: [ + { + name: 'todo-api' + path: 'todo' + displayName: 'Simple Todo API' + apiDescription: 'This is a simple Todo API' + serviceUrl: apiUri + subscriptionRequired: false + value: loadTextContent('../../../../../api/common/openapi.yaml') + policies: [ + { + value: replace(loadTextContent('../../../../../../common/infra/shared/gateway/apim/apim-api-policy.xml'), '{origin}', webUri) + format: 'rawxml' + } + ] + } + ] } } // Configures the API in the Azure API Management (APIM) service -module apimApi '../../../../../common/infra/bicep/app/apim-api.bicep' = if (useAPIM) { - name: 'apim-api-deployment' +module apimsettings '../../../../../common/infra/bicep/app/apim-api-settings.bicep' = if (useAPIM) { + name: 'apim-api-settings' scope: rg params: { - name: useAPIM ? apim.outputs.apimServiceName : '' apiName: 'todo-api' - apiDisplayName: 'Simple Todo API' - apiDescription: 'This is a simple Todo API' + name: useAPIM ? apim.outputs.name : '' apiPath: 'todo' - webFrontendUrl: web.outputs.SERVICE_WEB_URI - apiBackendUrl: api.outputs.SERVICE_API_URI + applicationInsightsName: applicationInsights.outputs.name } } // Data outputs -output AZURE_COSMOS_CONNECTION_STRING_KEY string = cosmos.outputs.connectionStringKey -output AZURE_COSMOS_DATABASE_NAME string = cosmos.outputs.databaseName +output AZURE_COSMOS_CONNECTION_STRING_KEY string = connectionStringKey +output AZURE_COSMOS_DATABASE_NAME string = !empty(cosmosDatabaseName) ? cosmosDatabaseName: 'Todo' // App outputs output API_CORS_ACA_URL string = corsAcaUrl -output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString -output APPLICATIONINSIGHTS_NAME string = monitoring.outputs.applicationInsightsName -output AZURE_CONTAINER_ENVIRONMENT_NAME string = containerApps.outputs.environmentName -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerApps.outputs.registryLoginServer -output AZURE_CONTAINER_REGISTRY_NAME string = containerApps.outputs.registryName -output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint +output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsights.outputs.connectionString +output APPLICATIONINSIGHTS_NAME string = applicationInsights.outputs.name +output AZURE_CONTAINER_ENVIRONMENT_NAME string = containerAppsEnvironment.outputs.name +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer +output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.outputs.name +output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.uri output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId -output API_BASE_URL string = useAPIM ? apimApi.outputs.SERVICE_API_URI : api.outputs.SERVICE_API_URI -output REACT_APP_WEB_BASE_URL string = web.outputs.SERVICE_WEB_URI -output SERVICE_API_NAME string = api.outputs.SERVICE_API_NAME -output SERVICE_WEB_NAME string = web.outputs.SERVICE_WEB_NAME +output API_BASE_URL string = useAPIM ? 'https://${apim.outputs.name}.azure-api.net/todo' : apiUri +output REACT_APP_WEB_BASE_URL string = webUri +output SERVICE_API_NAME string = api.outputs.name +output SERVICE_WEB_NAME string = web.outputs.name output USE_APIM bool = useAPIM -output SERVICE_API_ENDPOINTS array = useAPIM ? [ apimApi.outputs.SERVICE_API_URI, api.outputs.SERVICE_API_URI ]: [] +output SERVICE_API_ENDPOINTS array = useAPIM ? [ 'https://${apim.outputs.name}.azure-api.net/todo', apiUri ] : [] diff --git a/templates/todo/projects/nodejs-mongo-aca/.repo/bicep/repo.yaml b/templates/todo/projects/nodejs-mongo-aca/.repo/bicep/repo.yaml index bd5dfdff726..262af9dc33f 100644 --- a/templates/todo/projects/nodejs-mongo-aca/.repo/bicep/repo.yaml +++ b/templates/todo/projects/nodejs-mongo-aca/.repo/bicep/repo.yaml @@ -87,6 +87,16 @@ repo: patterns: - "apim-api.bicep" + - from: ../../../api/common/openapi.yaml + to: ../src/api/openapi.yaml + patterns: + - "**/main.bicep" + + - from: ../../../common/infra/shared/gateway/apim/apim-api-policy.xml + to: ./app/apim-api-policy.xml + patterns: + - "**/main.bicep" + assets: # # Common assets @@ -112,6 +122,12 @@ repo: - from: ../../../../common/infra/bicep/app/cosmos-mongo-db.bicep to: ./infra/app/db.bicep + - from: ../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep + to: ./infra/app/applicationinsights-dashboard.bicep + + - from: ../../../../common/infra/bicep/app/apim-api-settings.bicep + to: ./infra/app/apim-api-settings.bicep + - from: ./../../ to: ./ ignore: diff --git a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep index 4db6cbf4b4c..595e13e2e53 100644 --- a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep +++ b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep @@ -26,17 +26,102 @@ param cosmosDatabaseName string = '' param keyVaultName string = '' param logAnalyticsName string = '' param resourceGroupName string = '' +param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' +param collections array = [ + { + name: 'TodoList' + id: 'TodoList' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } + { + name: 'TodoItem' + id: 'TodoItem' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } +] @description('Id of the user or app to assign application roles') param principalId string = '' -@description('The type of principal to assign application roles') -@allowed(['Device', 'ForeignGroup', 'Group', 'ServicePrincipal', 'User']) -param principalType string = 'User' +@allowed([ + 'CostOptimised' + 'Standard' + 'HighSpec' + 'Custom' +]) +@description('The System Pool Preset sizing') +param systemPoolType string = 'CostOptimised' var abbrs = loadJsonContent('../../../../../../common/infra/bicep/abbreviations.json') var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) var tags = { 'azd-env-name': environmentName } +var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') +var aksClusterAdminRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b1ff04bb-8a4e-4dc4-8eb5-8693973ce19b') +var systemPoolSpec = nodePoolPresets[systemPoolType] +var nodePoolBase = { + osType: 'Linux' + maxPods: 30 + type: 'VirtualMachineScaleSets' + upgradeSettings: { + maxSurge: '33%' + } +} +var nodePoolPresets = { + CostOptimised: { + vmSize: 'Standard_B4ms' + count: 1 + minCount: 1 + maxCount: 3 + enableAutoScaling: true + availabilityZones: [] + } + Standard: { + vmSize: 'Standard_DS2_v2' + count: 3 + minCount: 3 + maxCount: 5 + enableAutoScaling: true + availabilityZones: [ + '1' + '2' + '3' + ] + } + HighSpec: { + vmSize: 'Standard_D4s_v3' + count: 3 + minCount: 3 + maxCount: 5 + enableAutoScaling: true + availabilityZones: [ + '1' + '2' + '3' + ] + } +} // Organize resources in a resource group resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { @@ -46,72 +131,215 @@ resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { } // The AKS cluster to host applications -module aks '../../../../../../common/infra/bicep/core/host/aks.bicep' = { - name: 'aks' +module managedCluster 'br/public:avm/res/container-service/managed-cluster:0.1.7' = { + name: 'managed-cluster' scope: rg params: { - location: location name: !empty(clusterName) ? clusterName : '${abbrs.containerServiceManagedClusters}${resourceToken}' - containerRegistryName: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}' - logAnalyticsName: monitoring.outputs.logAnalyticsWorkspaceName - keyVaultName: keyVault.outputs.name - principalId: principalId - principalType: principalType - // Set enableAzureRbac & disableLocalAccounts to use Azure AD for authentication and authorization - enableAzureRbac: false - disableLocalAccounts: false + primaryAgentPoolProfile: [ + union( + { name: 'npsystem', mode: 'System' }, + nodePoolBase, + systemPoolSpec + ) + ] + skuTier: 'Free' + kubernetesVersion: '1.27.7' + location: location + networkPlugin: 'azure' + networkPolicy: 'azure' + publicNetworkAccess: 'Enabled' + webApplicationRoutingEnabled: true + enableKeyvaultSecretsProvider: true + roleAssignments: [ + { + principalId: principalId + roleDefinitionIdOrName: aksClusterAdminRole + } + ] + monitoringWorkspaceId: loganalytics.outputs.resourceId + diagnosticSettings: [ + { + workspaceResourceId: loganalytics.outputs.resourceId + logCategoriesAndGroups: [ + { + category: 'cluster-autoscaler' + enabled: true + } + { + category: 'kube-controller-manager' + enabled: true + } + { + category: 'kube-audit-admin' + enabled: true + } + { + category: 'guard' + enabled: true + } + ] + metricCategories: [ + { + category: 'AllMetrics' + } + ] + } + ] + agentPools: [ + { + name: 'npuserpool' + mode: 'User' + osType: 'Linux' + maxPods: 30 + type: 'VirtualMachineScaleSets' + maxSurge: '33%' + vmSize: 'standard_a2' + } + ] + managedIdentities: { + systemAssigned: true + } + } +} + +//Azure Container Registries (ACR) +module containerRegistry 'br/public:avm/res/container-registry/registry:0.1.1' = { + name: 'container-registry' + scope: rg + params: { + name: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}' + acrSku: 'Basic' + publicNetworkAccess: 'Enabled' + location: location + diagnosticSettings: [ + { + workspaceResourceId: loganalytics.outputs.resourceId + logCategoriesAndGroups: [ + { + category: 'ContainerRegistryRepositoryEvents' + enabled: true + } + { + category: 'ContainerRegistryLoginEvents' + enabled: true + } + ] + metricCategories: [ + { + category: 'AllMetrics' + enabled: true + } + ] + } + ] + roleAssignments: [ + { + principalId: managedCluster.outputs.kubeletIdentityObjectId + roleDefinitionIdOrName: acrPullRole + principalType: 'ServicePrincipal' + } + ] } } // The application database -module cosmos '../../../../../common/infra/bicep/app/cosmos-mongo-db.bicep' = { +module cosmos 'br/public:avm/res/document-db/database-account:0.5.1' = { name: 'cosmos' scope: rg params: { - accountName: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' - databaseName: cosmosDatabaseName + locations: [ + { + failoverPriority: 0 + isZoneRedundant: false + locationName: location + } + ] + name: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' location: location - tags: tags - keyVaultName: keyVault.outputs.name + mongodbDatabases: [ + { + name: 'Todo' + tags: tags + collections: collections + } + ] + secretsKeyVault: { + keyVaultName: keyVault.outputs.name + primaryWriteConnectionStringSecretName: connectionStringKey + } } } -// Store secrets in a keyvault -module keyVault '../../../../../../common/infra/bicep/core/security/keyvault.bicep' = { +// Create a keyvault to store secrets +module keyVault 'br/public:avm/res/key-vault/vault:0.3.5' = { name: 'keyvault' scope: rg params: { name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' location: location tags: tags - principalId: principalId + enableRbacAuthorization: false + accessPolicies: [ + { + objectId: managedCluster.outputs.kubeletIdentityObjectId + permissions: { + secrets: [ 'get', 'list' ] + } + } + { + objectId: principalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + ] } } -// Monitor application with Azure Monitor -module monitoring '../../../../../../common/infra/bicep/core/monitor/monitoring.bicep' = { - name: 'monitoring' +// Monitor application with Azure loganalytics +module loganalytics 'br/public:avm/res/operational-insights/workspace:0.3.4' = { + name: 'loganalytics' scope: rg params: { + name: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' location: location - tags: tags - logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' - applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' - applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + } +} + +// Monitor application with Azure applicationInsights +module applicationInsights 'br/public:avm/res/insights/component:0.3.0' = { + name: 'applicationInsights' + scope: rg + params: { + name: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + workspaceResourceId: loganalytics.outputs.resourceId + location: location + } +} + +// Monitor application with Azure applicationInsightsDashboard +module applicationInsightsDashboard '../../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep' = { + name: 'application-insights-dashboard' + scope: rg + params: { + name: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + location: location + applicationInsightsName: applicationInsights.outputs.name } } // Data outputs -output AZURE_COSMOS_CONNECTION_STRING_KEY string = cosmos.outputs.connectionStringKey -output AZURE_COSMOS_DATABASE_NAME string = cosmos.outputs.databaseName +output AZURE_COSMOS_CONNECTION_STRING_KEY string = connectionStringKey +output AZURE_COSMOS_DATABASE_NAME string = !empty(cosmosDatabaseName) ? cosmosDatabaseName: 'Todo' // App outputs -output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString -output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint +output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsights.outputs.connectionString +output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.uri output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId -output AZURE_AKS_CLUSTER_NAME string = aks.outputs.clusterName -output AZURE_AKS_IDENTITY_CLIENT_ID string = aks.outputs.clusterIdentity.clientId -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = aks.outputs.containerRegistryLoginServer -output AZURE_CONTAINER_REGISTRY_NAME string = aks.outputs.containerRegistryName +output AZURE_AKS_CLUSTER_NAME string = managedCluster.outputs.name +output AZURE_AKS_IDENTITY_CLIENT_ID string = managedCluster.outputs.kubeletIdentityClientId +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer +output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.outputs.name diff --git a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml index 49375fc77d9..db69596b955 100644 --- a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml +++ b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml @@ -85,6 +85,9 @@ repo: - from: ../../../../common/infra/bicep/app/cosmos-mongo-db.bicep to: ./infra/app/db.bicep + - from: ../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep + to: ./infra/app/applicationinsights-dashboard.bicep + - from: ./../../ to: ./ ignore: diff --git a/templates/todo/projects/nodejs-mongo-swa-func/.repo/bicep/infra/main.bicep b/templates/todo/projects/nodejs-mongo-swa-func/.repo/bicep/infra/main.bicep index 80b97061c31..3b51ebff52b 100644 --- a/templates/todo/projects/nodejs-mongo-swa-func/.repo/bicep/infra/main.bicep +++ b/templates/todo/projects/nodejs-mongo-swa-func/.repo/bicep/infra/main.bicep @@ -25,19 +25,53 @@ param resourceGroupName string = '' param storageAccountName string = '' param webServiceName string = '' param apimServiceName string = '' +param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' +param collections array = [ + { + name: 'TodoList' + id: 'TodoList' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } + { + name: 'TodoItem' + id: 'TodoItem' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } +] @description('Flag to use Azure API Management to mediate the calls between the Web frontend and the backend API') param useAPIM bool = false -@description('API Management SKU to use if APIM is enabled') -param apimSku string = 'Consumption' - @description('Id of the user or app to assign application roles') param principalId string = '' var abbrs = loadJsonContent('../../../../../../common/infra/bicep/abbreviations.json') var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) var tags = { 'azd-env-name': environmentName } +var apiUri = 'https://${api.outputs.defaultHostname}' +var webUri = 'https://${web.outputs.defaultHostname}' // Organize resources in a resource group resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { @@ -47,151 +81,238 @@ resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { } // The application frontend -module web '../../../../../common/infra/bicep/app/web-staticwebapp.bicep' = { +module web 'br/public:avm/res/web/static-site:0.3.0' = { name: 'web' scope: rg params: { name: !empty(webServiceName) ? webServiceName : '${abbrs.webStaticSites}web-${resourceToken}' location: location - tags: tags + provider: 'Custom' + tags: union(tags, { 'azd-service-name': 'web' }) } } // The application backend -module api '../../../../../common/infra/bicep/app/api-functions-node.bicep' = { +module api 'br/public:avm/res/web/site:0.3.5' = { name: 'api' scope: rg params: { + kind: 'functionapp' name: !empty(apiServiceName) ? apiServiceName : '${abbrs.webSitesFunctions}api-${resourceToken}' + serverFarmResourceId: appServicePlan.outputs.resourceId + tags: union(tags, { 'azd-service-name': 'api' }) location: location - tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName - appServicePlanId: appServicePlan.outputs.id - keyVaultName: keyVault.outputs.name - storageAccountName: storage.outputs.name - allowedOrigins: [ web.outputs.SERVICE_WEB_URI ] - appSettings: { - AZURE_COSMOS_CONNECTION_STRING_KEY: cosmos.outputs.connectionStringKey - AZURE_COSMOS_DATABASE_NAME: cosmos.outputs.databaseName - AZURE_COSMOS_ENDPOINT: cosmos.outputs.endpoint - API_ALLOW_ORIGINS: web.outputs.SERVICE_WEB_URI - } + appInsightResourceId: applicationInsights.outputs.resourceId + managedIdentities: { + systemAssigned: true + } + clientAffinityEnabled: false + siteConfig: { + cors: { + allowedOrigins: [ 'https://portal.azure.com', 'https://ms.portal.azure.com' , webUri ] + } + linuxFxVersion: 'node|20' + use32BitWorkerProcess: false + } + appSettingsKeyValuePairs: { + API_ALLOW_ORIGINS: webUri + AZURE_COSMOS_CONNECTION_STRING_KEY: connectionStringKey + AZURE_COSMOS_DATABASE_NAME: !empty(cosmosDatabaseName) ? cosmosDatabaseName: 'Todo' + AZURE_KEY_VAULT_ENDPOINT:keyVault.outputs.uri + AZURE_COSMOS_ENDPOINT: 'https://${cosmos.outputs.name}.documents.azure.com:443/' + FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_WORKER_RUNTIME: 'node' + SCM_DO_BUILD_DURING_DEPLOYMENT: 'True' + ENABLE_ORYX_BUILD: 'True' + } + storageAccountResourceId: storage.outputs.resourceId } } // Give the API access to KeyVault -module apiKeyVaultAccess '../../../../../../common/infra/bicep/core/security/keyvault-access.bicep' = { - name: 'api-keyvault-access' +module accesskeyvault 'br/public:avm/res/key-vault/vault:0.5.1' = { + name: 'accesskeyvault' scope: rg params: { - keyVaultName: keyVault.outputs.name - principalId: api.outputs.SERVICE_API_IDENTITY_PRINCIPAL_ID + name: keyVault.outputs.name + enableRbacAuthorization: false + accessPolicies: [ + { + objectId: api.outputs.systemAssignedMIPrincipalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + { + objectId: principalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + ] } } // The application database -module cosmos '../../../../../common/infra/bicep/app/cosmos-mongo-db.bicep' = { +module cosmos 'br/public:avm/res/document-db/database-account:0.4.0' = { name: 'cosmos' scope: rg params: { - accountName: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' - databaseName: cosmosDatabaseName + locations: [ + { + failoverPriority: 0 + isZoneRedundant: false + locationName: location + } + ] + name: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' location: location - tags: tags - keyVaultName: keyVault.outputs.name + mongodbDatabases: [ + { + name: 'Todo' + tags: tags + collections: collections + } + ] + secretsKeyVault: { + keyVaultName: keyVault.outputs.name + primaryWriteConnectionStringSecretName: connectionStringKey + } } } // Create an App Service Plan to group applications under the same payment plan and SKU -module appServicePlan '../../../../../../common/infra/bicep/core/host/appserviceplan.bicep' = { +module appServicePlan 'br/public:avm/res/web/serverfarm:0.1.1' = { name: 'appserviceplan' scope: rg params: { name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}' - location: location - tags: tags sku: { name: 'Y1' tier: 'Dynamic' } + location: location + tags: tags + reserved: true + kind: 'Linux' } } // Backing storage for Azure functions backend API -module storage '../../../../../../common/infra/bicep/core/storage/storage-account.bicep' = { +module storage 'br/public:avm/res/storage/storage-account:0.8.3' = { name: 'storage' scope: rg params: { name: !empty(storageAccountName) ? storageAccountName : '${abbrs.storageStorageAccounts}${resourceToken}' + allowBlobPublicAccess: true + dnsEndpointType: 'Standard' + publicNetworkAccess:'Enabled' + networkAcls:{ + bypass: 'AzureServices' + defaultAction: 'Allow' + } location: location tags: tags } } -// Store secrets in a keyvault -module keyVault '../../../../../../common/infra/bicep/core/security/keyvault.bicep' = { +// Create a keyvault to store secrets +module keyVault 'br/public:avm/res/key-vault/vault:0.5.1' = { name: 'keyvault' scope: rg params: { name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' location: location tags: tags - principalId: principalId + enableRbacAuthorization: false } } -// Monitor application with Azure Monitor -module monitoring '../../../../../../common/infra/bicep/core/monitor/monitoring.bicep' = { - name: 'monitoring' +// Monitor application with Azure loganalytics +module loganalytics 'br/public:avm/res/operational-insights/workspace:0.3.4' = { + name: 'loganalytics' scope: rg params: { + name: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' location: location - tags: tags - logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' - applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' - applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + } +} + +// Monitor application with Azure applicationInsights +module applicationInsights 'br/public:avm/res/insights/component:0.3.0' = { + name: 'applicationInsights' + scope: rg + params: { + name: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + workspaceResourceId: loganalytics.outputs.resourceId + location: location + } +} + +// Monitor application with Azure applicationInsightsDashboard +module applicationInsightsDashboard '../../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep' = { + name: 'application-insights-dashboard' + scope: rg + params: { + name: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + location: location + applicationInsightsName: applicationInsights.outputs.name } } // Creates Azure API Management (APIM) service to mediate the requests between the frontend and the backend API -module apim '../../../../../../common/infra/bicep/core/gateway/apim.bicep' = if (useAPIM) { +module apim 'br/public:avm/res/api-management/service:0.1.3' = if (useAPIM) { name: 'apim-deployment' scope: rg params: { name: !empty(apimServiceName) ? apimServiceName : '${abbrs.apiManagementService}${resourceToken}' + publisherEmail: 'noreply@microsoft.com' + publisherName: 'n/a' location: location tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName + apis: [ + { + name: 'todo-api' + path: 'todo' + displayName: 'Simple Todo API' + apiDescription: 'This is a simple Todo API' + serviceUrl: apiUri + subscriptionRequired: false + value: loadTextContent('../../../../../api/common/openapi.yaml') + policies: [ + { + value: replace(loadTextContent('../../../../../../common/infra/shared/gateway/apim/apim-api-policy.xml'), '{origin}', webUri) + format: 'rawxml' + } + ] + } + ] } } // Configures the API in the Azure API Management (APIM) service -module apimApi '../../../../../common/infra/bicep/app/apim-api.bicep' = if (useAPIM) { - name: 'apim-api-deployment' +module apimsettings '../../../../../common/infra/bicep/app/apim-api-settings.bicep' = if (useAPIM) { + name: 'apim-api-settings' scope: rg params: { - name: useAPIM ? apim.outputs.apimServiceName : '' apiName: 'todo-api' - apiDisplayName: 'Simple Todo API' - apiDescription: 'This is a simple Todo API' + name: useAPIM ? apim.outputs.name : '' apiPath: 'todo' - webFrontendUrl: web.outputs.SERVICE_WEB_URI - apiBackendUrl: api.outputs.SERVICE_API_URI - apiAppName: api.outputs.SERVICE_API_NAME + applicationInsightsName: applicationInsights.outputs.name } } // Data outputs -output AZURE_COSMOS_CONNECTION_STRING_KEY string = cosmos.outputs.connectionStringKey -output AZURE_COSMOS_DATABASE_NAME string = cosmos.outputs.databaseName +output AZURE_COSMOS_CONNECTION_STRING_KEY string = connectionStringKey +output AZURE_COSMOS_DATABASE_NAME string = !empty(cosmosDatabaseName) ? cosmosDatabaseName: 'Todo' // App outputs -output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString -output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint +output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsights.outputs.connectionString +output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.uri output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId -output API_BASE_URL string = useAPIM ? apimApi.outputs.SERVICE_API_URI : api.outputs.SERVICE_API_URI -output REACT_APP_WEB_BASE_URL string = web.outputs.SERVICE_WEB_URI +output API_BASE_URL string = useAPIM ? 'https://${apim.outputs.name}.azure-api.net/todo' : apiUri +output REACT_APP_WEB_BASE_URL string = webUri output USE_APIM bool = useAPIM -output SERVICE_API_ENDPOINTS array = useAPIM ? [ apimApi.outputs.SERVICE_API_URI, api.outputs.SERVICE_API_URI ]: [] +output SERVICE_API_ENDPOINTS array = useAPIM ? [ 'https://${apim.outputs.name}.azure-api.net/todo', apiUri ]: [] diff --git a/templates/todo/projects/nodejs-mongo-swa-func/.repo/bicep/repo.yaml b/templates/todo/projects/nodejs-mongo-swa-func/.repo/bicep/repo.yaml index 571d6a56f23..eddc307ff63 100644 --- a/templates/todo/projects/nodejs-mongo-swa-func/.repo/bicep/repo.yaml +++ b/templates/todo/projects/nodejs-mongo-swa-func/.repo/bicep/repo.yaml @@ -80,6 +80,16 @@ repo: patterns: - "apim-api.bicep" + - from: ../../../api/common/openapi.yaml + to: ../src/api/openapi.yaml + patterns: + - "**/main.bicep" + + - from: ../../../common/infra/shared/gateway/apim/apim-api-policy.xml + to: ./app/apim-api-policy.xml + patterns: + - "**/main.bicep" + assets: # # Common assets @@ -105,6 +115,12 @@ repo: - from: ../../../../common/infra/bicep/app/cosmos-mongo-db.bicep to: ./infra/app/db.bicep + - from: ../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep + to: ./infra/app/applicationinsights-dashboard.bicep + + - from: ../../../../common/infra/bicep/app/apim-api-settings.bicep + to: ./infra/app/apim-api-settings.bicep + - from: ./../../ to: ./ ignore: diff --git a/templates/todo/projects/nodejs-mongo/.repo/bicep/infra/main.bicep b/templates/todo/projects/nodejs-mongo/.repo/bicep/infra/main.bicep index c9c109ae87d..bc6e76b0f15 100644 --- a/templates/todo/projects/nodejs-mongo/.repo/bicep/infra/main.bicep +++ b/templates/todo/projects/nodejs-mongo/.repo/bicep/infra/main.bicep @@ -24,19 +24,53 @@ param logAnalyticsName string = '' param resourceGroupName string = '' param webServiceName string = '' param apimServiceName string = '' +param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' +param collections array = [ + { + name: 'TodoList' + id: 'TodoList' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } + { + name: 'TodoItem' + id: 'TodoItem' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } +] @description('Flag to use Azure API Management to mediate the calls between the Web frontend and the backend API') param useAPIM bool = false -@description('API Management SKU to use if APIM is enabled') -param apimSku string = 'Consumption' - @description('Id of the user or app to assign application roles') param principalId string = '' var abbrs = loadJsonContent('../../../../../../common/infra/bicep/abbreviations.json') var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) var tags = { 'azd-env-name': environmentName } +var apiUri = 'https://${api.outputs.defaultHostname}' +var webUri = 'https://${web.outputs.defaultHostname}' // Organize resources in a resource group resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { @@ -46,141 +80,225 @@ resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { } // The application frontend -module web '../../../../../common/infra/bicep/app/web-appservice.bicep' = { +module web 'br/public:avm/res/web/site:0.2.0' = { name: 'web' scope: rg params: { + kind: 'app' name: !empty(webServiceName) ? webServiceName : '${abbrs.webSitesAppService}web-${resourceToken}' + serverFarmResourceId: appServicePlan.outputs.resourceId + tags: union(tags, { 'azd-service-name': 'web' }) location: location - tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName - appServicePlanId: appServicePlan.outputs.id + appInsightResourceId: applicationInsights.outputs.resourceId + siteConfig: { + linuxFxVersion: 'node|20-lts' + appCommandLine: 'pm2 serve /home/site/wwwroot --no-daemon --spa' + alwaysOn: true + } } } // The application backend -module api '../../../../../common/infra/bicep/app/api-appservice-node.bicep' = { - name: 'api' +module api 'br/public:avm/res/web/site:0.2.0' = { scope: rg + name: 'api' params: { + kind: 'app' name: !empty(apiServiceName) ? apiServiceName : '${abbrs.webSitesAppService}api-${resourceToken}' + serverFarmResourceId: appServicePlan.outputs.resourceId + tags: union(tags, { 'azd-service-name': 'api' }) location: location - tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName - appServicePlanId: appServicePlan.outputs.id - keyVaultName: keyVault.outputs.name - allowedOrigins: [ web.outputs.SERVICE_WEB_URI ] - appSettings: { - AZURE_COSMOS_CONNECTION_STRING_KEY: cosmos.outputs.connectionStringKey - AZURE_COSMOS_DATABASE_NAME: cosmos.outputs.databaseName - AZURE_COSMOS_ENDPOINT: cosmos.outputs.endpoint - API_ALLOW_ORIGINS: web.outputs.SERVICE_WEB_URI + appInsightResourceId: applicationInsights.outputs.resourceId + managedIdentities: { + systemAssigned: true + } + siteConfig: { + cors: { + allowedOrigins: [ 'https://portal.azure.com', 'https://ms.portal.azure.com' , webUri ] + } + alwaysOn: true + linuxFxVersion: 'node|20-lts' + appCommandLine: '' + } + appSettingsKeyValuePairs: { + AZURE_KEY_VAULT_ENDPOINT: keyVault.outputs.uri + AZURE_COSMOS_CONNECTION_STRING_KEY: connectionStringKey + AZURE_COSMOS_DATABASE_NAME: !empty(cosmosDatabaseName) ? cosmosDatabaseName: 'Todo' + AZURE_COSMOS_ENDPOINT: 'https://${cosmos.outputs.name}.documents.azure.com:443/' + API_ALLOW_ORIGINS: webUri + SCM_DO_BUILD_DURING_DEPLOYMENT: 'True' + ENABLE_ORYX_BUILD: 'True' } } } // Give the API access to KeyVault -module apiKeyVaultAccess '../../../../../../common/infra/bicep/core/security/keyvault-access.bicep' = { - name: 'api-keyvault-access' +module accesskeyvault 'br/public:avm/res/key-vault/vault:0.3.5' = { + name: 'accesskeyvault' scope: rg params: { - keyVaultName: keyVault.outputs.name - principalId: api.outputs.SERVICE_API_IDENTITY_PRINCIPAL_ID + name: keyVault.outputs.name + enableRbacAuthorization: false + accessPolicies: [ + { + objectId: principalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + { + objectId: api.outputs.systemAssignedMIPrincipalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + ] } } // The application database -module cosmos '../../../../../common/infra/bicep/app/cosmos-mongo-db.bicep' = { +module cosmos 'br/public:avm/res/document-db/database-account:0.4.0' = { name: 'cosmos' scope: rg params: { - accountName: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' - databaseName: cosmosDatabaseName + locations: [ + { + failoverPriority: 0 + isZoneRedundant: false + locationName: location + } + ] + name: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' location: location - tags: tags - keyVaultName: keyVault.outputs.name + mongodbDatabases: [ + { + name: 'Todo' + tags: tags + collections: collections + } + ] + secretsKeyVault: { + keyVaultName: keyVault.outputs.name + primaryWriteConnectionStringSecretName: connectionStringKey + } } } // Create an App Service Plan to group applications under the same payment plan and SKU -module appServicePlan '../../../../../../common/infra/bicep/core/host/appserviceplan.bicep' = { +module appServicePlan 'br/public:avm/res/web/serverfarm:0.1.0' = { name: 'appserviceplan' scope: rg params: { name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}' - location: location - tags: tags sku: { name: 'B3' + tier: 'Basic' } + location: location + tags: tags + reserved: true + kind: 'Linux' } } -// Store secrets in a keyvault -module keyVault '../../../../../../common/infra/bicep/core/security/keyvault.bicep' = { +// Create a keyvault to store secrets +module keyVault 'br/public:avm/res/key-vault/vault:0.3.5' = { name: 'keyvault' scope: rg params: { name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' location: location tags: tags - principalId: principalId + enableRbacAuthorization: false } } -// Monitor application with Azure Monitor -module monitoring '../../../../../../common/infra/bicep/core/monitor/monitoring.bicep' = { - name: 'monitoring' +// Monitor application with Azure loganalytics +module loganalytics 'br/public:avm/res/operational-insights/workspace:0.3.4' = { + name: 'loganalytics' scope: rg params: { + name: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' location: location - tags: tags - logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' - applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' - applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + } +} + +// Monitor application with Azure applicationInsights +module applicationInsights 'br/public:avm/res/insights/component:0.3.0' = { + name: 'applicationInsights' + scope: rg + params: { + name: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + workspaceResourceId: loganalytics.outputs.resourceId + location: location + } +} + +// Monitor application with Azure applicationInsightsDashboard +module applicationInsightsDashboard '../../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep' = { + name: 'application-insights-dashboard' + scope: rg + params: { + name: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + location: location + applicationInsightsName: applicationInsights.outputs.name } } // Creates Azure API Management (APIM) service to mediate the requests between the frontend and the backend API -module apim '../../../../../../common/infra/bicep/core/gateway/apim.bicep' = if (useAPIM) { +module apim 'br/public:avm/res/api-management/service:0.1.3' = if (useAPIM) { name: 'apim-deployment' scope: rg params: { name: !empty(apimServiceName) ? apimServiceName : '${abbrs.apiManagementService}${resourceToken}' - sku: apimSku + publisherEmail: 'noreply@microsoft.com' + publisherName: 'n/a' location: location tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName + apis: [ + { + name: 'todo-api' + path: 'todo' + displayName: 'Simple Todo API' + apiDescription: 'This is a simple Todo API' + serviceUrl: apiUri + subscriptionRequired: false + value: loadTextContent('../../../../../api/common/openapi.yaml') + policies: [ + { + value: replace(loadTextContent('../../../../../../common/infra/shared/gateway/apim/apim-api-policy.xml'), '{origin}', webUri) + format: 'rawxml' + } + ] + } + ] } } // Configures the API in the Azure API Management (APIM) service -module apimApi '../../../../../common/infra/bicep/app/apim-api.bicep' = if (useAPIM) { - name: 'apim-api-deployment' +module apimsettings '../../../../../common/infra/bicep/app/apim-api-settings.bicep' = if (useAPIM) { + name: 'apim-api-settings' scope: rg params: { - name: useAPIM ? apim.outputs.apimServiceName : '' + apiAppName: api.outputs.name apiName: 'todo-api' - apiDisplayName: 'Simple Todo API' - apiDescription: 'This is a simple Todo API' + name: useAPIM ? apim.outputs.name : '' apiPath: 'todo' - webFrontendUrl: web.outputs.SERVICE_WEB_URI - apiBackendUrl: api.outputs.SERVICE_API_URI - apiAppName: api.outputs.SERVICE_API_NAME + applicationInsightsName: applicationInsights.outputs.name } } // Data outputs -output AZURE_COSMOS_CONNECTION_STRING_KEY string = cosmos.outputs.connectionStringKey -output AZURE_COSMOS_DATABASE_NAME string = cosmos.outputs.databaseName +output AZURE_COSMOS_CONNECTION_STRING_KEY string = connectionStringKey +output AZURE_COSMOS_DATABASE_NAME string = !empty(cosmosDatabaseName) ? cosmosDatabaseName: 'Todo' // App outputs -output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString -output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint +output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsights.outputs.connectionString +output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.uri output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId -output API_BASE_URL string = useAPIM ? apimApi.outputs.SERVICE_API_URI : api.outputs.SERVICE_API_URI -output REACT_APP_WEB_BASE_URL string = web.outputs.SERVICE_WEB_URI +output API_BASE_URL string = useAPIM ? 'https://${apim.outputs.name}.azure-api.net/todo' : apiUri +output REACT_APP_WEB_BASE_URL string = webUri output USE_APIM bool = useAPIM -output SERVICE_API_ENDPOINTS array = useAPIM ? [ apimApi.outputs.SERVICE_API_URI, api.outputs.SERVICE_API_URI ]: [] +output SERVICE_API_ENDPOINTS array = useAPIM ? [ 'https://${apim.outputs.name}.azure-api.net/todo', apiUri ]: [] diff --git a/templates/todo/projects/nodejs-mongo/.repo/bicep/repo.yaml b/templates/todo/projects/nodejs-mongo/.repo/bicep/repo.yaml index 0893c48bd63..fd3d3e3129d 100644 --- a/templates/todo/projects/nodejs-mongo/.repo/bicep/repo.yaml +++ b/templates/todo/projects/nodejs-mongo/.repo/bicep/repo.yaml @@ -79,6 +79,16 @@ repo: to: ../../src/api/openapi.yaml patterns: - "apim-api.bicep" + + - from: ../../../api/common/openapi.yaml + to: ../src/api/openapi.yaml + patterns: + - "**/main.bicep" + + - from: ../../../common/infra/shared/gateway/apim/apim-api-policy.xml + to: ./app/apim-api-policy.xml + patterns: + - "**/main.bicep" assets: # Common assets @@ -101,6 +111,12 @@ repo: - from: ../../../../common/infra/bicep/app/cosmos-mongo-db.bicep to: ./infra/app/db.bicep + + - from: ../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep + to: ./infra/app/applicationinsights-dashboard.bicep + + - from: ../../../../common/infra/bicep/app/apim-api-settings.bicep + to: ./infra/app/apim-api-settings.bicep - from: ./../../ to: ./ diff --git a/templates/todo/projects/python-mongo-aca/.repo/bicep/infra/main.bicep b/templates/todo/projects/python-mongo-aca/.repo/bicep/infra/main.bicep index 96945b54d09..29b8d8c2016 100644 --- a/templates/todo/projects/python-mongo-aca/.repo/bicep/infra/main.bicep +++ b/templates/todo/projects/python-mongo-aca/.repo/bicep/infra/main.bicep @@ -25,30 +25,59 @@ param logAnalyticsName string = '' param resourceGroupName string = '' param webContainerAppName string = '' param apimServiceName string = '' -param apiAppExists bool = false -param webAppExists bool = false +param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' +param collections array = [ + { + name: 'TodoList' + id: 'TodoList' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } + { + name: 'TodoItem' + id: 'TodoItem' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } +] @description('Flag to use Azure API Management to mediate the calls between the Web frontend and the backend API') param useAPIM bool = false -@description('API Management SKU to use if APIM is enabled') -param apimSku string = 'Consumption' - @description('Hostname suffix for container registry. Set when deploying to sovereign clouds') param containerRegistryHostSuffix string = 'azurecr.io' - @description('Id of the user or app to assign application roles') param principalId string = '' -@description('The base URL used by the web service for sending API requests') -param webApiBaseUrl string = '' - var abbrs = loadJsonContent('../../../../../../common/infra/bicep/abbreviations.json') var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) var tags = { 'azd-env-name': environmentName } var apiContainerAppNameOrDefault = '${abbrs.appContainerApps}web-${resourceToken}' -var corsAcaUrl = 'https://${apiContainerAppNameOrDefault}.${containerApps.outputs.defaultDomain}' +var corsAcaUrl = 'https://${apiContainerAppNameOrDefault}.${containerAppsEnvironment.outputs.defaultDomain}' +var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') +var webUri = 'https://${web.outputs.fqdn}' +var apiUri = 'https://${api.outputs.fqdn}' // Organize resources in a resource group resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { @@ -57,147 +86,299 @@ resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { tags: tags } -// Container apps host (including container registry) -module containerApps '../../../../../../common/infra/bicep/core/host/container-apps.bicep' = { - name: 'container-apps' +// Container registry +module containerRegistry 'br/public:avm/res/container-registry/registry:0.1.1' = { + name: 'registry' + scope: rg + params: { + name: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}' + location: location + acrAdminUserEnabled: true + tags: tags + publicNetworkAccess: 'Enabled' + roleAssignments:[ + { + principalId: webIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: acrPullRole + } + { + principalId: apiIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: acrPullRole + } + ] + } +} + +//Container apps environment +module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.4.5' = { + name: 'containerAppsEnvironment' scope: rg params: { - name: 'app' + logAnalyticsWorkspaceResourceId: loganalytics.outputs.resourceId + name: !empty(containerAppsEnvironmentName) ? containerAppsEnvironmentName : '${abbrs.appManagedEnvironments}${resourceToken}' location: location + zoneRedundant: false tags: tags - containerAppsEnvironmentName: !empty(containerAppsEnvironmentName) ? containerAppsEnvironmentName : '${abbrs.appManagedEnvironments}${resourceToken}' - containerRegistryName: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}' - // Work around Azure/azure-dev#3157 (the root cause of which is Azure/acr#723) by explicitly enabling the admin user to allow users which - // don't have the `Owner` role granted (and instead are classic administrators) to access the registry to push even if AAD authentication fails. - // - // This addresses the following error during deploy: - // - // failed getting ACR token: POST https://.azurecr.io/oauth2/exchange 401 Unauthorized - containerRegistryAdminUserEnabled: true - logAnalyticsWorkspaceName: monitoring.outputs.logAnalyticsWorkspaceName - applicationInsightsName: monitoring.outputs.applicationInsightsName + } +} + +//the managed identity for web frontend +module webIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { + name: 'webIdentity' + scope: rg + params: { + name: '${abbrs.managedIdentityUserAssignedIdentities}web-${resourceToken}' + location: location } } // Web frontend -module web '../../../../../common/infra/bicep/app/web-container-app.bicep' = { +module web 'br/public:avm/res/app/container-app:0.2.0' = { name: 'web' scope: rg params: { name: !empty(webContainerAppName) ? webContainerAppName : '${abbrs.appContainerApps}web-${resourceToken}' + containers: [ + { + image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' + name: 'simple-hello-world-container' + resources: { + cpu: json('0.5') + memory: '1.0Gi' + } + } + ] + managedIdentities:{ + systemAssigned: false + userAssignedResourceIds: [webIdentity.outputs.resourceId] + } + registries:[ + { + server: '${containerRegistry.outputs.name}.${containerRegistryHostSuffix}' + identity: webIdentity.outputs.resourceId + } + ] + dapr: { + enabled: true + appId: 'main' + appProtocol: 'http' + appPort: 80 + } + environmentId: containerAppsEnvironment.outputs.resourceId + location: location + tags: union(tags, { 'azd-service-name': 'web' }) + } +} + +//the managed identity for api backend +module apiIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { + name: 'apiIdentity' + scope: rg + params: { + name: '${abbrs.managedIdentityUserAssignedIdentities}api-${resourceToken}' location: location - tags: tags - identityName: '${abbrs.managedIdentityUserAssignedIdentities}web-${resourceToken}' - containerAppsEnvironmentName: containerApps.outputs.environmentName - containerRegistryName: containerApps.outputs.registryName - containerRegistryHostSuffix: containerRegistryHostSuffix - exists: webAppExists } } // Api backend -module api '../../../../../common/infra/bicep/app/api-container-app.bicep' = { +module api 'br/public:avm/res/app/container-app:0.2.0' = { name: 'api' scope: rg params: { name: !empty(apiContainerAppName) ? apiContainerAppName : '${abbrs.appContainerApps}api-${resourceToken}' + containers: [ + { + image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' + name: 'simple-hello-world-container' + resources: { + cpu: json('1.0') + memory: '2.0Gi' + } + env: [ + { + name: 'AZURE_CLIENT_ID' + value: apiIdentity.outputs.clientId + } + { + name: 'AZURE_KEY_VAULT_ENDPOINT' + value: keyVault.outputs.uri + } + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: applicationInsights.outputs.connectionString + } + { + name: 'API_ALLOW_ORIGINS' + value: corsAcaUrl + } + ] + } + ] + managedIdentities:{ + systemAssigned: false + userAssignedResourceIds: [apiIdentity.outputs.resourceId] + } + registries:[ + { + server: '${containerRegistry.outputs.name}.${containerRegistryHostSuffix}' + identity: apiIdentity.outputs.resourceId + } + ] + environmentId: containerAppsEnvironment.outputs.resourceId + ingressTargetPort: 3100 location: location - tags: tags - identityName: '${abbrs.managedIdentityUserAssignedIdentities}api-${resourceToken}' - applicationInsightsName: monitoring.outputs.applicationInsightsName - containerAppsEnvironmentName: containerApps.outputs.environmentName - containerRegistryName: containerApps.outputs.registryName - containerRegistryHostSuffix: containerRegistryHostSuffix - keyVaultName: keyVault.outputs.name - corsAcaUrl: corsAcaUrl - exists: apiAppExists + tags: union(tags, { 'azd-service-name': 'api' }) } } // The application database -module cosmos '../../../../../common/infra/bicep/app/cosmos-mongo-db.bicep' = { +module cosmos 'br/public:avm/res/document-db/database-account:0.4.0' = { name: 'cosmos' scope: rg params: { - accountName: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' - databaseName: cosmosDatabaseName + locations: [ + { + failoverPriority: 0 + isZoneRedundant: false + locationName: location + } + ] + name: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' location: location - tags: tags - keyVaultName: keyVault.outputs.name + mongodbDatabases: [ + { + name: 'Todo' + tags: tags + collections: collections + } + ] + secretsKeyVault: { + keyVaultName: keyVault.outputs.name + primaryWriteConnectionStringSecretName: connectionStringKey + } } } -// Store secrets in a keyvault -module keyVault '../../../../../../common/infra/bicep/core/security/keyvault.bicep' = { +// Create a keyvault to store secrets +module keyVault 'br/public:avm/res/key-vault/vault:0.5.1' = { name: 'keyvault' scope: rg params: { name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' location: location tags: tags - principalId: principalId + enableRbacAuthorization: false + accessPolicies: [ + { + objectId: principalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + { + objectId: apiIdentity.outputs.principalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + ] } } -// Monitor application with Azure Monitor -module monitoring '../../../../../../common/infra/bicep/core/monitor/monitoring.bicep' = { - name: 'monitoring' +// Monitor application with Azure loganalytics +module loganalytics 'br/public:avm/res/operational-insights/workspace:0.3.4' = { + name: 'loganalytics' scope: rg params: { + name: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' location: location - tags: tags - logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' - applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' - applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + } +} + +// Monitor application with Azure applicationInsights +module applicationInsights 'br/public:avm/res/insights/component:0.3.0' = { + name: 'applicationInsights' + scope: rg + params: { + name: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + workspaceResourceId: loganalytics.outputs.resourceId + location: location + } +} + +//Monitor application with Azure applicationInsightsDashboard +module applicationInsightsDashboard '../../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep' = { + name: 'application-insights-dashboard' + scope: rg + params: { + name: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + location: location + applicationInsightsName: applicationInsights.outputs.name } } // Creates Azure API Management (APIM) service to mediate the requests between the frontend and the backend API -module apim '../../../../../../common/infra/bicep/core/gateway/apim.bicep' = if (useAPIM) { +module apim 'br/public:avm/res/api-management/service:0.1.3' = if (useAPIM) { name: 'apim-deployment' scope: rg params: { name: !empty(apimServiceName) ? apimServiceName : '${abbrs.apiManagementService}${resourceToken}' - sku: apimSku + publisherEmail: 'noreply@microsoft.com' + publisherName: 'n/a' location: location tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName + apis: [ + { + name: 'todo-api' + path: 'todo' + displayName: 'Simple Todo API' + apiDescription: 'This is a simple Todo API' + serviceUrl: apiUri + subscriptionRequired: false + value: loadTextContent('../../../../../api/common/openapi.yaml') + policies: [ + { + value: replace(loadTextContent('../../../../../../common/infra/shared/gateway/apim/apim-api-policy.xml'), '{origin}', webUri) + format: 'rawxml' + } + ] + } + ] } } // Configures the API in the Azure API Management (APIM) service -module apimApi '../../../../../common/infra/bicep/app/apim-api.bicep' = if (useAPIM) { - name: 'apim-api-deployment' +module apimsettings '../../../../../common/infra/bicep/app/apim-api-settings.bicep' = if (useAPIM) { + name: 'apim-api-settings' scope: rg params: { - name: useAPIM ? apim.outputs.apimServiceName : '' apiName: 'todo-api' - apiDisplayName: 'Simple Todo API' - apiDescription: 'This is a simple Todo API' + name: useAPIM ? apim.outputs.name : '' apiPath: 'todo' - webFrontendUrl: web.outputs.SERVICE_WEB_URI - apiBackendUrl: api.outputs.SERVICE_API_URI + applicationInsightsName: applicationInsights.outputs.name } } // Data outputs -output AZURE_COSMOS_CONNECTION_STRING_KEY string = cosmos.outputs.connectionStringKey -output AZURE_COSMOS_DATABASE_NAME string = cosmos.outputs.databaseName +output AZURE_COSMOS_CONNECTION_STRING_KEY string = connectionStringKey +output AZURE_COSMOS_DATABASE_NAME string = !empty(cosmosDatabaseName) ? cosmosDatabaseName: 'Todo' // App outputs output API_CORS_ACA_URL string = corsAcaUrl -output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString -output APPLICATIONINSIGHTS_NAME string = monitoring.outputs.applicationInsightsName -output AZURE_CONTAINER_ENVIRONMENT_NAME string = containerApps.outputs.environmentName -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerApps.outputs.registryLoginServer -output AZURE_CONTAINER_REGISTRY_NAME string = containerApps.outputs.registryName -output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint +output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsights.outputs.connectionString +output APPLICATIONINSIGHTS_NAME string = applicationInsights.outputs.name +output AZURE_CONTAINER_ENVIRONMENT_NAME string = containerAppsEnvironment.outputs.name +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer +output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.outputs.name +output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.uri output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId -output API_BASE_URL string = useAPIM ? apimApi.outputs.SERVICE_API_URI : api.outputs.SERVICE_API_URI -output REACT_APP_WEB_BASE_URL string = web.outputs.SERVICE_WEB_URI -output SERVICE_API_NAME string = api.outputs.SERVICE_API_NAME -output SERVICE_WEB_NAME string = web.outputs.SERVICE_WEB_NAME +output API_BASE_URL string = useAPIM ? 'https://${apim.outputs.name}.azure-api.net/todo' : apiUri +output REACT_APP_WEB_BASE_URL string = webUri +output SERVICE_API_NAME string = api.outputs.name +output SERVICE_WEB_NAME string = web.outputs.name output USE_APIM bool = useAPIM -output SERVICE_API_ENDPOINTS array = useAPIM ? [ apimApi.outputs.SERVICE_API_URI, api.outputs.SERVICE_API_URI ] : [] +output SERVICE_API_ENDPOINTS array = useAPIM ? [ 'https://${apim.outputs.name}.azure-api.net/todo', apiUri ] : [] diff --git a/templates/todo/projects/python-mongo-aca/.repo/bicep/repo.yaml b/templates/todo/projects/python-mongo-aca/.repo/bicep/repo.yaml index 75a6a958b3c..a47fa56baad 100644 --- a/templates/todo/projects/python-mongo-aca/.repo/bicep/repo.yaml +++ b/templates/todo/projects/python-mongo-aca/.repo/bicep/repo.yaml @@ -86,6 +86,16 @@ repo: to: ../../src/api/openapi.yaml patterns: - "apim-api.bicep" + + - from: ../../../api/common/openapi.yaml + to: ../src/api/openapi.yaml + patterns: + - "**/main.bicep" + + - from: ../../../common/infra/shared/gateway/apim/apim-api-policy.xml + to: ./app/apim-api-policy.xml + patterns: + - "**/main.bicep" assets: # # Common assets @@ -112,6 +122,12 @@ repo: - from: ../../../../common/infra/bicep/app/cosmos-mongo-db.bicep to: ./infra/app/db.bicep + - from: ../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep + to: ./infra/app/applicationinsights-dashboard.bicep + + - from: ../../../../common/infra/bicep/app/apim-api-settings.bicep + to: ./infra/app/apim-api-settings.bicep + - from: ./../../ to: ./ ignore: diff --git a/templates/todo/projects/python-mongo-swa-func/.repo/bicep/infra/main.bicep b/templates/todo/projects/python-mongo-swa-func/.repo/bicep/infra/main.bicep index fbbcfcd6aef..bd6423f7b50 100644 --- a/templates/todo/projects/python-mongo-swa-func/.repo/bicep/infra/main.bicep +++ b/templates/todo/projects/python-mongo-swa-func/.repo/bicep/infra/main.bicep @@ -25,19 +25,53 @@ param resourceGroupName string = '' param storageAccountName string = '' param webServiceName string = '' param apimServiceName string = '' +param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' +param collections array = [ + { + name: 'TodoList' + id: 'TodoList' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } + { + name: 'TodoItem' + id: 'TodoItem' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } +] @description('Flag to use Azure API Management to mediate the calls between the Web frontend and the backend API') param useAPIM bool = false -@description('API Management SKU to use if APIM is enabled') -param apimSku string = 'Consumption' - @description('Id of the user or app to assign application roles') param principalId string = '' var abbrs = loadJsonContent('../../../../../../common/infra/bicep/abbreviations.json') var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) var tags = { 'azd-env-name': environmentName } +var webUri = 'https://${web.outputs.defaultHostname}' +var apiUri = 'https://${api.outputs.defaultHostname}' // Organize resources in a resource group resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { @@ -47,152 +81,238 @@ resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { } // The application frontend -module web '../../../../../common/infra/bicep/app/web-staticwebapp.bicep' = { +module web 'br/public:avm/res/web/static-site:0.3.0' = { name: 'web' scope: rg params: { name: !empty(webServiceName) ? webServiceName : '${abbrs.webStaticSites}web-${resourceToken}' location: location - tags: tags + provider: 'Custom' + tags: union(tags, { 'azd-service-name': 'web' }) } } // The application backend -module api '../../../../../common/infra/bicep/app/api-functions-python.bicep' = { +module api 'br/public:avm/res/web/site:0.3.5' = { name: 'api' scope: rg params: { + kind: 'functionapp' name: !empty(apiServiceName) ? apiServiceName : '${abbrs.webSitesFunctions}api-${resourceToken}' + serverFarmResourceId: appServicePlan.outputs.resourceId + tags: union(tags, { 'azd-service-name': 'api' }) location: location - tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName - appServicePlanId: appServicePlan.outputs.id - keyVaultName: keyVault.outputs.name - storageAccountName: storage.outputs.name - allowedOrigins: [ web.outputs.SERVICE_WEB_URI ] - appSettings: { - AZURE_COSMOS_CONNECTION_STRING_KEY: cosmos.outputs.connectionStringKey - AZURE_COSMOS_DATABASE_NAME: cosmos.outputs.databaseName - AZURE_COSMOS_ENDPOINT: cosmos.outputs.endpoint - API_ALLOW_ORIGINS: web.outputs.SERVICE_WEB_URI + appInsightResourceId: applicationInsights.outputs.resourceId + managedIdentities: { + systemAssigned: true + } + clientAffinityEnabled: false + siteConfig: { + cors: { + allowedOrigins: [ 'https://portal.azure.com', 'https://ms.portal.azure.com' , webUri ] + } + linuxFxVersion: 'python|3.10' + use32BitWorkerProcess: false + } + appSettingsKeyValuePairs: { + API_ALLOW_ORIGINS: webUri + AZURE_COSMOS_CONNECTION_STRING_KEY: connectionStringKey + AZURE_COSMOS_DATABASE_NAME: !empty(cosmosDatabaseName) ? cosmosDatabaseName: 'Todo' + AZURE_KEY_VAULT_ENDPOINT:keyVault.outputs.uri + AZURE_COSMOS_ENDPOINT: 'https://${cosmos.outputs.name}.documents.azure.com:443/' + FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_WORKER_RUNTIME: 'python' + SCM_DO_BUILD_DURING_DEPLOYMENT: 'True' + ENABLE_ORYX_BUILD: 'True' } + storageAccountResourceId: storage.outputs.resourceId } } // Give the API access to KeyVault -module apiKeyVaultAccess '../../../../../../common/infra/bicep/core/security/keyvault-access.bicep' = { - name: 'api-keyvault-access' +module accesskeyvault 'br/public:avm/res/key-vault/vault:0.5.1' = { + name: 'accesskeyvault' scope: rg params: { - keyVaultName: keyVault.outputs.name - principalId: api.outputs.SERVICE_API_IDENTITY_PRINCIPAL_ID + name: keyVault.outputs.name + enableRbacAuthorization: false + accessPolicies: [ + { + objectId: api.outputs.systemAssignedMIPrincipalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + { + objectId: principalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + ] } } // The application database -module cosmos '../../../../../common/infra/bicep/app/cosmos-mongo-db.bicep' = { +module cosmos 'br/public:avm/res/document-db/database-account:0.4.0' = { name: 'cosmos' scope: rg params: { - accountName: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' - databaseName: cosmosDatabaseName + locations: [ + { + failoverPriority: 0 + isZoneRedundant: false + locationName: location + } + ] + name: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' location: location - tags: tags - keyVaultName: keyVault.outputs.name + mongodbDatabases: [ + { + name: 'Todo' + tags: tags + collections: collections + } + ] + secretsKeyVault: { + keyVaultName: keyVault.outputs.name + primaryWriteConnectionStringSecretName: connectionStringKey + } } } // Create an App Service Plan to group applications under the same payment plan and SKU -module appServicePlan '../../../../../../common/infra/bicep/core/host/appserviceplan.bicep' = { +module appServicePlan 'br/public:avm/res/web/serverfarm:0.1.1' = { name: 'appserviceplan' scope: rg params: { name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}' - location: location - tags: tags sku: { name: 'Y1' tier: 'Dynamic' } + location: location + tags: tags + reserved: true + kind: 'Linux' } } // Backing storage for Azure functions backend API -module storage '../../../../../../common/infra/bicep/core/storage/storage-account.bicep' = { +module storage 'br/public:avm/res/storage/storage-account:0.8.3' = { name: 'storage' scope: rg params: { name: !empty(storageAccountName) ? storageAccountName : '${abbrs.storageStorageAccounts}${resourceToken}' + allowBlobPublicAccess: true + dnsEndpointType: 'Standard' + publicNetworkAccess:'Enabled' + networkAcls:{ + bypass: 'AzureServices' + defaultAction: 'Allow' + } location: location tags: tags } } -// Store secrets in a keyvault -module keyVault '../../../../../../common/infra/bicep/core/security/keyvault.bicep' = { +// Create a keyvault to store secrets +module keyVault 'br/public:avm/res/key-vault/vault:0.5.1' = { name: 'keyvault' scope: rg params: { name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' location: location tags: tags - principalId: principalId + enableRbacAuthorization: false } } -// Monitor application with Azure Monitor -module monitoring '../../../../../../common/infra/bicep/core/monitor/monitoring.bicep' = { - name: 'monitoring' +// Monitor application with Azure loganalytics +module loganalytics 'br/public:avm/res/operational-insights/workspace:0.3.4' = { + name: 'loganalytics' scope: rg params: { + name: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' location: location - tags: tags - logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' - applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' - applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + } +} + +// Monitor application with Azure applicationInsights +module applicationInsights 'br/public:avm/res/insights/component:0.3.0' = { + name: 'applicationInsights' + scope: rg + params: { + name: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + workspaceResourceId: loganalytics.outputs.resourceId + location: location + } +} + +// Monitor application with Azure applicationInsightsDashboard +module applicationInsightsDashboard '../../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep' = { + name: 'application-insights-dashboard' + scope: rg + params: { + name: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + location: location + applicationInsightsName: applicationInsights.outputs.name } } // Creates Azure API Management (APIM) service to mediate the requests between the frontend and the backend API -module apim '../../../../../../common/infra/bicep/core/gateway/apim.bicep' = if (useAPIM) { +module apim 'br/public:avm/res/api-management/service:0.1.3' = if (useAPIM) { name: 'apim-deployment' scope: rg params: { name: !empty(apimServiceName) ? apimServiceName : '${abbrs.apiManagementService}${resourceToken}' - sku: apimSku + publisherEmail: 'noreply@microsoft.com' + publisherName: 'n/a' location: location tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName + apis: [ + { + name: 'todo-api' + path: 'todo' + displayName: 'Simple Todo API' + apiDescription: 'This is a simple Todo API' + serviceUrl: apiUri + subscriptionRequired: false + value: loadTextContent('../../../../../api/common/openapi.yaml') + policies: [ + { + value: replace(loadTextContent('../../../../../../common/infra/shared/gateway/apim/apim-api-policy.xml'), '{origin}', webUri) + format: 'rawxml' + } + ] + } + ] } } // Configures the API in the Azure API Management (APIM) service -module apimApi '../../../../../common/infra/bicep/app/apim-api.bicep' = if (useAPIM) { - name: 'apim-api-deployment' +module apimsettings '../../../../../common/infra/bicep/app/apim-api-settings.bicep' = if (useAPIM) { + name: 'apim-api-settings' scope: rg params: { - name: useAPIM ? apim.outputs.apimServiceName : '' apiName: 'todo-api' - apiDisplayName: 'Simple Todo API' - apiDescription: 'This is a simple Todo API' + name: useAPIM ? apim.outputs.name : '' apiPath: 'todo' - webFrontendUrl: web.outputs.SERVICE_WEB_URI - apiBackendUrl: api.outputs.SERVICE_API_URI - apiAppName: api.outputs.SERVICE_API_NAME + applicationInsightsName: applicationInsights.outputs.name } } // Data outputs -output AZURE_COSMOS_CONNECTION_STRING_KEY string = cosmos.outputs.connectionStringKey -output AZURE_COSMOS_DATABASE_NAME string = cosmos.outputs.databaseName +output AZURE_COSMOS_CONNECTION_STRING_KEY string = connectionStringKey +output AZURE_COSMOS_DATABASE_NAME string = !empty(cosmosDatabaseName) ? cosmosDatabaseName: 'Todo' // App outputs -output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString -output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint +output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsights.outputs.connectionString +output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.uri output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId -output API_BASE_URL string = useAPIM ? apimApi.outputs.SERVICE_API_URI : api.outputs.SERVICE_API_URI -output REACT_APP_WEB_BASE_URL string = web.outputs.SERVICE_WEB_URI +output API_BASE_URL string = useAPIM ? 'https://${apim.outputs.name}.azure-api.net/todo' : apiUri +output REACT_APP_WEB_BASE_URL string = webUri output USE_APIM bool = useAPIM -output SERVICE_API_ENDPOINTS array = useAPIM ? [ apimApi.outputs.SERVICE_API_URI, api.outputs.SERVICE_API_URI ]: [] +output SERVICE_API_ENDPOINTS array = useAPIM ? [ 'https://${apim.outputs.name}.azure-api.net/todo', apiUri ]: [] diff --git a/templates/todo/projects/python-mongo-swa-func/.repo/bicep/repo.yaml b/templates/todo/projects/python-mongo-swa-func/.repo/bicep/repo.yaml index f61814d76d4..b187594e6a6 100644 --- a/templates/todo/projects/python-mongo-swa-func/.repo/bicep/repo.yaml +++ b/templates/todo/projects/python-mongo-swa-func/.repo/bicep/repo.yaml @@ -80,6 +80,16 @@ repo: patterns: - "apim-api.bicep" + - from: ../../../api/common/openapi.yaml + to: ../src/api/openapi.yaml + patterns: + - "**/main.bicep" + + - from: ../../../common/infra/shared/gateway/apim/apim-api-policy.xml + to: ./app/apim-api-policy.xml + patterns: + - "**/main.bicep" + assets: # # Common assets @@ -104,6 +114,12 @@ repo: - from: ../../../../common/infra/bicep/app/cosmos-mongo-db.bicep to: ./infra/app/db.bicep + + - from: ../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep + to: ./infra/app/applicationinsights-dashboard.bicep + + - from: ../../../../common/infra/bicep/app/apim-api-settings.bicep + to: ./infra/app/apim-api-settings.bicep - from: ./../../ to: ./ diff --git a/templates/todo/projects/python-mongo/.repo/bicep/infra/main.bicep b/templates/todo/projects/python-mongo/.repo/bicep/infra/main.bicep index 241e77aab17..0bc3da3a61f 100644 --- a/templates/todo/projects/python-mongo/.repo/bicep/infra/main.bicep +++ b/templates/todo/projects/python-mongo/.repo/bicep/infra/main.bicep @@ -24,19 +24,53 @@ param logAnalyticsName string = '' param resourceGroupName string = '' param webServiceName string = '' param apimServiceName string = '' +param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' +param collections array = [ + { + name: 'TodoList' + id: 'TodoList' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } + { + name: 'TodoItem' + id: 'TodoItem' + shardKey: {keys: [ + 'Hash' + ]} + indexes: [ + { + key: { + keys: [ + '_id' + ] + } + } + ] + } +] @description('Flag to use Azure API Management to mediate the calls between the Web frontend and the backend API') param useAPIM bool = false -@description('API Management SKU to use if APIM is enabled') -param apimSku string = 'Consumption' - @description('Id of the user or app to assign application roles') param principalId string = '' var abbrs = loadJsonContent('../../../../../../common/infra/bicep/abbreviations.json') var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) var tags = { 'azd-env-name': environmentName } +var webUri = 'https://${web.outputs.defaultHostname}' +var apiUri = 'https://${api.outputs.defaultHostname}' // Organize resources in a resource group resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { @@ -46,141 +80,225 @@ resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { } // The application frontend -module web '../../../../../common/infra/bicep/app/web-appservice.bicep' = { +module web 'br/public:avm/res/web/site:0.2.0' = { name: 'web' scope: rg params: { + kind: 'app' name: !empty(webServiceName) ? webServiceName : '${abbrs.webSitesAppService}web-${resourceToken}' + serverFarmResourceId: appServicePlan.outputs.resourceId + tags: union(tags, { 'azd-service-name': 'web' }) location: location - tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName - appServicePlanId: appServicePlan.outputs.id + appInsightResourceId: applicationInsights.outputs.resourceId + siteConfig: { + linuxFxVersion: 'node|20-lts' + appCommandLine: 'pm2 serve /home/site/wwwroot --no-daemon --spa' + alwaysOn: true + } } } // The application backend -module api '../../../../../common/infra/bicep/app/api-appservice-python.bicep' = { - name: 'api' +module api 'br/public:avm/res/web/site:0.2.0' = { scope: rg + name: 'api' params: { + kind: 'app' name: !empty(apiServiceName) ? apiServiceName : '${abbrs.webSitesAppService}api-${resourceToken}' + serverFarmResourceId: appServicePlan.outputs.resourceId + tags: union(tags, { 'azd-service-name': 'api' }) location: location - tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName - appServicePlanId: appServicePlan.outputs.id - keyVaultName: keyVault.outputs.name - allowedOrigins: [ web.outputs.SERVICE_WEB_URI ] - appSettings: { - AZURE_COSMOS_CONNECTION_STRING_KEY: cosmos.outputs.connectionStringKey - AZURE_COSMOS_DATABASE_NAME: cosmos.outputs.databaseName - AZURE_COSMOS_ENDPOINT: cosmos.outputs.endpoint - API_ALLOW_ORIGINS: web.outputs.SERVICE_WEB_URI + appInsightResourceId: applicationInsights.outputs.resourceId + managedIdentities: { + systemAssigned: true + } + siteConfig: { + cors: { + allowedOrigins: [ 'https://portal.azure.com', 'https://ms.portal.azure.com' , webUri ] + } + alwaysOn: true + linuxFxVersion: 'python|3.10' + appCommandLine: 'gunicorn --workers 4 --threads 2 --timeout 60 --access-logfile "-" --error-logfile "-" --bind=0.0.0.0:8000 -k uvicorn.workers.UvicornWorker todo.app:app' + } + appSettingsKeyValuePairs: { + AZURE_KEY_VAULT_ENDPOINT: keyVault.outputs.uri + AZURE_COSMOS_CONNECTION_STRING_KEY: connectionStringKey + AZURE_COSMOS_DATABASE_NAME: !empty(cosmosDatabaseName) ? cosmosDatabaseName: 'Todo' + AZURE_COSMOS_ENDPOINT: 'https://${cosmos.outputs.name}.documents.azure.com:443/' + API_ALLOW_ORIGINS: webUri + SCM_DO_BUILD_DURING_DEPLOYMENT: 'True' + ENABLE_ORYX_BUILD: 'True' } } } // Give the API access to KeyVault -module apiKeyVaultAccess '../../../../../../common/infra/bicep/core/security/keyvault-access.bicep' = { - name: 'api-keyvault-access' +module accesskeyvault 'br/public:avm/res/key-vault/vault:0.3.5' = { + name: 'accesskeyvault' scope: rg params: { - keyVaultName: keyVault.outputs.name - principalId: api.outputs.SERVICE_API_IDENTITY_PRINCIPAL_ID + name: keyVault.outputs.name + enableRbacAuthorization: false + accessPolicies: [ + { + objectId: principalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + { + objectId: api.outputs.systemAssignedMIPrincipalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + ] } } // The application database -module cosmos '../../../../../common/infra/bicep/app/cosmos-mongo-db.bicep' = { +module cosmos 'br/public:avm/res/document-db/database-account:0.4.0' = { name: 'cosmos' scope: rg params: { - accountName: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' - databaseName: cosmosDatabaseName + locations: [ + { + failoverPriority: 0 + isZoneRedundant: false + locationName: location + } + ] + name: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' location: location - tags: tags - keyVaultName: keyVault.outputs.name + mongodbDatabases: [ + { + name: 'Todo' + tags: tags + collections: collections + } + ] + secretsKeyVault: { + keyVaultName: keyVault.outputs.name + primaryWriteConnectionStringSecretName: connectionStringKey + } } } // Create an App Service Plan to group applications under the same payment plan and SKU -module appServicePlan '../../../../../../common/infra/bicep/core/host/appserviceplan.bicep' = { +module appServicePlan 'br/public:avm/res/web/serverfarm:0.1.0' = { name: 'appserviceplan' scope: rg params: { name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}' - location: location - tags: tags sku: { name: 'B3' + tier: 'Basic' } + location: location + tags: tags + reserved: true + kind: 'Linux' } } -// Store secrets in a keyvault -module keyVault '../../../../../../common/infra/bicep/core/security/keyvault.bicep' = { +// Create a keyvault to store secrets +module keyVault 'br/public:avm/res/key-vault/vault:0.3.5' = { name: 'keyvault' scope: rg params: { name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' location: location tags: tags - principalId: principalId + enableRbacAuthorization: false } } -// Monitor application with Azure Monitor -module monitoring '../../../../../../common/infra/bicep/core/monitor/monitoring.bicep' = { - name: 'monitoring' +// Monitor application with Azure loganalytics +module loganalytics 'br/public:avm/res/operational-insights/workspace:0.3.4' = { + name: 'loganalytics' scope: rg params: { + name: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' location: location - tags: tags - logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' - applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' - applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + } +} + +// Monitor application with Azure applicationInsights +module applicationInsights 'br/public:avm/res/insights/component:0.3.0' = { + name: 'applicationInsights' + scope: rg + params: { + name: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + workspaceResourceId: loganalytics.outputs.resourceId + location: location + } +} + +// Monitor application with Azure applicationInsightsDashboard +module applicationInsightsDashboard '../../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep' = { + name: 'application-insights-dashboard' + scope: rg + params: { + name: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + location: location + applicationInsightsName: applicationInsights.outputs.name } } // Creates Azure API Management (APIM) service to mediate the requests between the frontend and the backend API -module apim '../../../../../../common/infra/bicep/core/gateway/apim.bicep' = if (useAPIM) { +module apim 'br/public:avm/res/api-management/service:0.1.3' = if (useAPIM) { name: 'apim-deployment' scope: rg params: { name: !empty(apimServiceName) ? apimServiceName : '${abbrs.apiManagementService}${resourceToken}' - sku: apimSku + publisherEmail: 'noreply@microsoft.com' + publisherName: 'n/a' location: location tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName + apis: [ + { + name: 'todo-api' + path: 'todo' + displayName: 'Simple Todo API' + apiDescription: 'This is a simple Todo API' + serviceUrl: apiUri + subscriptionRequired: false + value: loadTextContent('../../../../../api/common/openapi.yaml') + policies: [ + { + value: replace(loadTextContent('../../../../../../common/infra/shared/gateway/apim/apim-api-policy.xml'), '{origin}', webUri) + format: 'rawxml' + } + ] + } + ] } } // Configures the API in the Azure API Management (APIM) service -module apimApi '../../../../../common/infra/bicep/app/apim-api.bicep' = if (useAPIM) { - name: 'apim-api-deployment' +module apimsettings '../../../../../common/infra/bicep/app/apim-api-settings.bicep' = if (useAPIM) { scope: rg + name: 'apim-api-settings' params: { - name: useAPIM ? apim.outputs.apimServiceName : '' + apiAppName: api.outputs.name apiName: 'todo-api' - apiDisplayName: 'Simple Todo API' - apiDescription: 'This is a simple Todo API' + name: useAPIM ? apim.outputs.name : '' apiPath: 'todo' - webFrontendUrl: web.outputs.SERVICE_WEB_URI - apiBackendUrl: api.outputs.SERVICE_API_URI - apiAppName: api.outputs.SERVICE_API_NAME + applicationInsightsName: applicationInsights.outputs.name } } // Data outputs -output AZURE_COSMOS_CONNECTION_STRING_KEY string = cosmos.outputs.connectionStringKey -output AZURE_COSMOS_DATABASE_NAME string = cosmos.outputs.databaseName +output AZURE_COSMOS_CONNECTION_STRING_KEY string = connectionStringKey +output AZURE_COSMOS_DATABASE_NAME string = !empty(cosmosDatabaseName) ? cosmosDatabaseName: 'Todo' // App outputs -output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString -output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint +output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsights.outputs.connectionString +output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.uri output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId -output API_BASE_URL string = useAPIM ? apimApi.outputs.SERVICE_API_URI : api.outputs.SERVICE_API_URI -output REACT_APP_WEB_BASE_URL string = web.outputs.SERVICE_WEB_URI +output API_BASE_URL string = useAPIM ? 'https://${apim.outputs.name}.azure-api.net/todo' : apiUri +output REACT_APP_WEB_BASE_URL string = webUri output USE_APIM bool = useAPIM -output SERVICE_API_ENDPOINTS array = useAPIM ? [ apimApi.outputs.SERVICE_API_URI, api.outputs.SERVICE_API_URI ]: [] +output SERVICE_API_ENDPOINTS array = useAPIM ? [ 'https://${apim.outputs.name}.azure-api.net/todo', apiUri ]: [] diff --git a/templates/todo/projects/python-mongo/.repo/bicep/repo.yaml b/templates/todo/projects/python-mongo/.repo/bicep/repo.yaml index 891ecee119a..4ee83de91ce 100644 --- a/templates/todo/projects/python-mongo/.repo/bicep/repo.yaml +++ b/templates/todo/projects/python-mongo/.repo/bicep/repo.yaml @@ -90,6 +90,16 @@ repo: patterns: - "apim-api.bicep" + - from: ../../../api/common/openapi.yaml + to: ../src/api/openapi.yaml + patterns: + - "**/main.bicep" + + - from: ../../../common/infra/shared/gateway/apim/apim-api-policy.xml + to: ./app/apim-api-policy.xml + patterns: + - "**/main.bicep" + assets: # Common assets @@ -115,6 +125,12 @@ repo: - from: ../../../../common/infra/bicep/app/cosmos-mongo-db.bicep to: ./infra/app/db.bicep + - from: ../../../../common/infra/bicep/app/applicationinsights-dashboard.bicep + to: ./infra/app/applicationinsights-dashboard.bicep + + - from: ../../../../common/infra/bicep/app/apim-api-settings.bicep + to: ./infra/app/apim-api-settings.bicep + - from: ./../../ to: ./ ignore: