diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index 2f4a98b7408..aacb23b7f7b 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -24,7 +24,9 @@ func (i *Initializer) infraSpecFromDetect( detect detectConfirm) (scaffold.InfraSpec, error) { spec := scaffold.InfraSpec{} for database := range detect.Databases { - if database == appdetect.DbRedis { // no configuration needed for redis + if database == appdetect.DbRedis { + spec.DbRedis = &scaffold.DatabaseRedis{} + // no further configuration needed for redis continue } diff --git a/cli/azd/internal/scaffold/scaffold.go b/cli/azd/internal/scaffold/scaffold.go index 3fed32e7777..4a10e2a1d37 100644 --- a/cli/azd/internal/scaffold/scaffold.go +++ b/cli/azd/internal/scaffold/scaffold.go @@ -115,32 +115,16 @@ func ExecInfra( return err } - if spec.DbCosmosMongo != nil { - err = Execute(t, "db-cosmos-mongo.bicep", spec.DbCosmosMongo, filepath.Join(infraApp, "db-cosmos-mongo.bicep")) - if err != nil { - return fmt.Errorf("scaffolding cosmos mongodb: %w", err) - } - } - - if spec.DbPostgres != nil { - err = Execute(t, "db-postgres.bicep", spec.DbPostgres, filepath.Join(infraApp, "db-postgres.bicep")) - if err != nil { - return fmt.Errorf("scaffolding postgres: %w", err) - } - } - - for _, svc := range spec.Services { - err = Execute(t, "host-containerapp.bicep", svc, filepath.Join(infraApp, svc.Name+".bicep")) - if err != nil { - return fmt.Errorf("scaffolding containerapp: %w", err) - } - } - err = Execute(t, "main.bicep", spec, filepath.Join(infraRoot, "main.bicep")) if err != nil { return fmt.Errorf("scaffolding main.bicep: %w", err) } + err = Execute(t, "resources.bicep", spec, filepath.Join(infraRoot, "resources.bicep")) + if err != nil { + return fmt.Errorf("scaffolding resources.bicep: %w", err) + } + err = Execute(t, "main.parameters.json", spec, filepath.Join(infraRoot, "main.parameters.json")) if err != nil { return fmt.Errorf("scaffolding main.parameters.json: %w", err) diff --git a/cli/azd/internal/scaffold/scaffold_test.go b/cli/azd/internal/scaffold/scaffold_test.go index 89677afe197..775aa924ff6 100644 --- a/cli/azd/internal/scaffold/scaffold_test.go +++ b/cli/azd/internal/scaffold/scaffold_test.go @@ -76,6 +76,51 @@ func TestExecInfra(t *testing.T) { }, }, }, + { + "All", + InfraSpec{ + DbPostgres: &DatabasePostgres{ + DatabaseName: "appdb", + }, + DbCosmosMongo: &DatabaseCosmosMongo{ + DatabaseName: "appdb", + }, + DbRedis: &DatabaseRedis{}, + Services: []ServiceSpec{ + { + Name: "api", + Port: 3100, + Backend: &Backend{ + Frontends: []ServiceReference{ + { + Name: "web", + }, + }, + }, + DbCosmosMongo: &DatabaseReference{ + DatabaseName: "appdb", + }, + DbRedis: &DatabaseReference{ + DatabaseName: "redis", + }, + DbPostgres: &DatabaseReference{ + DatabaseName: "appdb", + }, + }, + { + Name: "web", + Port: 3101, + Frontend: &Frontend{ + Backends: []ServiceReference{ + { + Name: "api", + }, + }, + }, + }, + }, + }, + }, { "API with Postgres", InfraSpec{ @@ -114,6 +159,7 @@ func TestExecInfra(t *testing.T) { { "API with Redis", InfraSpec{ + DbRedis: &DatabaseRedis{}, Services: []ServiceSpec{ { Name: "api", diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index 9788f8c247c..b50afc4fd60 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -12,6 +12,7 @@ type InfraSpec struct { // Databases to create DbPostgres *DatabasePostgres DbCosmosMongo *DatabaseCosmosMongo + DbRedis *DatabaseRedis } type Parameter struct { @@ -30,6 +31,9 @@ type DatabaseCosmosMongo struct { DatabaseName string } +type DatabaseRedis struct { +} + type ServiceSpec struct { Name string Port int diff --git a/cli/azd/resources/scaffold/base/modules/set-redis-conn.bicep b/cli/azd/resources/scaffold/base/modules/set-redis-conn.bicep new file mode 100644 index 00000000000..813f96fbcbf --- /dev/null +++ b/cli/azd/resources/scaffold/base/modules/set-redis-conn.bicep @@ -0,0 +1,29 @@ +param name string +param keyVaultName string +param passwordSecretName string +param urlSecretName string + +resource redisConn 'Microsoft.Cache/redis@2024-03-01' existing = { + name: name +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} + +resource passwordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + name: passwordSecretName + parent: keyVault + properties: { + value: redisConn.listKeys().primaryKey + } +} + +resource urlSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + name: urlSecretName + parent: keyVault + properties: { + value: 'rediss://:${redisConn.listKeys().primaryKey}@${redisConn.properties.hostName}:${redisConn.properties.sslPort}' + } +} + diff --git a/cli/azd/resources/scaffold/base/shared/apps-env.bicep b/cli/azd/resources/scaffold/base/shared/apps-env.bicep deleted file mode 100644 index 030b8233139..00000000000 --- a/cli/azd/resources/scaffold/base/shared/apps-env.bicep +++ /dev/null @@ -1,33 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param logAnalyticsWorkspaceName string -param applicationInsightsName string = '' - -resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2022-10-01' = { - name: name - location: location - tags: tags - properties: { - appLogsConfiguration: { - destination: 'log-analytics' - logAnalyticsConfiguration: { - customerId: logAnalyticsWorkspace.properties.customerId - sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey - } - } - daprAIConnectionString: applicationInsights.properties.ConnectionString - } -} - -resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { - name: logAnalyticsWorkspaceName -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { - name: applicationInsightsName -} - -output name string = containerAppsEnvironment.name -output domain string = containerAppsEnvironment.properties.defaultDomain diff --git a/cli/azd/resources/scaffold/base/shared/dashboard-web.bicep b/cli/azd/resources/scaffold/base/shared/dashboard-web.bicep deleted file mode 100644 index eccce0dbf6b..00000000000 --- a/cli/azd/resources/scaffold/base/shared/dashboard-web.bicep +++ /dev/null @@ -1,1231 +0,0 @@ -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/${applicationInsightsName}' - } - { - 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: applicationInsightsName - 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: applicationInsightsName - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'ResourceId' - value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsightsName}' - } - ] - #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: applicationInsightsName - 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: applicationInsightsName - 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: applicationInsightsName - 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/${applicationInsightsName}' - } - { - 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/${applicationInsightsName}' - } - { - 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: applicationInsightsName - 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: applicationInsightsName - 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/${applicationInsightsName}' - } - 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/${applicationInsightsName}' - } - 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/${applicationInsightsName}' - 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/${applicationInsightsName}' - } - 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/${applicationInsightsName}' - 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/${applicationInsightsName}' - } - 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/${applicationInsightsName}' - 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/${applicationInsightsName}' - } - 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/${applicationInsightsName}' - } - 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/${applicationInsightsName}' - } - 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/${applicationInsightsName}' - } - 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/${applicationInsightsName}' - } - 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/${applicationInsightsName}' - 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/${applicationInsightsName}' - } - 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/${applicationInsightsName}' - } - 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/${applicationInsightsName}' - } - 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/${applicationInsightsName}' - } - 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/${applicationInsightsName}' - } - 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/${applicationInsightsName}' - } - 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/${applicationInsightsName}' - } - 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/${applicationInsightsName}' - } - 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: {} - } - } - ] - } - ] - } -} diff --git a/cli/azd/resources/scaffold/base/shared/keyvault.bicep b/cli/azd/resources/scaffold/base/shared/keyvault.bicep deleted file mode 100644 index f84f7508ddc..00000000000 --- a/cli/azd/resources/scaffold/base/shared/keyvault.bicep +++ /dev/null @@ -1,31 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('Service principal that should be granted read access to the KeyVault. If unset, no service principal is granted access by default') -param principalId string = '' - -var defaultAccessPolicies = !empty(principalId) ? [ - { - objectId: principalId - permissions: { secrets: [ 'get', 'list' ] } - tenantId: subscription().tenantId - } -] : [] - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { - name: name - location: location - tags: tags - properties: { - tenantId: subscription().tenantId - sku: { family: 'A', name: 'standard' } - enabledForTemplateDeployment: true - accessPolicies: union(defaultAccessPolicies, [ - // define access policies here - ]) - } -} - -output endpoint string = keyVault.properties.vaultUri -output name string = keyVault.name diff --git a/cli/azd/resources/scaffold/base/shared/monitoring.bicep b/cli/azd/resources/scaffold/base/shared/monitoring.bicep deleted file mode 100644 index 4ae9796cc3b..00000000000 --- a/cli/azd/resources/scaffold/base/shared/monitoring.bicep +++ /dev/null @@ -1,34 +0,0 @@ -param logAnalyticsName string -param applicationInsightsName string -param location string = resourceGroup().location -param tags object = {} - -resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { - name: logAnalyticsName - location: location - tags: tags - properties: any({ - retentionInDays: 30 - features: { - searchVersion: 1 - } - sku: { - name: 'PerGB2018' - } - }) -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { - name: applicationInsightsName - location: location - tags: tags - kind: 'web' - properties: { - Application_Type: 'web' - WorkspaceResourceId: logAnalytics.id - } -} - -output applicationInsightsName string = applicationInsights.name -output logAnalyticsWorkspaceId string = logAnalytics.id -output logAnalyticsWorkspaceName string = logAnalytics.name diff --git a/cli/azd/resources/scaffold/base/shared/registry.bicep b/cli/azd/resources/scaffold/base/shared/registry.bicep deleted file mode 100644 index d6629f86201..00000000000 --- a/cli/azd/resources/scaffold/base/shared/registry.bicep +++ /dev/null @@ -1,36 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param adminUserEnabled bool = true -param anonymousPullEnabled bool = false -param dataEndpointEnabled bool = false -param encryption object = { - status: 'disabled' -} -param networkRuleBypassOptions string = 'AzureServices' -param publicNetworkAccess string = 'Enabled' -param sku object = { - name: 'Standard' -} -param zoneRedundancy string = 'Disabled' - -// 2023-01-01-preview needed for anonymousPullEnabled -resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' = { - name: name - location: location - tags: tags - sku: sku - properties: { - adminUserEnabled: adminUserEnabled - anonymousPullEnabled: anonymousPullEnabled - dataEndpointEnabled: dataEndpointEnabled - encryption: encryption - networkRuleBypassOptions: networkRuleBypassOptions - publicNetworkAccess: publicNetworkAccess - zoneRedundancy: zoneRedundancy - } -} - -output loginServer string = containerRegistry.properties.loginServer -output name string = containerRegistry.name diff --git a/cli/azd/resources/scaffold/templates/db-cosmos-mongo.bicept b/cli/azd/resources/scaffold/templates/db-cosmos-mongo.bicept deleted file mode 100644 index fed66c10f9f..00000000000 --- a/cli/azd/resources/scaffold/templates/db-cosmos-mongo.bicept +++ /dev/null @@ -1,75 +0,0 @@ -{{define "db-cosmos-mongo.bicep" -}} -param accountName string -param location string = resourceGroup().location -param tags object = {} - -param keyVaultName string - -resource account 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' = { - name: accountName - kind: 'MongoDB' - location: location - tags: tags - properties: { - consistencyPolicy: { defaultConsistencyLevel: 'Session' } - locations: [ - { - locationName: location - failoverPriority: 0 - isZoneRedundant: false - } - ] - databaseAccountOfferType: 'Standard' - enableAutomaticFailover: false - enableMultipleWriteLocations: false - apiProperties: { serverVersion: '4.0' } - capabilities: [ { name: 'EnableServerless' } ] - } -} - -{{- if .DatabaseName}} -resource database 'Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2022-05-15' = { - parent: account - name: '{{.DatabaseName}}' - properties: { - resource: { - id: '{{.DatabaseName}}' - } - } -} -{{- end}} - - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { - name: keyVaultName -} - -resource cosmosConnectionString 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: 'cosmosConnectionString' - properties: { - value: account.listConnectionStrings().connectionStrings[0].connectionString - } -} - -// Typically, collections are automatically created in the application on-demand. -// If you would like to create collections as part of infrastructure provisioning, a commented out reference is provided below. -// var collection1Name = 'Table1' -// resource collection1 'Microsoft.DocumentDb/databaseAccounts/mongodbDatabases/collections@2022-05-15' = { -// parent: database -// name: collection1Name -// properties: { -// resource: { -// id: collection1Name -// shardKey: { _id: 'Hash' } // use hash(_id) as the partition key -// indexes: [ -// // Default index on id -// { key: { keys: ['_id' ]} } -// ] -// } -// } -// } - -output accountName string = account.name -output connectionStringKey string = 'cosmosConnectionString' -{{ end}} diff --git a/cli/azd/resources/scaffold/templates/db-postgres.bicept b/cli/azd/resources/scaffold/templates/db-postgres.bicept deleted file mode 100644 index 54866987449..00000000000 --- a/cli/azd/resources/scaffold/templates/db-postgres.bicept +++ /dev/null @@ -1,80 +0,0 @@ -{{define "db-postgres.bicep" -}} -param serverName string -param location string = resourceGroup().location -param tags object = {} - -param keyVaultName string - -param databaseUser string = 'psqladmin' -param databaseName string = '{{.DatabaseName}}' -@secure() -param databasePassword string - -param allowAllIPsFirewall bool = false - -resource postgreServer'Microsoft.DBforPostgreSQL/flexibleServers@2022-01-20-preview' = { - location: location - tags: tags - name: serverName - sku: { - name: 'Standard_B1ms' - tier: 'Burstable' - } - properties: { - version: '13' - administratorLogin: databaseUser - administratorLoginPassword: databasePassword - storage: { - storageSizeGB: 128 - } - backup: { - backupRetentionDays: 7 - geoRedundantBackup: 'Disabled' - } - highAvailability: { - mode: 'Disabled' - } - maintenanceWindow: { - customWindow: 'Disabled' - dayOfWeek: 0 - startHour: 0 - startMinute: 0 - } - } - - resource firewall_all 'firewallRules' = if (allowAllIPsFirewall) { - name: 'allow-all-IPs' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '255.255.255.255' - } - } -} - -resource database 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2022-01-20-preview' = { - parent: postgreServer - name: databaseName - properties: { - // Azure defaults to UTF-8 encoding, override if required. - // charset: 'string' - // collation: 'string' - } -} - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { - name: keyVaultName -} - -resource dbPasswordKey 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: 'databasePassword' - properties: { - value: databasePassword - } -} - -output databaseHost string = postgreServer.properties.fullyQualifiedDomainName -output databaseName string = databaseName -output databaseUser string = databaseUser -output databaseConnectionKey string = 'databasePassword' -{{ end}} diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept deleted file mode 100644 index 452402f4a96..00000000000 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ /dev/null @@ -1,239 +0,0 @@ -{{define "host-containerapp.bicep" -}} -param name string -param location string = resourceGroup().location -param tags object = {} - -param identityName string -param containerRegistryName string -param containerAppsEnvironmentName string -param applicationInsightsName string -{{- if .DbCosmosMongo}} -@secure() -param cosmosDbConnectionString string -{{- end}} -{{- if .DbPostgres}} -param databaseHost string -param databaseUser string -param databaseName string -@secure() -param databasePassword string -{{- end}} -{{- if .DbRedis}} -param redisName string -{{- end}} -{{- if (and .Frontend .Frontend.Backends)}} -param apiUrls array -{{- end}} -{{- if (and .Backend .Backend.Frontends)}} -param allowedOrigins array -{{- end}} -param exists bool -@secure() -param appDefinition object - -var appSettingsArray = filter(array(appDefinition.settings), i => i.name != '') -var secrets = map(filter(appSettingsArray, i => i.?secret != null), i => { - name: i.name - value: i.value - secretRef: i.?secretRef ?? take(replace(replace(toLower(i.name), '_', '-'), '.', '-'), 32) -}) -var env = map(filter(appSettingsArray, i => i.?secret == null), i => { - name: i.name - value: i.value -}) - -resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { - name: identityName - location: location -} - -resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = { - name: containerRegistryName -} - -resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' existing = { - name: containerAppsEnvironmentName -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { - name: applicationInsightsName -} - -resource acrPullRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - scope: containerRegistry - name: guid(subscription().id, resourceGroup().id, identity.id, 'acrPullRole') - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') - principalType: 'ServicePrincipal' - principalId: identity.properties.principalId - } -} - -module fetchLatestImage '../modules/fetch-container-image.bicep' = { - name: '${name}-fetch-image' - params: { - exists: exists - name: name - } -} -{{- if .DbRedis}} - -resource redis 'Microsoft.App/containerApps@2023-05-02-preview' = { - name: redisName - location: location - properties: { - environmentId: containerAppsEnvironment.id - configuration: { - service: { - type: 'redis' - } - } - template: { - containers: [ - { - image: 'redis' - name: 'redis' - } - ] - } - } -} -{{- end}} - -resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { - name: name - location: location - tags: union(tags, {'azd-service-name': '{{.Name}}' }) - dependsOn: [ acrPullRole ] - identity: { - type: 'UserAssigned' - userAssignedIdentities: { '${identity.id}': {} } - } - properties: { - managedEnvironmentId: containerAppsEnvironment.id - configuration: { - {{- if ne .Port 0}} - ingress: { - external: true - targetPort: {{.Port}} - transport: 'auto' - {{- if (and .Backend .Backend.Frontends)}} - corsPolicy: { - allowedOrigins: union(allowedOrigins, [ - // define additional allowed origins here - ]) - } - {{- end}} - } - {{- end}} - registries: [ - { - server: '${containerRegistryName}.azurecr.io' - identity: identity.id - } - ] - secrets: union([ - {{- if .DbCosmosMongo}} - { - name: 'azure-cosmos-connection-string' - value: cosmosDbConnectionString - } - {{- end}} - {{- if .DbPostgres}} - { - name: 'db-pass' - value: databasePassword - } - {{- end}} - ], - map(secrets, secret => { - name: secret.secretRef - value: secret.value - })) - } - template: { - containers: [ - { - image: fetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' - name: 'main' - env: union([ - { - name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' - value: applicationInsights.properties.ConnectionString - } - {{- if .DbCosmosMongo}} - { - name: 'AZURE_COSMOS_MONGODB_CONNECTION_STRING' - secretRef: 'azure-cosmos-connection-string' - } - {{- end}} - {{- if .DbPostgres}} - { - name: 'POSTGRES_HOST' - value: databaseHost - } - { - name: 'POSTGRES_USERNAME' - value: databaseUser - } - { - name: 'POSTGRES_DATABASE' - value: databaseName - } - { - name: 'POSTGRES_PASSWORD' - secretRef: 'db-pass' - } - { - name: 'POSTGRES_PORT' - value: '5432' - } - {{- end}} - {{- if .Frontend}} - {{- range $i, $e := .Frontend.Backends}} - { - name: '{{upper .Name}}_BASE_URL' - value: apiUrls[{{$i}}] - } - {{- end}} - {{- end}} - {{- if ne .Port 0}} - { - name: 'PORT' - value: '{{ .Port }}' - } - {{- end}} - ], - env, - map(secrets, secret => { - name: secret.name - secretRef: secret.secretRef - })) - resources: { - cpu: json('1.0') - memory: '2.0Gi' - } - } - ] - {{- if .DbRedis}} - serviceBinds: [ - { - serviceId: redis.id - name: 'redis' - } - ] - {{- end}} - scale: { - minReplicas: 1 - maxReplicas: 10 - } - } - } -} - -output defaultDomain string = containerAppsEnvironment.properties.defaultDomain -output name string = app.name -output uri string = 'https://${app.properties.configuration.ingress.fqdn}' -output id string = app.id -{{ end}} diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index f2aefaaca33..6a574ab55e1 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -28,150 +28,35 @@ var tags = { 'azd-env-name': environmentName } -var abbrs = loadJsonContent('./abbreviations.json') -var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) - -resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = { +// Organize resources in a resource group +resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { name: 'rg-${environmentName}' location: location tags: tags } -module monitoring './shared/monitoring.bicep' = { - name: 'monitoring' - params: { - location: location - tags: tags - logAnalyticsName: '${abbrs.operationalInsightsWorkspaces}${resourceToken}' - applicationInsightsName: '${abbrs.insightsComponents}${resourceToken}' - } - scope: rg -} - -module dashboard './shared/dashboard-web.bicep' = { - name: 'dashboard' - params: { - name: '${abbrs.portalDashboards}${resourceToken}' - applicationInsightsName: monitoring.outputs.applicationInsightsName - location: location - tags: tags - } +module resources 'resources.bicep' = { scope: rg -} - -module registry './shared/registry.bicep' = { - name: 'registry' + name: 'resources' params: { location: location tags: tags - name: '${abbrs.containerRegistryRegistries}${resourceToken}' - } - scope: rg -} - -module keyVault './shared/keyvault.bicep' = { - name: 'keyvault' - params: { - location: location - tags: tags - name: '${abbrs.keyVaultVaults}${resourceToken}' principalId: principalId +{{- range .Parameters}} + {{.Name}}: {{.Name}} +{{- end }} } - scope: rg -} - -module appsEnv './shared/apps-env.bicep' = { - name: 'apps-env' - params: { - name: '${abbrs.appManagedEnvironments}${resourceToken}' - location: location - tags: tags - applicationInsightsName: monitoring.outputs.applicationInsightsName - logAnalyticsWorkspaceName: monitoring.outputs.logAnalyticsWorkspaceName - } - scope: rg } -{{- if (or .DbCosmosMongo .DbPostgres)}} -resource vault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { - name: keyVault.outputs.name - scope: rg -} +{{- if .Services}} +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT +output AZURE_KEY_VAULT_ENDPOINT string = resources.outputs.AZURE_KEY_VAULT_ENDPOINT +output AZURE_KEY_VAULT_NAME string = resources.outputs.AZURE_KEY_VAULT_NAME {{- end}} -{{- if .DbCosmosMongo}} - -module cosmosDb './app/db-cosmos-mongo.bicep' = { - name: 'cosmosDb' - params: { - accountName: '${abbrs.documentDBDatabaseAccounts}${resourceToken}' - location: location - tags: tags - keyVaultName: keyVault.outputs.name - } - scope: rg -} +{{- if .DbRedis}} +output AZURE_CACHE_REDIS_ID string = resources.outputs.AZURE_CACHE_REDIS_ID {{- end}} {{- if .DbPostgres}} - -module postgresDb './app/db-postgres.bicep' = { - name: 'postgresDb' - params: { - serverName: '${abbrs.dBforPostgreSQLServers}${resourceToken}' - location: location - tags: tags - databasePassword: databasePassword - keyVaultName: keyVault.outputs.name - allowAllIPsFirewall: true - } - scope: rg -} +output AZURE_POSTGRES_FLEXIBLE_SERVER_ID string = resources.outputs.AZURE_POSTGRES_FLEXIBLE_SERVER_ID {{- end}} -{{- range .Services}} - -module {{bicepName .Name}} './app/{{.Name}}.bicep' = { - name: '{{.Name}}' - params: { - name: '{{containerAppName .Name}}' - location: location - tags: tags - identityName: '${abbrs.managedIdentityUserAssignedIdentities}{{.Name}}-${resourceToken}' - applicationInsightsName: monitoring.outputs.applicationInsightsName - containerAppsEnvironmentName: appsEnv.outputs.name - containerRegistryName: registry.outputs.name - exists: {{bicepName .Name}}Exists - appDefinition: {{bicepName .Name}}Definition - {{- if .DbRedis}} - redisName: 'rd-{{containerAppName .Name}}' - {{- end}} - {{- if .DbCosmosMongo}} - cosmosDbConnectionString: vault.getSecret(cosmosDb.outputs.connectionStringKey) - {{- end}} - {{- if .DbPostgres}} - databaseName: postgresDb.outputs.databaseName - databaseHost: postgresDb.outputs.databaseHost - databaseUser: postgresDb.outputs.databaseUser - databasePassword: vault.getSecret(postgresDb.outputs.databaseConnectionKey) - {{- end}} - {{- if (and .Frontend .Frontend.Backends)}} - apiUrls: [ - {{- range .Frontend.Backends}} - {{bicepName .Name}}.outputs.uri - {{- end}} - ] - {{- end}} - {{- if (and .Backend .Backend.Frontends)}} - allowedOrigins: [ - {{- range .Backend.Frontends}} - 'https://{{containerAppName .Name}}.${appsEnv.outputs.domain}' - {{- end}} - ] - {{- end}} - } - scope: rg -} -{{- end}} - -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = registry.outputs.loginServer -output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name -output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint {{ end}} diff --git a/cli/azd/resources/scaffold/templates/next-steps.mdt b/cli/azd/resources/scaffold/templates/next-steps.mdt index 14be7cbbbfe..7fe72dec118 100644 --- a/cli/azd/resources/scaffold/templates/next-steps.mdt +++ b/cli/azd/resources/scaffold/templates/next-steps.mdt @@ -24,15 +24,20 @@ Configure environment variables for running services by updating `settings` in [ {{- if or .DbPostgres .DbCosmosMongo .DbRedis }} #### Database connections for `{{.Name}}` + +The following environment variables are set for `{{.Name}}` in [resources.bicep](./infra/resources.bicep). +They allow connection to the database instances, and can be modified or adapted to your service's needs: {{ end}} {{- if .DbPostgres }} -- `POSTGRES_*` environment variables are configured in [{{.Name}}.bicep](./infra/app/{{.Name}}.bicep) to connect to the Postgres database. Modify these variables to match your application's needs. +- `POSTGRES_URL` - The URL of the Azure Postgres Flexible Server database instance. +Individual components are also available as: `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DATABASE`, `POSTGRES_USERNAME`, `POSTGRES_PASSWORD`. {{- end}} {{- if .DbCosmosMongo }} -- `AZURE_COSMOS_MONGODB_CONNECTION_STRING` environment variable is configured in [{{.Name}}.bicep](./infra/app/{{.Name}}.bicep) to connect to the MongoDB database. Modify this variable to match your application's needs. +- `MONGODB_URL` - The URL of the Azure Cosmos DB (MongoDB) instance. {{- end}} {{- if .DbRedis }} -- Environment variables for connecting to Redis are available implicitly as: `REDIS_ENDPOINT`, `REDIS_HOST`, `REDIS_PASSWORD`, and `REDIS_PORT`. +- `REDIS_URL` - The URL of the Azure Cache for Redis instance. +Individual components are also available as: `REDIS_HOST`, `REDIS_PASSWORD`, and `REDIS_PORT`. {{- end}} {{- end}} @@ -50,27 +55,28 @@ Configure environment variables for running services by updating `settings` in [ To describe the infrastructure and application, `azure.yaml` along with Infrastructure as Code files using Bicep were added with the following directory structure: ```yaml -- azure.yaml # azd project configuration -- infra/ # Infrastructure as Code (bicep) files - - main.bicep # main deployment module - - app/ # Application resource modules - - shared/ # Shared resource modules - - modules/ # Library modules +- azure.yaml # azd project configuration +- infra/ # Infrastructure-as-code Bicep files + - main.bicep # Subscription level resources + - resources.bicep # Primary resource group resources + - modules/ # Library modules ``` -Each bicep file declares resources to be provisioned. The resources are provisioned when running `azd up` or `azd provision`. +The resources declared in [resources.bicep](./infra/resources.bicep) are provisioned when running `azd up` or `azd provision`. +This includes: + {{range .Services}} -- [app/{{.Name}}.bicep](./infra/app/{{.Name}}.bicep) - Azure Container Apps resources to host the '{{.Name}}' service. +- Azure Container App to host the '{{.Name}}' service. {{- end}} {{- if .DbPostgres}} -- [app/db-postgre.bicep](./infra/app/db-postgre.bicep) - Azure Postgres Flexible Server to host the '{{.DbPostgres.DatabaseName}}' database. +- Azure Postgres Flexible Server to host the '{{.DbPostgres.DatabaseName}}' database. {{- end}} {{- if .DbCosmosMongo}} -- [app/db-cosmos.bicep](./infra/app/db-cosmos.bicep) - Azure Cosmos DB (MongoDB) to host the '{{.DbCosmosMongo.DatabaseName}}' database. +- Azure Cosmos DB (MongoDB) to host the '{{.DbCosmosMongo.DatabaseName}}' database. +{{- end}} +{{- if .DbRedis}} +- Azure Cache for Redis to host the redis database. {{- end}} -- [shared/keyvault.bicep](./infra/shared/keyvault.bicep) - Azure KeyVault to store secrets. -- [shared/monitoring.bicep](./infra/shared/monitoring.bicep) - Azure Log Analytics workspace and Application Insights to log and store instrumentation logs. -- [shared/registry.bicep](./infra/shared/registry.bicep) - Azure Container Registry to store docker images. More information about [Bicep](https://aka.ms/bicep) language. diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept new file mode 100644 index 00000000000..7d39c83c534 --- /dev/null +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -0,0 +1,395 @@ +{{define "resources.bicep" -}} +@description('The location used for all deployed resources') +param location string = resourceGroup().location + +@description('Tags that will be applied to all resources') +param tags object = {} + +{{range .Parameters}} +{{- if .Secret}} +@secure() +{{- end}} +param {{.Name}} {{.Type}} +{{- end}} + +@description('Id of the user or app to assign application roles') +param principalId string + +var abbrs = loadJsonContent('./abbreviations.json') +var resourceToken = uniqueString(subscription().id, resourceGroup().id, location) + +{{- if .Services }} + +// Monitor application with Azure Monitor +module monitoring 'br/public:avm/ptn/azd/monitoring:0.1.0' = { + name: 'monitoring' + params: { + logAnalyticsName: '${abbrs.operationalInsightsWorkspaces}${resourceToken}' + applicationInsightsName: '${abbrs.insightsComponents}${resourceToken}' + applicationInsightsDashboardName: '${abbrs.portalDashboards}${resourceToken}' + location: location + tags: tags + } +} + +// Container registry +module containerRegistry 'br/public:avm/res/container-registry/registry:0.1.1' = { + name: 'registry' + params: { + name: '${abbrs.containerRegistryRegistries}${resourceToken}' + location: location + acrAdminUserEnabled: true + tags: tags + publicNetworkAccess: 'Enabled' + roleAssignments:[ + {{- range .Services}} + { + principalId: {{bicepName .Name}}Identity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + } + {{- end}} + ] + } +} + +// Container apps environment +module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.4.5' = { + name: 'container-apps-environment' + params: { + logAnalyticsWorkspaceResourceId: monitoring.outputs.logAnalyticsWorkspaceResourceId + name: '${abbrs.appManagedEnvironments}${resourceToken}' + location: location + zoneRedundant: false + } +} +{{- end}} + +{{- if .DbCosmosMongo}} +module cosmos 'br/public:avm/res/document-db/database-account:0.4.0' = { + name: 'cosmos' + params: { + tags: tags + locations: [ + { + failoverPriority: 0 + isZoneRedundant: false + locationName: location + } + ] + name: '${abbrs.documentDBDatabaseAccounts}${resourceToken}' + location: location + {{- if .DbCosmosMongo.DatabaseName}} + mongodbDatabases: [ + { + name: '{{ .DbCosmosMongo.DatabaseName }}' + } + ] + {{- end}} + secretsKeyVault: { + keyVaultName: keyVault.outputs.name + primaryWriteConnectionStringSecretName: 'MONGODB-URL' + } + capabilitiesToAdd: [ 'EnableServerless' ] + } +} +{{- end}} + +{{- if .DbPostgres}} +var databaseName = '{{ .DbPostgres.DatabaseName }}' +var databaseUser = 'psqladmin' +module postgreServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.1.4' = { + name: 'postgreServer' + params: { + // Required parameters + name: '${abbrs.dBforPostgreSQLServers}${resourceToken}' + skuName: 'Standard_B1ms' + tier: 'Burstable' + // Non-required parameters + administratorLogin: databaseUser + administratorLoginPassword: databasePassword + geoRedundantBackup: 'Disabled' + passwordAuth:'Enabled' + firewallRules: [ + { + name: 'AllowAllIps' + startIpAddress: '0.0.0.0' + endIpAddress: '255.255.255.255' + } + ] + databases: [ + { + name: databaseName + } + ] + location: location + } +} +{{- end}} + +{{- range .Services}} + +module {{bicepName .Name}}Identity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { + name: '{{bicepName .Name}}identity' + params: { + name: '${abbrs.managedIdentityUserAssignedIdentities}{{bicepName .Name}}-${resourceToken}' + location: location + } +} + +module {{bicepName .Name}}FetchLatestImage './modules/fetch-container-image.bicep' = { + name: '{{bicepName .Name}}-fetch-image' + params: { + exists: {{bicepName .Name}}Exists + name: '{{.Name}}' + } +} + +var {{bicepName .Name}}AppSettingsArray = filter(array({{bicepName .Name}}Definition.settings), i => i.name != '') +var {{bicepName .Name}}Secrets = map(filter({{bicepName .Name}}AppSettingsArray, i => i.?secret != null), i => { + name: i.name + value: i.value + secretRef: i.?secretRef ?? take(replace(replace(toLower(i.name), '_', '-'), '.', '-'), 32) +}) +var {{bicepName .Name}}Env = map(filter({{bicepName .Name}}AppSettingsArray, i => i.?secret == null), i => { + name: i.name + value: i.value +}) + +module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { + name: '{{bicepName .Name}}' + params: { + name: '{{.Name}}' + {{- if ne .Port 0}} + ingressTargetPort: {{.Port}} + {{- end}} + {{- if (and .Backend .Backend.Frontends)}} + corsPolicy: { + allowedOrigins: [ + {{- range .Backend.Frontends}} + 'https://{{.Name}}.${containerAppsEnvironment.outputs.defaultDomain}' + {{- end}} + ] + } + {{- end}} + scaleMinReplicas: 1 + scaleMaxReplicas: 10 + secrets: { + secureList: union([ + {{- if .DbCosmosMongo}} + { + name: 'mongodb-url' + identity:{{bicepName .Name}}Identity.outputs.resourceId + keyVaultUrl: '${keyVault.outputs.uri}secrets/MONGODB-URL' + } + {{- end}} + {{- if .DbPostgres}} + { + name: 'db-pass' + value: databasePassword + } + { + name: 'db-url' + value: 'postgresql://${databaseUser}:${databasePassword}@${postgreServer.outputs.fqdn}:5432/${databaseName}' + } + {{- end}} + {{- if .DbRedis}} + { + name: 'redis-pass' + identity:{{bicepName .Name}}Identity.outputs.resourceId + keyVaultUrl: '${keyVault.outputs.uri}secrets/REDIS-PASSWORD' + } + { + name: 'redis-url' + identity:{{bicepName .Name}}Identity.outputs.resourceId + keyVaultUrl: '${keyVault.outputs.uri}secrets/REDIS-URL' + } + {{- end}} + ], + map({{bicepName .Name}}Secrets, secret => { + name: secret.secretRef + value: secret.value + })) + } + containers: [ + { + image: {{bicepName .Name}}FetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' + name: 'main' + resources: { + cpu: json('0.5') + memory: '1.0Gi' + } + env: union([ + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: monitoring.outputs.applicationInsightsConnectionString + } + { + name: 'AZURE_CLIENT_ID' + value: {{bicepName .Name}}Identity.outputs.clientId + } + {{- if .DbCosmosMongo}} + { + name: 'MONGODB_URL' + secretRef: 'mongodb-url' + } + {{- end}} + {{- if .DbPostgres}} + { + name: 'POSTGRES_HOST' + value: postgreServer.outputs.fqdn + } + { + name: 'POSTGRES_USERNAME' + value: databaseUser + } + { + name: 'POSTGRES_DATABASE' + value: databaseName + } + { + name: 'POSTGRES_PASSWORD' + secretRef: 'db-pass' + } + { + name: 'POSTGRES_URL' + secretRef: 'db-url' + } + { + name: 'POSTGRES_PORT' + value: '5432' + } + {{- end}} + {{- if .DbRedis}} + { + name: 'REDIS_HOST' + value: redis.outputs.hostName + } + { + name: 'REDIS_PORT' + value: string(redis.outputs.sslPort) + } + { + name: 'REDIS_URL' + secretRef: 'redis-url' + } + { + name: 'REDIS_ENDPOINT' + value: '${redis.outputs.hostName}:${string(redis.outputs.sslPort)}' + } + { + name: 'REDIS_PASSWORD' + secretRef: 'redis-pass' + } + {{- end}} + {{- if .Frontend}} + {{- range $i, $e := .Frontend.Backends}} + { + name: '{{upper .Name}}_BASE_URL' + value: 'https://{{.Name}}.internal.${containerAppsEnvironment.outputs.defaultDomain}' + } + {{- end}} + {{- end}} + {{- if ne .Port 0}} + { + name: 'PORT' + value: '{{ .Port }}' + } + {{- end}} + ], + {{bicepName .Name}}Env, + map({{bicepName .Name}}Secrets, secret => { + name: secret.name + secretRef: secret.secretRef + })) + } + ] + managedIdentities:{ + systemAssigned: false + userAssignedResourceIds: [{{bicepName .Name}}Identity.outputs.resourceId] + } + registries:[ + { + server: containerRegistry.outputs.loginServer + identity: {{bicepName .Name}}Identity.outputs.resourceId + } + ] + environmentResourceId: containerAppsEnvironment.outputs.resourceId + location: location + tags: union(tags, { 'azd-service-name': '{{.Name}}' }) + } +} +{{- end}} + +{{- if .DbRedis}} +module redis 'br/public:avm/res/cache/redis:0.3.2' = { + name: 'redisDeployment' + params: { + // Required parameters + name: '${abbrs.cacheRedis}${resourceToken}' + // Non-required parameters + location: location + skuName: 'Basic' + } +} + +module redisConn './modules/set-redis-conn.bicep' = { + name: 'redisConn' + params: { + name: redis.outputs.name + passwordSecretName: 'REDIS-PASSWORD' + urlSecretName: 'REDIS-URL' + keyVaultName: keyVault.outputs.name + } +} +{{- end}} + +{{- if .Services}} +// Create a keyvault to store secrets +module keyVault 'br/public:avm/res/key-vault/vault:0.6.1' = { + name: 'keyvault' + params: { + name: '${abbrs.keyVaultVaults}${resourceToken}' + location: location + tags: tags + enableRbacAuthorization: false + accessPolicies: [ + { + objectId: principalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + {{- range .Services}} + { + objectId: {{bicepName .Name}}Identity.outputs.principalId + permissions: { + secrets: [ 'get', 'list' ] + } + } + {{- end}} + ] + secrets: [ + {{- if .DbPostgres}} + { + name: 'db-pass' + value: databasePassword + } + {{- end}} + ] + } +} +{{- end}} + +{{- if .Services}} +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer +output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.uri +output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name +{{- end}} +{{- if .DbRedis}} +output AZURE_CACHE_REDIS_ID string = redis.outputs.resourceId +{{- end}} +{{- if .DbPostgres}} +output AZURE_POSTGRES_FLEXIBLE_SERVER_ID string = postgreServer.outputs.resourceId +{{- end}} +{{ end}} diff --git a/cli/azd/test/functional/init_test.go b/cli/azd/test/functional/init_test.go index 4d7e7e96282..db81819476a 100644 --- a/cli/azd/test/functional/init_test.go +++ b/cli/azd/test/functional/init_test.go @@ -205,7 +205,7 @@ func Test_CLI_Init_From_App(t *testing.T) { require.NoError(t, err) require.FileExists(t, filepath.Join(dir, "infra", "main.bicep")) + require.FileExists(t, filepath.Join(dir, "infra", "main.parameters.json")) + require.FileExists(t, filepath.Join(dir, "infra", "resources.bicep")) require.FileExists(t, filepath.Join(dir, "azure.yaml")) - require.FileExists(t, filepath.Join(dir, "infra", "app", "app.bicep")) - require.FileExists(t, filepath.Join(dir, "infra", "app", "db-postgres.bicep")) }