From f8b7b5c5fe84b2cca0a5bf15563d5b6f97332b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20Collin=20L=C3=B8kken?= <43639886+collinlokken@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:41:40 +0100 Subject: [PATCH] ci: add bicep files for resource setup --- IaC/app-registration.bicep | 85 ++++++++++++ IaC/bicepconfig.json | 5 + IaC/exceptionEmailNotification.bicep | 68 ++++++++++ IaC/main.bicep | 24 ++++ IaC/resources.bicep | 192 +++++++++++++++++++++++++++ 5 files changed, 374 insertions(+) create mode 100644 IaC/app-registration.bicep create mode 100644 IaC/bicepconfig.json create mode 100644 IaC/exceptionEmailNotification.bicep create mode 100644 IaC/main.bicep create mode 100644 IaC/resources.bicep diff --git a/IaC/app-registration.bicep b/IaC/app-registration.bicep new file mode 100644 index 00000000..4c4fdd1b --- /dev/null +++ b/IaC/app-registration.bicep @@ -0,0 +1,85 @@ +extension microsoftGraph +param applicationName string = 'template-fastapi-react' +param repositoryName string = 'template-fastapi-react' + +// The Entra ID application +// Resource format https://learn.microsoft.com/en-us/graph/templates/reference/applications?view=graph-bicep-1.0 +resource app 'Microsoft.Graph/applications@v1.0' = { + displayName: '${applicationName}' + signInAudience: 'AzureADMyOrg' + uniqueName: '${applicationName}' + spa: { + // The callback URL is the URL that the user is redirected to after the login, + // and it contains the URL of the application that is registered in Radix and localhost for doing development. + redirectUris: [ + // Development + 'https://proxy-${applicationName}-dev.radix.equinor.com/api/docs/oauth2-redirect' + 'https://proxy-${applicationName}-dev.radix.equinor.com' + 'https://proxy-${applicationName}-dev.radix.equinor.com/' + // Staging + 'https://proxy-${applicationName}-staging.radix.equinor.com/api/docs/oauth2-redirect' + 'https://proxy-${applicationName}-staging.radix.equinor.com' + 'https://proxy-${applicationName}-staging.radix.equinor.com/' + // Production + 'https://${applicationName}.app.radix.equinor.com/api/docs/oauth2-redirect' + 'https://proxy-${applicationName}-prod.radix.equinor.com/' + 'https://${applicationName}.app.radix.equinor.com/' + 'https://${applicationName}.app.radix.equinor.com' + // For development + 'http://localhost/api/docs/oauth2-redirect' + 'http://localhost/' + 'http://localhost:5000/docs/oauth2-redirect' + ] + } + api: { + // In version 2 the audience is always the client id, and does not contain the api:// in the decoded JWT. + // It is important to know this because the API expects a JWT token with a specific signature for validation, + // and this is specified in the configuration settings and must match. + requestedAccessTokenVersion: 2 + // To allow OpenAPI and clients to talk to the API, we need to add the scope to the API. + oauth2PermissionScopes: [ + { + id: '31a61854-0d6d-4c60-918b-efffd4fac373' + adminConsentDescription: 'Allow users to access the API' + adminConsentDisplayName: 'Read' + isEnabled: true + type: 'User' + userConsentDescription: 'Access the API' + userConsentDisplayName: 'Access the API' + value: 'api${app.appId}' + } + ] + } + appRoles: [ + { + id: '31a61854-0d6d-4c60-918b-efffd4fac379' + allowedMemberTypes: [ + 'User' + 'Application' + ] + description: '${applicationName} administrators. Access to all fields. Permission to edit admin values.' + displayName: 'Admin' + isEnabled: true + value: 'admin' + } + ] + // Resource format https://learn.microsoft.com/en-us/graph/templates/reference/federatedidentitycredentials?view=graph-bicep-1.0 + resource githubFic 'federatedIdentityCredentials' = { + name: '${app.uniqueName}/githubFic' + audiences: [ + 'api://AzureADTokenExchange' + ] + description: 'Federated Identity Credentials for Github Actions to access Entra protected resources' + issuer: 'https://token.actions.githubusercontent.com' + // Subject is checked before issuing an Entra ID access token to access Azure resources. + // GitHub Actions subject examples can be found in https://docs.github.com/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#example-subject-claims + subject: 'repo:equinor/${repositoryName}:ref:refs/heads/main' + } +} + +// The Service Principle (or Enterprise App) +resource appSP 'Microsoft.Graph/servicePrincipals@v1.0' = { + appId: app.appId + displayName: '${applicationName}' + +} diff --git a/IaC/bicepconfig.json b/IaC/bicepconfig.json new file mode 100644 index 00000000..3ce62216 --- /dev/null +++ b/IaC/bicepconfig.json @@ -0,0 +1,5 @@ +{ + "experimentalFeaturesEnabled": { + "extensibility": true + } +} diff --git a/IaC/exceptionEmailNotification.bicep b/IaC/exceptionEmailNotification.bicep new file mode 100644 index 00000000..135469a7 --- /dev/null +++ b/IaC/exceptionEmailNotification.bicep @@ -0,0 +1,68 @@ +/* Example of separate deployment that uses existing resource. +E.g.: sending email notifications on exceptions in (existing) Application Insights. +Needs to be deployed on resource group level: +az deployment group create --resource-group template-fastapi-react-dev --template-file ./exceptionEmailNotifications.bicep --parameters environment=staging +*/ +resource appInsight 'Microsoft.Insights/components@2020-02-02' existing = { + name: '${resourceGroup().name}-logs' +} + +resource sendEmailActionGroup 'Microsoft.Insights/actionGroups@2023-01-01' = { + name: 'send-email-action-group' + location: 'global' + properties: { + groupShortName: 'ErrorNotify' + enabled: true + emailReceivers: [ + { + name: 'Notify Chris by email_-EmailAction-' + emailAddress: 'chcl@equinor.com' + useCommonAlertSchema: false + } + { + name: 'Notify Eirik by email_-EmailAction-' + emailAddress: 'eaks@equinor.com' + useCommonAlertSchema: false + } + ] + } +} + + +resource metricAlerts 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'Send email on error in template-fastapi-react' + location: 'global' + properties: { + description: 'When an error is detected in template-fastapi-react, an email is dispatched' + severity: 1 + enabled: true + scopes: [ + appInsight.id + ] + evaluationFrequency: 'PT1H' + windowSize: 'PT1H' + criteria: { + allOf: [ + { + threshold: 0 + name: 'Metric1' + metricNamespace: 'microsoft.insights/components' + metricName: 'exceptions/count' + operator: 'GreaterThan' + timeAggregation: 'Count' + skipMetricValidation: false + criterionType: 'StaticThresholdCriterion' + } + ] + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + } + autoMitigate: false + targetResourceType: 'microsoft.insights/components' + targetResourceRegion: 'norwayeast' + actions: [ + { + actionGroupId: sendEmailActionGroup.id + } + ] + } +} diff --git a/IaC/main.bicep b/IaC/main.bicep new file mode 100644 index 00000000..6f23f8f5 --- /dev/null +++ b/IaC/main.bicep @@ -0,0 +1,24 @@ +targetScope='subscription' + +@allowed([ 'dev', 'staging', 'prod' ]) +param environment string +@description('Specifies the location for resources.') +param resourceGroupLocation string = 'norwayeast' +@description('Create admin password for the database. Will be stored in the KeyVault') +@secure() +param postgresDBPassword string + +resource newRG 'Microsoft.Resources/resourceGroups@2024-03-01' = { + name: 'template-fastapi-react-${environment}' + location: resourceGroupLocation +} + +module resources 'resources.bicep' = { + name: 'template-fastapi-react-${environment}-resources' + scope: newRG + params: { + storageLocation: resourceGroupLocation + environment: environment + postgresDBPassword: postgresDBPassword + } +} diff --git a/IaC/resources.bicep b/IaC/resources.bicep new file mode 100644 index 00000000..b001f3a0 --- /dev/null +++ b/IaC/resources.bicep @@ -0,0 +1,192 @@ +param storageLocation string +param environment string +@secure() +param postgresDBPassword string + +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: 'template-fastapi-react-${environment}-logWorkspace' + location: storageLocation + properties: { + publicNetworkAccessForQuery: 'Enabled' + publicNetworkAccessForIngestion: 'Enabled' + forceCmkForQuery: false + sku: { + name: 'pergb2018' + } + retentionInDays: environment == 'prod' ? 730 : 90 + workspaceCapping: { + dailyQuotaGb: environment == 'prod' ? 10 : 1 + } + } +} + +resource appInsight 'Microsoft.Insights/components@2020-02-02' = { + name: 'template-fastapi-react-${environment}-logs' + location: storageLocation + kind: 'web' + properties: { + Application_Type: 'web' + Flow_Type: 'Bluefield' + IngestionMode: 'LogAnalytics' + publicNetworkAccessForIngestion: 'Enabled' + publicNetworkAccessForQuery: 'Enabled' + Request_Source: 'rest' + RetentionInDays: environment == 'prod' ? 730 : 90 + WorkspaceResourceId: logAnalyticsWorkspace.id + } +} + +resource queryPack 'Microsoft.OperationalInsights/queryPacks@2019-09-01' = { + location: storageLocation + name: 'template-fastapi-react-${environment}-queryPack' + properties: { + + } +} + + +resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = { + name: 'template-fastapi-react-${environment}-keyVault' + location: storageLocation + properties: { + tenantId: '3aa4a235-b6e2-48d5-9195-7fcf05b459b0' + softDeleteRetentionInDays: 30 + enabledForDeployment: true + enableSoftDelete: true + accessPolicies: [] // Grant each user explicit access after the vault has been created + sku: { + name: 'standard' + family: 'A' + } + publicNetworkAccess: 'Disabled' + } +} + +resource databasePassword 'Microsoft.KeyVault/vaults/secrets@2024-04-01-preview' = { + parent: keyVault + name: 'template-fastapi-react-database-${environment}-password' + properties: { + value: postgresDBPassword + } +} + + +resource sqlServer 'Microsoft.DBforPostgreSQL/flexibleServers@2023-12-01-preview' = { + name: 'template-fastapi-react-${environment}-database' + location: storageLocation + sku: { + name: 'Standard_B1ms' + tier: 'Burstable' + } + properties: { + version: '16' + administratorLogin: 'template-fastapi-react' + administratorLoginPassword: postgresDBPassword + maintenanceWindow: { + customWindow: 'Enabled' + dayOfWeek: 0 + startHour: 3 + startMinute: 18 + } + network:{publicNetworkAccess: 'Enabled'} + highAvailability: { + mode: 'Disabled' + } + storage: { + storageSizeGB: 64 + type: 'Premium_LRS' + } + backup: { + backupRetentionDays: 7 + geoRedundantBackup: 'Disabled' + } + } +} + +resource template-fastapi-reactDatabase 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2023-12-01-preview' = { + name: 'template-fastapi-react' + parent: sqlServer + properties: { + charset: 'UTF8' + collation: 'en_US.utf8' + } +} + + +resource databaseAllowRadixConnection 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2023-12-01-preview' = { + name: 'allow-radix-connection' + parent: sqlServer + properties: { + startIpAddress: '52.178.214.192' + endIpAddress: '52.178.214.199' + } +} + +resource databaseAllowRadixConnection2 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2023-12-01-preview' = { + name: 'allow-radix-connection2' + parent: sqlServer + properties: { + startIpAddress: '137.135.191.80' + endIpAddress: '137.135.191.95' + } +} + +resource sendEmailActionGroup 'Microsoft.Insights/actionGroups@2023-01-01' = { + name: 'send-email-action-group' + location: 'global' + properties: { + groupShortName: 'ErrorNotify' + enabled: true + emailReceivers: [ + { + name: 'Notify Chris by email_-EmailAction-' + emailAddress: 'chcl@equinor.com' + useCommonAlertSchema: false + } + { + name: 'Notify Eirik by email_-EmailAction-' + emailAddress: 'eaks@equinor.com' + useCommonAlertSchema: false + } + ] + } +} + + +resource metricAlerts 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'Send email on error in template-fastapi-react' + location: 'global' + properties: { + description: 'When an error is detected in template-fastapi-react, an email is dispatched' + severity: 1 + enabled: true + scopes: [ + appInsight.id + ] + evaluationFrequency: 'PT1H' + windowSize: 'PT1H' + criteria: { + allOf: [ + { + threshold: 0 + name: 'Metric1' + metricNamespace: 'microsoft.insights/components' + metricName: 'exceptions/count' + operator: 'GreaterThan' + timeAggregation: 'Count' + skipMetricValidation: false + criterionType: 'StaticThresholdCriterion' + } + ] + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + } + autoMitigate: false + targetResourceType: 'microsoft.insights/components' + targetResourceRegion: 'norwayeast' + actions: [ + { + actionGroupId: sendEmailActionGroup.id + } + ] + } +}