diff --git a/azure.yaml b/azure.yaml index f07e30b..3274323 100644 --- a/azure.yaml +++ b/azure.yaml @@ -1,7 +1,7 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json name: java-microservices-aca-lab -resourceGroup: rg-sonwan-petclinic +resourceGroup: rg-petclinic infra: provider: bicep path: infra/bicep @@ -13,7 +13,7 @@ services: host: containerapp language: java docker: - registry: sonwanacr.azurecr.io + registry: .azurecr.io image: java-microservices-aca-lab/spring-petclinic-api-gateway tag: passwordless @@ -23,7 +23,7 @@ services: host: containerapp language: java docker: - registry: sonwanacr.azurecr.io + registry: .azurecr.io image: java-microservices-aca-lab/spring-petclinic-customers-service tag: passwordless @@ -33,7 +33,7 @@ services: host: containerapp language: java docker: - registry: sonwanacr.azurecr.io + registry: .azurecr.io image: java-microservices-aca-lab/spring-petclinic-vets-service tag: passwordless @@ -43,17 +43,27 @@ services: host: containerapp language: java docker: - registry: sonwanacr.azurecr.io + registry: .azurecr.io image: java-microservices-aca-lab/spring-petclinic-visits-service tag: passwordless + chat-agent: + resourceName: chat-agent + project: ./src/spring-petclinic-chat-agent + host: containerapp + language: java + docker: + registry: .azurecr.io + image: java-microservices-aca-lab/spring-petclinic-chat-agent + tag: passwordless + admin-server: resourceName: admin-server project: ./src/spring-petclinic-admin-server host: containerapp language: java docker: - registry: sonwanacr.azurecr.io + registry: .azurecr.io image: java-microservices-aca-lab/spring-petclinic-admin-server tag: passwordless @@ -61,7 +71,12 @@ hooks: prepackage: windows: shell: pwsh - run: 'cd src; .\mvnw.cmd package -DskipTests' + run: 'cd src; .\mvnw.cmd clean package -DskipTests' + posix: + shell: sh + run: 'cd src; chmod +x ./mvnw; ./mvnw clean package -DskipTests' + + postprovision: posix: shell: sh - run: 'cd src; chmod +x ./mvnw; ./mvnw package -DskipTests' + run: ./infra/bicep/hooks/postprovision.sh diff --git a/infra/bicep/hooks/postprovision.sh b/infra/bicep/hooks/postprovision.sh new file mode 100755 index 0000000..4fc8396 --- /dev/null +++ b/infra/bicep/hooks/postprovision.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -x + +# refresh service connection, via customers-service +az containerapp connection create mysql-flexible --connection $sqlConnectName --source-id $customersServiceId --target-id $sqlDatabaseId --client-type springBoot \ + --user-identity client-id=$appUserIdentityClientId subs-id=$subscriptionId mysql-identity-id=$sqlAdminIdentityId -c $customersServiceName -y \ No newline at end of file diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index 539bc71..4e5b9b1 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -2,46 +2,71 @@ targetScope = 'subscription' @minLength(2) @maxLength(32) -@description('Name of the the environment.') +@description('Name of the the azd environment.') param environmentName string @minLength(2) @description('Primary location for all resources.') param location string -param resourceGroupName string = '' +@description('Name of the the resource group. Default: rg-{environmentName}') +param resourceGroupName string + +@description('Name of the the new containerapp environment. Default: aca-env-{environmentName}') param managedEnvironmentsName string = '' +@description('Boolean indicating the aca environment only has an internal load balancer. ') param vnetEndpointInternal bool = false -// mysql +@description('Name of the the sql server. Default: sql-{environmentName}') param sqlServerName string = '' -param sqlAdmin string + +@description('Name of the the sql admin.') +param sqlAdmin string = 'sqladmin' + +@description('The the sql admin password.') @secure() param sqlAdminPassword string +@description('Repo url of the configure server.') param configGitRepo string + +@description('Repo branch of the configure server.') param configGitBranch string = 'main' + +@description('Repo path of the configure server.') param configGitPath string -param acrRegistry string -param acrIdentityId string -param miClientId string -param miPrincipalId string -param apiGatewayImage string -param customersServiceImage string -param vetsServiceImage string -param visitsServiceImage string -param adminServerImage string -param chatAgentImage string +@description('Name of the azure container registry.') +param acrName string +@description('Resource group of the azure container registry.') +param acrGroupName string = '' +@description('Subscription of the azure container registry.') +param acrSubscription string = '' +@description('Name of the log analytics server. Default la-{environmentName}') param logAnalyticsName string = '' + +@description('Name of the log analytics server. Default ai-{environmentName}') param applicationInsightsName string = '' +@description('Images for petclinic services, will replaced by new images on step `azd deploy`') +param apiGatewayImage string = '' +param customersServiceImage string = '' +param vetsServiceImage string = '' +param visitsServiceImage string = '' +param adminServerImage string = '' +param chatAgentImage string = '' + +@description('Name of the virtual network. Default vnet-{environmentName}') +param vnetName string = '' + var vnetPrefix = '10.1.0.0/16' var infraSubnetPrefix = '10.1.0.0/24' var infraSubnetName = '${abbrs.networkVirtualNetworksSubnets}infra' +var placeholderImage = 'azurespringapps/default-banner:distroless-2024022107-66ea1a62-87936983' + var abbrs = loadJsonContent('./abbreviations.json') var tags = { 'azd-env-name': environmentName } @@ -52,11 +77,27 @@ resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { tags: tags } +module umiAcrPull 'modules/shared/userAssignedIdentity.bicep' = { + name: 'umi-acr-pull' + scope: rg + params: { + name: 'umi-${acrName}-acrpull' + } +} + +module umiApps 'modules/shared/userAssignedIdentity.bicep' = { + name: 'umi-apps' + scope: rg + params: { + name: 'umi-apps-${environmentName}' + } +} + module vnet './modules/network/vnet.bicep' = { name: 'vnet' scope: rg params: { - name: '${abbrs.networkVirtualNetworks}${environmentName}' + name: !empty(vnetName) ? vnetName : '${abbrs.networkVirtualNetworks}${environmentName}' location: location vnetAddressPrefixes: [vnetPrefix] subnets: [ @@ -100,6 +141,25 @@ module applicationInsights 'modules/shared/applicationInsights.bicep' = { } } +// group id: /subscriptions//resourceGroups/ +var acrSub = !empty(acrSubscription) ? acrSubscription : split(rg.id, '/')[2] +var acrGroup = !empty(acrGroupName) ? acrGroupName : rg.name + +@description('roles for Azure Container Registry') +module acrRoleAssignments 'modules/shared/containerRegistryRoleAssignment.bicep' = { + name: 'acr-roles-assignments' + scope: resourceGroup(acrSub, acrGroup) + params: { + name: acrName + roleAssignments: [ + { + principalId: umiAcrPull.outputs.principalId + roleDefinitionIdOrName: 'AcrPull' + } + ] + } +} + module managedEnvironment 'modules/containerapps/aca-environment.bicep' = { name: 'managedEnvironment' scope: rg @@ -107,6 +167,10 @@ module managedEnvironment 'modules/containerapps/aca-environment.bicep' = { name: !empty(managedEnvironmentsName) ? managedEnvironmentsName : 'aca-env-${environmentName}' location: location vnetEndpointInternal: vnetEndpointInternal + userAssignedIdentities: { + '${umiAcrPull.outputs.id}': {} + '${umiApps.outputs.id}': {} + } diagnosticWorkspaceId: logAnalytics.outputs.logAnalyticsWsId subnetId: first(filter(vnet.outputs.vnetSubnets, x => x.name == infraSubnetName)).id tags: tags @@ -141,7 +205,7 @@ module openai 'modules/ai/openai.bicep' = { params: { accountName: 'openai-${environmentName}' location: location - appPrincipalId: miPrincipalId + appPrincipalId: umiApps.outputs.principalId } } @@ -153,25 +217,39 @@ module applications 'modules/app/petclinic.bicep' = { eurekaId: javaComponents.outputs.eurekaId configServerId: javaComponents.outputs.configServerId mysqlDBId: mysql.outputs.databaseId - mysqlUserAssignedIdentityClientId: mysql.outputs.userAssignedIdentityClientId - acrRegistry: acrRegistry - acrIdentityId: acrIdentityId - apiGatewayImage: apiGatewayImage - customersServiceImage: customersServiceImage - vetsServiceImage: vetsServiceImage - visitsServiceImage: visitsServiceImage - adminServerImage: adminServerImage - chatAgentImage: chatAgentImage + mysqlUserAssignedIdentityClientId: umiApps.outputs.clientId + acrRegistry: '${acrRoleAssignments.outputs.registryName}.azurecr.io' // add dependency to make sure roles are assigned + acrIdentityId: umiAcrPull.outputs.id + apiGatewayImage: !empty(apiGatewayImage) ? apiGatewayImage : placeholderImage + customersServiceImage: !empty(customersServiceImage) ? customersServiceImage : placeholderImage + vetsServiceImage: !empty(vetsServiceImage) ? vetsServiceImage : placeholderImage + visitsServiceImage: !empty(visitsServiceImage) ? visitsServiceImage : placeholderImage + adminServerImage: !empty(adminServerImage) ? adminServerImage : placeholderImage + chatAgentImage: !empty(chatAgentImage) ? chatAgentImage : placeholderImage targetPort: 8080 applicationInsightsConnString: applicationInsights.outputs.connectionString azureOpenAiEndpoint: openai.outputs.endpoint - openAiClientId: acrIdentityId + openAiClientId: umiApps.outputs.id } } +output subscriptionId string = subscription().subscriptionId +output resourceGroupName string = rg.name + output gatewayFqdn string = applications.outputs.gatewayFqdn output adminFqdn string = applications.outputs.adminFqdn -output eurekaId string = javaComponents.outputs.eurekaId -output configServerId string = javaComponents.outputs.configServerId -output databaseId string = mysql.outputs.databaseId -output userAssignedIdentityClientId string = mysql.outputs.userAssignedIdentityClientId + +output sqlDatabaseId string = mysql.outputs.databaseId +output sqlAdminIdentityClientId string = mysql.outputs.adminIdentityClientId +output sqlAdminIdentityId string = mysql.outputs.adminIdentityId +output sqlConnectName string = applications.outputs.connectionName + +output appUserIdentityClientId string = umiApps.outputs.clientId +output appUserIdentityId string = umiApps.outputs.id + +output customersServiceName string = applications.outputs.customersServiceName +output customersServiceId string = applications.outputs.customersServiceId +output vetsServiceName string = applications.outputs.vetsServiceName +output vetsServiceId string = applications.outputs.vetsServiceId +output visitsServiceName string = applications.outputs.visitsServiceName +output visitsServiceId string = applications.outputs.visitsServiceId diff --git a/infra/bicep/main.parameters.json b/infra/bicep/main.parameters.json index 1c2b944..ccbc5ba 100644 --- a/infra/bicep/main.parameters.json +++ b/infra/bicep/main.parameters.json @@ -2,11 +2,20 @@ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { + "environmentName": { + "value": "petclinic" + }, + "resourceGroupName": { + "value": "rg-petclinic" + }, + "location": { + "value": "westus2" + }, "vnetEndpointInternal": { "value": false }, "sqlAdmin": { - "value": "azureuser" + "value": "sqladmin" }, "sqlAdminPassword": { "value": "Password#123" @@ -17,35 +26,14 @@ "configGitPath": { "value": "config" }, - "acrRegistry": { - "value": ".azurecr.io" - }, - "acrIdentityId": { - "value": "" - }, - "miClientId": { - "value": "" - }, - "miPrincipalId": { - "value": "" - }, - "apiGatewayImage": { - "value": "java-microservices-aca-lab/spring-petclinic-api-gateway:passwordless" - }, - "customersServiceImage": { - "value": "java-microservices-aca-lab/spring-petclinic-customers-service:passwordless" - }, - "vetsServiceImage": { - "value": "java-microservices-aca-lab/spring-petclinic-vets-service:passwordless" - }, - "visitsServiceImage": { - "value": "java-microservices-aca-lab/spring-petclinic-visits-service:passwordless" + "acrName": { + "value": "" }, - "chatAgentImage": { - "value": "java-microservices-aca-lab/spring-petclinic-chat-agent:passwordless" + "acrGroupName": { + "value": "" }, - "adminServerImage": { - "value": "java-microservices-aca-lab/spring-petclinic-admin-server:passwordless" + "acrSubscription": { + "value": "" } } -} +} \ No newline at end of file diff --git a/infra/bicep/modules/app/petclinic.bicep b/infra/bicep/modules/app/petclinic.bicep index 466e16d..eba2081 100644 --- a/infra/bicep/modules/app/petclinic.bicep +++ b/infra/bicep/modules/app/petclinic.bicep @@ -56,7 +56,7 @@ module apiGateway '../containerapps/containerapp.bicep' = { } } -module customerService '../containerapps/containerapp.bicep' = { +module customersService '../containerapps/containerapp.bicep' = { name: 'customers-service' params: { location: environment.location @@ -201,3 +201,12 @@ module adminServer '../containerapps/containerapp.bicep' = { output gatewayFqdn string = apiGateway.outputs.appFqdn output adminFqdn string = adminServer.outputs.appFqdn + +output customersServiceName string = customersService.outputs.appName +output customersServiceId string = customersService.outputs.appId +output vetsServiceName string = vetsService.outputs.appName +output vetsServiceId string = vetsService.outputs.appId +output visitsServiceName string = visitsService.outputs.appName +output visitsServiceId string = visitsService.outputs.appId + +output connectionName string = customersService.outputs.connectionName diff --git a/infra/bicep/modules/containerapps/aca-environment.bicep b/infra/bicep/modules/containerapps/aca-environment.bicep index 4b776b9..3022c66 100644 --- a/infra/bicep/modules/containerapps/aca-environment.bicep +++ b/infra/bicep/modules/containerapps/aca-environment.bicep @@ -71,6 +71,12 @@ param diagnosticMetricsToEnable array = [ @description('Optional. The name of the diagnostic setting, if deployed. If left empty, it defaults to "-diagnosticSettings".') param diagnosticSettingsName string = '' +@description('Optional. Enables system assigned managed identity on the resource.') +param systemAssignedIdentity bool = false + +@description('Optional. The ID(s) to assign to the resource.') +param userAssignedIdentities object = {} + // ------------------ // VARIABLES @@ -103,14 +109,22 @@ var defaultWorkloadProfile = [ var effectiveWorkloadProfiles = workloadProfiles != [] ? concat(defaultWorkloadProfile, workloadProfiles) : defaultWorkloadProfile +var identityType = systemAssignedIdentity ? (!empty(userAssignedIdentities) ? 'SystemAssigned, UserAssigned' : 'SystemAssigned') : (!empty(userAssignedIdentities) ? 'UserAssigned' : 'None') + +var identity = identityType != 'None' ? { + type: identityType + userAssignedIdentities: !empty(userAssignedIdentities) ? userAssignedIdentities : null +} : null + // ------------------ // RESOURCES // ------------------ -resource acaEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = { +resource acaEnvironment 'Microsoft.App/managedEnvironments@2024-02-02-preview' = { name: name location: location tags: tags + identity: identity properties: { zoneRedundant: zoneRedundant daprAIInstrumentationKey: appInsightsInstrumentationKey diff --git a/infra/bicep/modules/containerapps/containerapp-java-components.bicep b/infra/bicep/modules/containerapps/containerapp-java-components.bicep index aeb0992..bb83cc6 100644 --- a/infra/bicep/modules/containerapps/containerapp-java-components.bicep +++ b/infra/bicep/modules/containerapps/containerapp-java-components.bicep @@ -34,6 +34,12 @@ resource eureka 'Microsoft.App/managedEnvironments/javaComponents@2024-02-02-pre name: 'eureka' properties: { componentType: 'SpringCloudEureka' + configurations: [ + { + propertyName: 'eureka.server.response-cache-update-interval-ms' + value: '10000' + } + ] } } diff --git a/infra/bicep/modules/containerapps/containerapp.bicep b/infra/bicep/modules/containerapps/containerapp.bicep index 600826b..5bef7b0 100644 --- a/infra/bicep/modules/containerapps/containerapp.bicep +++ b/infra/bicep/modules/containerapps/containerapp.bicep @@ -111,8 +111,10 @@ resource app 'Microsoft.App/containerApps@2024-02-02-preview' = { var mysqlToken = !empty(mysqlDBId) ? split(mysqlDBId, '/') : array('') var mysqlSubscriptionId = length(mysqlToken) > 2 ? mysqlToken[2] : '' +var connectionName = 'mysql_conn' + resource connectDB 'Microsoft.ServiceLinker/linkers@2023-04-01-preview' = if (createSqlConnection) { - name: 'mysql_conn' + name: connectionName scope: app properties: { scope: appName @@ -121,7 +123,7 @@ resource connectDB 'Microsoft.ServiceLinker/linkers@2023-04-01-preview' = if (cr authType: 'userAssignedIdentity' clientId: mysqlUserAssignedIdentityClientId subscriptionId: mysqlSubscriptionId - userName: 'aad_mysql_conn' + userName: 'aad_${connectionName}' } targetService: { type: 'AzureResource' @@ -130,5 +132,8 @@ resource connectDB 'Microsoft.ServiceLinker/linkers@2023-04-01-preview' = if (cr } } +output appName string = app.name output appId string = app.id output appFqdn string = app.properties.configuration.ingress != null ? app.properties.configuration.ingress.fqdn : '' + +output connectionName string = createSqlConnection ? connectionName : '' diff --git a/infra/bicep/modules/database/mysql.bicep b/infra/bicep/modules/database/mysql.bicep index 321efe5..787fda4 100644 --- a/infra/bicep/modules/database/mysql.bicep +++ b/infra/bicep/modules/database/mysql.bicep @@ -29,13 +29,8 @@ param tags object = {} @description('Optional. The version of MySQL.') param version string = '8.0.21' -resource mysqlUserAssignedIdentityRW 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { - name: 'mysqluserassignedidentity-rw' - location: location -} - resource mysqlUserAssignedIdentityAdmin 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { - name: 'mysqluserassignedidentity-admin' + name: 'umi-${serverName}-admin' location: location } @@ -51,7 +46,7 @@ resource serverNew 'Microsoft.DBforMySQL/flexibleServers@2023-06-30' = if (newOr identity: { type: 'UserAssigned' userAssignedIdentities: { - '${mysqlUserAssignedIdentityRW.id}': {} + '${mysqlUserAssignedIdentityAdmin.id}': {} } } properties: { @@ -113,8 +108,8 @@ resource databaseExisting 'Microsoft.DBforMySQL/flexibleServers/databases@2023-0 @description('The resource id of the database.') output databaseId string = ((newOrExisting == 'new') ? databaseNew.id : databaseExisting.id) -@description('The client id of the user assigned identity with r/w permission.') -output userAssignedIdentityClientId string = mysqlUserAssignedIdentityRW.properties.clientId +@description('The client id of the user assigned identity with admin permission.') +output adminIdentityClientId string = mysqlUserAssignedIdentityAdmin.properties.clientId -@description('The resource id of the user assigned identity with r/w permission.') -output userAssignedIdentity string = mysqlUserAssignedIdentityRW.id +@description('The resource id of the user assigned identity with admin permission.') +output adminIdentityId string = mysqlUserAssignedIdentityAdmin.id diff --git a/infra/bicep/modules/shared/applicationInsights.bicep b/infra/bicep/modules/shared/applicationInsights.bicep index 404d2c1..b274273 100644 --- a/infra/bicep/modules/shared/applicationInsights.bicep +++ b/infra/bicep/modules/shared/applicationInsights.bicep @@ -87,7 +87,11 @@ output name string = ((newOrExisting == 'new') ? aiNew.name : aiExisting.name) output resourceId string = ((newOrExisting == 'new') ? aiNew.id : aiExisting.id) @description('The applicationInsights Instrumentation Key.') -output instrumentationKey string = ((newOrExisting == 'new') ? aiNew.properties.InstrumentationKey : aiExisting.properties.InstrumentationKey) +output instrumentationKey string = ((newOrExisting == 'new') + ? aiNew.properties.InstrumentationKey + : aiExisting.properties.InstrumentationKey) @description('The applicationInsights Connection String.') -output connectionString string = ((newOrExisting == 'new') ? aiNew.properties.ConnectionString : aiExisting.properties.ConnectionString) +output connectionString string = ((newOrExisting == 'new') + ? aiNew.properties.ConnectionString + : aiExisting.properties.ConnectionString) diff --git a/infra/bicep/modules/shared/containerRegistry.bicep b/infra/bicep/modules/shared/containerRegistry.bicep new file mode 100644 index 0000000..993f6d5 --- /dev/null +++ b/infra/bicep/modules/shared/containerRegistry.bicep @@ -0,0 +1,263 @@ +import { roleAssignmentType, builtInRoleNames } from 'containerRegistryRolesDef.bicep' + +@description('Required. Name of your Azure container registry.') +@minLength(5) +@maxLength(50) +param name string + +@description('Optional. Enable admin user that have push / pull permission to the registry.') +param acrAdminUserEnabled bool = false + +@description('Optional. Location for all resources.') +param location string + +@description('Optional. Tier of your Azure container registry.') +@allowed([ + 'Basic' + 'Premium' + 'Standard' +]) +param acrSku string = 'Basic' + +@allowed([ + 'disabled' + 'enabled' +]) +@description('Optional. The value that indicates whether the export policy is enabled or not.') +param exportPolicyStatus string = 'disabled' + +@allowed([ + 'disabled' + 'enabled' +]) +@description('Optional. The value that indicates whether the quarantine policy is enabled or not.') +param quarantinePolicyStatus string = 'disabled' + +@allowed([ + 'disabled' + 'enabled' +]) +@description('Optional. The value that indicates whether the trust policy is enabled or not.') +param trustPolicyStatus string = 'disabled' + +@allowed([ + 'disabled' + 'enabled' +]) +@description('Optional. The value that indicates whether the retention policy is enabled or not.') +param retentionPolicyStatus string = 'enabled' + +@description('Optional. The number of days to retain an untagged manifest after which it gets purged.') +param retentionPolicyDays int = 15 + +@allowed([ + 'disabled' + 'enabled' +]) +@description('Optional. The value that indicates whether the policy for using ARM audience token for a container registr is enabled or not. Default is enabled.') +param azureADAuthenticationAsArmPolicyStatus string = 'enabled' + +@allowed([ + 'disabled' + 'enabled' +]) +@description('Optional. Soft Delete policy status. Default is disabled.') +param softDeletePolicyStatus string = 'disabled' + +@description('Optional. The number of days after which a soft-deleted item is permanently deleted.') +param softDeletePolicyDays int = 7 + +@description('Optional. Enable a single data endpoint per region for serving data. Not relevant in case of disabled public access. Note, requires the \'acrSku\' to be \'Premium\'.') +param dataEndpointEnabled bool = false + +@description('Optional. Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set and networkRuleSetIpRules are not set. Note, requires the \'acrSku\' to be \'Premium\'.') +@allowed([ + '' + 'Enabled' + 'Disabled' +]) +param publicNetworkAccess string = '' + +@allowed([ + 'AzureServices' + 'None' +]) +@description('Optional. Whether to allow trusted Azure services to access a network restricted registry.') +param networkRuleBypassOptions string = 'AzureServices' + +@allowed([ + 'Allow' + 'Deny' +]) +@description('Optional. The default action of allow or deny when no other rules match.') +param networkRuleSetDefaultAction string = 'Deny' + +@description('Optional. The IP ACL rules. Note, requires the \'acrSku\' to be \'Premium\'.') +param networkRuleSetIpRules array = [] + +@description('Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible. Note, requires the \'acrSku\' to be \'Premium\'.') +param privateEndpoints array = [] + +@allowed([ + 'Disabled' + 'Enabled' +]) +@description('Optional. Whether or not zone redundancy is enabled for this container registry.') +param zoneRedundancy string = 'Disabled' + +@description('Optional. Enables system assigned managed identity on the resource.') +param systemAssignedIdentity bool = false + +@description('Optional. The ID(s) to assign to the resource.') +param userAssignedIdentities object = {} + +@description('Optional. Tags of the resource.') +param tags object = {} + +@description('Optional. The name of logs that will be streamed. "allLogs" includes all possible logs for the resource.') +@allowed([ + 'allLogs' + 'ContainerRegistryRepositoryEvents' + 'ContainerRegistryLoginEvents' +]) +param diagnosticLogCategoriesToEnable array = [ + 'allLogs' +] + +@description('Optional. The name of metrics that will be streamed.') +@allowed([ + 'AllMetrics' +]) +param diagnosticMetricsToEnable array = [ + 'AllMetrics' +] + +@description('Optional. Resource ID of the diagnostic storage account.') +param diagnosticStorageAccountId string = '' + +@description('Optional. Resource ID of the diagnostic log analytics workspace.') +param diagnosticWorkspaceId string = '' + +@description('Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to.') +param diagnosticEventHubAuthorizationRuleId string = '' + +@description('Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category.') +param diagnosticEventHubName string = '' + +@description('Optional. The name of the diagnostic setting, if deployed. If left empty, it defaults to "-diagnosticSettings".') +param diagnosticSettingsName string = '' + +@description('Optional. Enables registry-wide pull from unauthenticated clients. It\'s in preview and available in the Standard and Premium service tiers.') +param anonymousPullEnabled bool = false + +@description('Optional. Array of role assignment objects that contain the \'roleDefinitionIdOrName\' and \'principalId\' to define RBAC role assignments on this resource. In the roleDefinitionIdOrName attribute, you can provide either the display name of the role definition, or its fully qualified ID in the following format: \'/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11\'.') +param roleAssignments roleAssignmentType + +var diagnosticsLogsSpecified = [for category in filter(diagnosticLogCategoriesToEnable, item => item != 'allLogs'): { + category: category + enabled: true +}] + +var diagnosticsLogs = contains(diagnosticLogCategoriesToEnable, 'allLogs') ? [ + { + categoryGroup: 'allLogs' + enabled: true + } +] : diagnosticsLogsSpecified + +var diagnosticsMetrics = [for metric in diagnosticMetricsToEnable: { + category: metric + timeGrain: null + enabled: true +}] + +var identityType = systemAssignedIdentity ? (!empty(userAssignedIdentities) ? 'SystemAssigned, UserAssigned' : 'SystemAssigned') : (!empty(userAssignedIdentities) ? 'UserAssigned' : 'None') + +var identity = identityType != 'None' ? { + type: identityType + userAssignedIdentities: !empty(userAssignedIdentities) ? userAssignedIdentities : null +} : null + +resource registry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' = { + name: name + location: location + identity: identity + tags: tags + sku: { + name: acrSku + } + properties: { + anonymousPullEnabled: anonymousPullEnabled + adminUserEnabled: acrAdminUserEnabled + policies: { + azureADAuthenticationAsArmPolicy: { + status: azureADAuthenticationAsArmPolicyStatus + } + exportPolicy: acrSku == 'Premium' ? { + status: exportPolicyStatus + } : null + quarantinePolicy: { + status: quarantinePolicyStatus + } + trustPolicy: { + type: 'Notary' + status: trustPolicyStatus + } + retentionPolicy: acrSku == 'Premium' ? { + days: retentionPolicyDays + status: retentionPolicyStatus + } : null + softDeletePolicy: { + retentionDays: softDeletePolicyDays + status: softDeletePolicyStatus + } + } + dataEndpointEnabled: dataEndpointEnabled + publicNetworkAccess: !empty(publicNetworkAccess) ? any(publicNetworkAccess) : (!empty(privateEndpoints) && empty(networkRuleSetIpRules) ? 'Disabled' : null) + networkRuleBypassOptions: networkRuleBypassOptions + networkRuleSet: !empty(networkRuleSetIpRules) ? { + defaultAction: networkRuleSetDefaultAction + ipRules: networkRuleSetIpRules + } : null + zoneRedundancy: acrSku == 'Premium' ? zoneRedundancy : null + } +} + +resource registry_diagnosticSettingName 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if ((!empty(diagnosticStorageAccountId)) || (!empty(diagnosticWorkspaceId)) || (!empty(diagnosticEventHubAuthorizationRuleId)) || (!empty(diagnosticEventHubName))) { + name: !empty(diagnosticSettingsName) ? diagnosticSettingsName : '${name}-diagnosticSettings' + properties: { + storageAccountId: !empty(diagnosticStorageAccountId) ? diagnosticStorageAccountId : null + workspaceId: !empty(diagnosticWorkspaceId) ? diagnosticWorkspaceId : null + eventHubAuthorizationRuleId: !empty(diagnosticEventHubAuthorizationRuleId) ? diagnosticEventHubAuthorizationRuleId : null + eventHubName: !empty(diagnosticEventHubName) ? diagnosticEventHubName : null + metrics: diagnosticsMetrics + logs: diagnosticsLogs + } + scope: registry +} + +resource registry_roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for (roleAssignment, index) in (roleAssignments ?? []): { + name: guid(registry.id, roleAssignment.principalId, roleAssignment.roleDefinitionIdOrName) + properties: { + roleDefinitionId: builtInRoleNames[?roleAssignment.roleDefinitionIdOrName] ?? roleAssignment.roleDefinitionIdOrName + principalId: roleAssignment.principalId + description: roleAssignment.?description + principalType: roleAssignment.?principalType + condition: roleAssignment.?condition + conditionVersion: !empty(roleAssignment.?condition) ? (roleAssignment.?conditionVersion ?? '2.0') : null // Must only be set if condtion is set + delegatedManagedIdentityResourceId: roleAssignment.?delegatedManagedIdentityResourceId + } + scope: registry +}] + +@description('The Name of the Azure container registry.') +output name string = registry.name + +@description('The reference to the Azure container registry.') +output loginServer string = registry.properties.loginServer + +@description('The resource ID of the Azure container registry.') +output resourceId string = registry.id + +@description('The principal ID of the system assigned identity.') +output systemAssignedPrincipalId string = systemAssignedIdentity && contains(registry.identity, 'principalId') ? registry.identity.principalId : '' diff --git a/infra/bicep/modules/shared/containerRegistryRoleAssignment.bicep b/infra/bicep/modules/shared/containerRegistryRoleAssignment.bicep new file mode 100644 index 0000000..ad2fbce --- /dev/null +++ b/infra/bicep/modules/shared/containerRegistryRoleAssignment.bicep @@ -0,0 +1,30 @@ +targetScope = 'resourceGroup' + +import { roleAssignmentType, builtInRoleNames } from 'containerRegistryRolesDef.bicep' + +@description('Name of the Azure Container Registry') +param name string + +@description('Array of role assignment objects that contain the \'roleDefinitionIdOrName\' and \'principalId\' to define RBAC role assignments on this resource. In the roleDefinitionIdOrName attribute, you can provide either the display name of the role definition, or its fully qualified ID in the following format: \'/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11\'.') +param roleAssignments roleAssignmentType + +resource registry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' existing = { + name: name +} + +resource registry_roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for (roleAssignment, index) in (roleAssignments ?? []): { + name: guid(registry.id, roleAssignment.principalId, roleAssignment.roleDefinitionIdOrName) + properties: { + roleDefinitionId: builtInRoleNames[?roleAssignment.roleDefinitionIdOrName] ?? roleAssignment.roleDefinitionIdOrName + principalId: roleAssignment.principalId + description: roleAssignment.?description + principalType: roleAssignment.?principalType + condition: roleAssignment.?condition + conditionVersion: !empty(roleAssignment.?condition) ? (roleAssignment.?conditionVersion ?? '2.0') : null // Must only be set if condtion is set + delegatedManagedIdentityResourceId: roleAssignment.?delegatedManagedIdentityResourceId + } + scope: registry +}] + +@description('Name of the Azure Container Registry') +output registryName string = registry.name diff --git a/infra/bicep/modules/shared/containerRegistryRolesDef.bicep b/infra/bicep/modules/shared/containerRegistryRolesDef.bicep new file mode 100644 index 0000000..370a8b2 --- /dev/null +++ b/infra/bicep/modules/shared/containerRegistryRolesDef.bicep @@ -0,0 +1,58 @@ +@export() +type roleAssignmentType = { + @description('Required. The name of the role to assign. If it cannot be found you can specify the role definition ID instead.') + roleDefinitionIdOrName: string + + @description('Required. The principal ID of the principal (user/group/identity) to assign the role to.') + principalId: string + + @description('Optional. The principal type of the assigned principal ID.') + principalType: ('ServicePrincipal' | 'Group' | 'User' | 'ForeignGroup' | 'Device' | null)? + + @description('Optional. The description of the role assignment.') + description: string? + + @description('Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase "foo_storage_container"') + condition: string? + + @description('Optional. Version of the condition.') + conditionVersion: '2.0'? + + @description('Optional. The Resource Id of the delegated managed identity resource.') + delegatedManagedIdentityResourceId: string? +}[]? + +@export() +var builtInRoleNames = { + // https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#containers + AcrDelete: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c2f4ef07-c644-48eb-af81-4b1b4947fb11') + AcrImageSigner: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '6cef56e8-d556-48e5-a04f-b8e64114680f') + AcrPull: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + AcrPush: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8311e382-0749-4cb8-b61a-304f252e45ec') + AcrQuarantineReader: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'cdda3590-29a3-44f6-95f2-9f980659eb04') + AcrQuarantineWriter: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c8d4ff99-41c3-41a8-9f60-21dfdad59608') + 'Azure Arc Enabled Kubernetes Cluster User Role': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00493d72-78f6-4148-b6c5-d3ce8e4799dd') + 'Azure Arc Kubernetes Admin': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'dffb1e0c-446f-4dde-a09f-99eb5cc68b96') + 'Azure Arc Kubernetes Cluster Admin': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8393591c-06b9-48a2-a542-1bd6b377f6a2') + 'Azure Arc Kubernetes Viewer': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '63f0a09d-1495-4db4-a681-037d84835eb4') + 'Azure Arc Kubernetes Writer': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5b999177-9696-4545-85c7-50de3797e5a1') + 'Azure Container Storage Contributor': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '95dd08a6-00bd-4661-84bf-f6726f83a4d0') + 'Azure Container Storage Operator': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '08d4c71a-cc63-4ce4-a9c8-5dd251b4d619') + 'Azure Container Storage Owner': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '95de85bd-744d-4664-9dde-11430bc34793') + 'Azure Kubernetes Fleet Manager Contributor Role': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '63bb64ad-9799-4770-b5c3-24ed299a07bf') + 'Azure Kubernetes Fleet Manager RBAC Admin': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '434fb43a-c01c-447e-9f67-c3ad923cfaba') + 'Azure Kubernetes Fleet Manager RBAC Cluster Admin': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18ab4d3d-a1bf-4477-8ad9-8359bc988f69') + 'Azure Kubernetes Fleet Manager RBAC Reader': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '30b27cfc-9c84-438e-b0ce-70e35255df80') + 'Azure Kubernetes Fleet Manager RBAC Writer': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5af6afb3-c06c-4fa4-8848-71a8aee05683') + 'Azure Kubernetes Service Cluster Admin Role': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0ab0b1a8-8aac-4efd-b8c2-3ee1fb270be8') + 'Azure Kubernetes Service Cluster Monitoring User': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '1afdec4b-e479-420e-99e7-f82237c7c5e6') + 'Azure Kubernetes Service Cluster User Role': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4abbcc35-e782-43d8-92c5-2d3f1bd2253f') + 'Azure Kubernetes Service Contributor Role': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ed7f3fbd-7b88-4dd4-9017-9adb7ce333f8') + 'Azure Kubernetes Service RBAC Admin': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '3498e952-d568-435e-9b2c-8d77e338d7f7') + 'Azure Kubernetes Service RBAC Cluster Admin': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b1ff04bb-8a4e-4dc4-8eb5-8693973ce19b') + 'Azure Kubernetes Service RBAC Reader': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f6c6a51-bcf8-42ba-9220-52d62157d7db') + 'Azure Kubernetes Service RBAC Writer': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a7ffa36f-339b-4b5c-8bdf-e2c188b2c0eb') + 'Kubernetes Agentless Operator': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'd5a2ae44-610b-4500-93be-660a0c5f5ca6') + 'Kubernetes Cluster - Azure Arc Onboarding': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '34e09817-6cbe-4d01-b1a2-e0eac5743d41') + 'Kubernetes Extension Contributor': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '85cb6faf-e071-4c9b-8136-154b5a04f717') +} diff --git a/infra/bicep/modules/shared/userAssignedIdentity.bicep b/infra/bicep/modules/shared/userAssignedIdentity.bicep new file mode 100644 index 0000000..7760c35 --- /dev/null +++ b/infra/bicep/modules/shared/userAssignedIdentity.bicep @@ -0,0 +1,15 @@ +targetScope = 'resourceGroup' + +@description('Required. Name of your user managed identity.') +param name string + +resource umi 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: name + location: resourceGroup().location +} + +output id string = umi.id + +output principalId string = umi.properties.principalId + +output clientId string = umi.properties.clientId diff --git a/src/pom.xml b/src/pom.xml index f86808e..2e5b41b 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -38,14 +38,19 @@ ${basedir} v0.6.1 1.2.0 - 5.15.0 - 1.0.0-SNAPSHOT + + com.azure.spring + spring-cloud-azure-dependencies + ${version.spring.cloud.azure} + pom + import + org.springframework.cloud spring-cloud-dependencies diff --git a/src/spring-petclinic-admin-server/Dockerfile b/src/spring-petclinic-admin-server/Dockerfile index a8816ec..9b56bfd 100644 --- a/src/spring-petclinic-admin-server/Dockerfile +++ b/src/spring-petclinic-admin-server/Dockerfile @@ -13,7 +13,7 @@ RUN wget https://github.com/microsoft/ApplicationInsights-Java/releases/download # run FROM mcr.microsoft.com/openjdk/jdk:17-distroless -ARG VERSION="3.0.2" +ARG VERSION=3.2.5 COPY --from=build ./ai.jar ai.jar diff --git a/src/spring-petclinic-api-gateway/Dockerfile b/src/spring-petclinic-api-gateway/Dockerfile index 0899bf0..36e5deb 100644 --- a/src/spring-petclinic-api-gateway/Dockerfile +++ b/src/spring-petclinic-api-gateway/Dockerfile @@ -13,7 +13,7 @@ RUN wget https://github.com/microsoft/ApplicationInsights-Java/releases/download # run FROM mcr.microsoft.com/openjdk/jdk:17-distroless -ARG VERSION="3.0.2" +ARG VERSION=3.2.5 COPY --from=build ./ai.jar ai.jar diff --git a/src/spring-petclinic-chat-agent/Dockerfile b/src/spring-petclinic-chat-agent/Dockerfile new file mode 100644 index 0000000..2dce50d --- /dev/null +++ b/src/spring-petclinic-chat-agent/Dockerfile @@ -0,0 +1,25 @@ +# syntax=docker/dockerfile:1 + +# build +FROM mcr.microsoft.com/openjdk/jdk:17-mariner AS build + +ARG AI_VERSION=3.5.4 + +RUN yum update && \ + yum install -y wget + +RUN wget https://github.com/microsoft/ApplicationInsights-Java/releases/download/$AI_VERSION/applicationinsights-agent-$AI_VERSION.jar -O ai.jar + +# run +FROM mcr.microsoft.com/openjdk/jdk:17-distroless + +ARG VERSION=3.2.5 + +COPY --from=build ./ai.jar ai.jar + +COPY ./target/spring-petclinic-chat-agent-$VERSION.jar app.jar + +EXPOSE 8080 + +# Run the jar file +ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-javaagent:/ai.jar", "-jar", "/app.jar"] diff --git a/src/spring-petclinic-customers-service/Dockerfile b/src/spring-petclinic-customers-service/Dockerfile index 83544a3..a58c03a 100644 --- a/src/spring-petclinic-customers-service/Dockerfile +++ b/src/spring-petclinic-customers-service/Dockerfile @@ -13,7 +13,7 @@ RUN wget https://github.com/microsoft/ApplicationInsights-Java/releases/download # run FROM mcr.microsoft.com/openjdk/jdk:17-distroless -ARG VERSION="3.0.2" +ARG VERSION=3.2.5 COPY --from=build ./ai.jar ai.jar diff --git a/src/spring-petclinic-customers-service/pom.xml b/src/spring-petclinic-customers-service/pom.xml index 1769116..f8f82f3 100644 --- a/src/spring-petclinic-customers-service/pom.xml +++ b/src/spring-petclinic-customers-service/pom.xml @@ -56,9 +56,8 @@ - com.mysql - mysql-connector-j - runtime + com.azure.spring + spring-cloud-azure-starter-jdbc-mysql org.hsqldb diff --git a/src/spring-petclinic-vets-service/Dockerfile b/src/spring-petclinic-vets-service/Dockerfile index ee8c430..376e83c 100644 --- a/src/spring-petclinic-vets-service/Dockerfile +++ b/src/spring-petclinic-vets-service/Dockerfile @@ -13,7 +13,7 @@ RUN wget https://github.com/microsoft/ApplicationInsights-Java/releases/download # run FROM mcr.microsoft.com/openjdk/jdk:17-distroless -ARG VERSION="3.0.2" +ARG VERSION=3.2.5 COPY --from=build ./ai.jar ai.jar diff --git a/src/spring-petclinic-vets-service/pom.xml b/src/spring-petclinic-vets-service/pom.xml index 9577a6f..d31f9a2 100644 --- a/src/spring-petclinic-vets-service/pom.xml +++ b/src/spring-petclinic-vets-service/pom.xml @@ -89,11 +89,10 @@ hsqldb runtime - - com.mysql - mysql-connector-j - runtime - + + com.azure.spring + spring-cloud-azure-starter-jdbc-mysql + io.micrometer micrometer-registry-prometheus diff --git a/src/spring-petclinic-visits-service/Dockerfile b/src/spring-petclinic-visits-service/Dockerfile index 11fb2e0..f4d2d36 100644 --- a/src/spring-petclinic-visits-service/Dockerfile +++ b/src/spring-petclinic-visits-service/Dockerfile @@ -13,7 +13,7 @@ RUN wget https://github.com/microsoft/ApplicationInsights-Java/releases/download # run FROM mcr.microsoft.com/openjdk/jdk:17-distroless -ARG VERSION="3.0.2" +ARG VERSION=3.2.5 COPY --from=build ./ai.jar ai.jar diff --git a/src/spring-petclinic-visits-service/pom.xml b/src/spring-petclinic-visits-service/pom.xml index 71a0309..3c110b7 100644 --- a/src/spring-petclinic-visits-service/pom.xml +++ b/src/spring-petclinic-visits-service/pom.xml @@ -74,11 +74,10 @@ org.jolokia jolokia-core - - com.mysql - mysql-connector-j - runtime - + + com.azure.spring + spring-cloud-azure-starter-jdbc-mysql + io.micrometer micrometer-registry-prometheus