From f9910d00e15b65ea286b26893d1638cd267422b6 Mon Sep 17 00:00:00 2001 From: Thiago Almeida Date: Fri, 4 Apr 2025 07:33:17 -0700 Subject: [PATCH 1/4] Adding architecture diagram and samples gallery metadata --- README.md | 18 +++++++++++++++ architecture-diagram.drawio | 44 ++++++++++++++++++++++++++++++++++++ architecture-diagram.png | Bin 0 -> 51727 bytes 3 files changed, 62 insertions(+) create mode 100644 architecture-diagram.drawio create mode 100644 architecture-diagram.png diff --git a/README.md b/README.md index 32b9917..dd08afb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,21 @@ + + # Getting Started with Remote MCP Servers using Azure Functions (Python) This is a quickstart template to easily build and deploy a custom remote MCP server to the cloud using Azure Functions with Python. You can clone/restore/run on your local machine with debugging, and `azd up` to have it in the cloud in a couple minutes. The MCP server is secured by design using keys and HTTPS, and allows more options for OAuth using EasyAuth and/or API Management as well as network isolation using VNET. diff --git a/architecture-diagram.drawio b/architecture-diagram.drawio new file mode 100644 index 0000000..2207446 --- /dev/null +++ b/architecture-diagram.drawio @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/architecture-diagram.png b/architecture-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..05b9308a9a0a7aa80e7e2f069f72f9becc7ef2b8 GIT binary patch literal 51727 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 KmP(h@_4+TTEqBTQ literal 0 HcmV?d00001 From 562a221b66894fffc5350d1583050395558a251c Mon Sep 17 00:00:00 2001 From: Thiago Almeida Date: Fri, 4 Apr 2025 07:58:30 -0700 Subject: [PATCH 2/4] Adding AZD and bicep --- CHANGELOG.md | 13 ++ CONTRIBUTING.md | 76 ++++++++ azure.yaml | 10 + infra/abbreviations.json | 135 ++++++++++++++ infra/app/api.bicep | 46 +++++ infra/app/storage-Access.bicep | 20 ++ infra/app/storage-PrivateEndpoint.bicep | 89 +++++++++ infra/app/vnet.bicep | 75 ++++++++ infra/core/host/appserviceplan.bicep | 20 ++ .../core/host/functions-flexconsumption.bicep | 87 +++++++++ .../core/identity/userAssignedIdentity.bicep | 14 ++ infra/core/monitor/appinsights-access.bicep | 21 +++ infra/core/monitor/applicationinsights.bicep | 22 +++ infra/core/monitor/loganalytics.bicep | 21 +++ infra/core/monitor/monitoring.bicep | 31 ++++ infra/core/storage/storage-account.bicep | 43 +++++ infra/main.bicep | 171 ++++++++++++++++++ infra/main.parameters.json | 15 ++ 18 files changed, 909 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 azure.yaml create mode 100644 infra/abbreviations.json create mode 100644 infra/app/api.bicep create mode 100644 infra/app/storage-Access.bicep create mode 100644 infra/app/storage-PrivateEndpoint.bicep create mode 100644 infra/app/vnet.bicep create mode 100644 infra/core/host/appserviceplan.bicep create mode 100644 infra/core/host/functions-flexconsumption.bicep create mode 100644 infra/core/identity/userAssignedIdentity.bicep create mode 100644 infra/core/monitor/appinsights-access.bicep create mode 100644 infra/core/monitor/applicationinsights.bicep create mode 100644 infra/core/monitor/loganalytics.bicep create mode 100644 infra/core/monitor/monitoring.bicep create mode 100644 infra/core/storage/storage-account.bicep create mode 100644 infra/main.bicep create mode 100644 infra/main.parameters.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9824752 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +## [project-title] Changelog + + +# x.y.z (yyyy-mm-dd) + +*Features* +* ... + +*Bug Fixes* +* ... + +*Breaking Changes* +* ... diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a9115cf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,76 @@ +# Contributing to [project-title] + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. + +When you submit a pull request, a CLA bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + + - [Code of Conduct](#coc) + - [Issues and Bugs](#issue) + - [Feature Requests](#feature) + - [Submission Guidelines](#submit) + +## Code of Conduct +Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +## Found an Issue? +If you find a bug in the source code or a mistake in the documentation, you can help us by +[submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can +[submit a Pull Request](#submit-pr) with a fix. + +## Want a Feature? +You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub +Repository. If you would like to *implement* a new feature, please submit an issue with +a proposal for your work first, to be sure that we can use it. + +* **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). + +## Submission Guidelines + +### Submitting an Issue +Before you submit an issue, search the archive, maybe your question was already answered. + +If your issue appears to be a bug, and hasn't been reported, open a new issue. +Help us to maximize the effort we can spend fixing issues and adding new +features, by not reporting duplicate issues. Providing the following information will increase the +chances of your issue being dealt with quickly: + +* **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps +* **Version** - what version is affected (e.g. 0.1.2) +* **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you +* **Browsers and Operating System** - is this a problem with all browsers? +* **Reproduce the Error** - provide a live example or a unambiguous set of steps +* **Related Issues** - has a similar issue been reported before? +* **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be + causing the problem (line of code or commit) + +You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. + +### Submitting a Pull Request (PR) +Before you submit your Pull Request (PR) consider the following guidelines: + +* Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR + that relates to your submission. You don't want to duplicate effort. + +* Make your changes in a new git fork: + +* Commit your changes using a descriptive commit message +* Push your fork to GitHub: +* In GitHub, create a pull request +* If we suggest changes then: + * Make the required updates. + * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): + + ```shell + git rebase master -i + git push -f + ``` + +That's it! Thank you for your contribution! diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 0000000..d7a208c --- /dev/null +++ b/azure.yaml @@ -0,0 +1,10 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: remote-mcp-functions-python +metadata: + template: remote-mcp-functions-python@1.0.0 +services: + api: + project: ./src/ + language: python + host: function diff --git a/infra/abbreviations.json b/infra/abbreviations.json new file mode 100644 index 0000000..a4fc9df --- /dev/null +++ b/infra/abbreviations.json @@ -0,0 +1,135 @@ +{ + "analysisServicesServers": "as", + "apiManagementService": "apim-", + "appConfigurationConfigurationStores": "appcs-", + "appManagedEnvironments": "cae-", + "appContainerApps": "ca-", + "authorizationPolicyDefinitions": "policy-", + "automationAutomationAccounts": "aa-", + "blueprintBlueprints": "bp-", + "blueprintBlueprintsArtifacts": "bpa-", + "cacheRedis": "redis-", + "cdnProfiles": "cdnp-", + "cdnProfilesEndpoints": "cdne-", + "cognitiveServicesAccounts": "cog-", + "cognitiveServicesFormRecognizer": "cog-fr-", + "cognitiveServicesTextAnalytics": "cog-ta-", + "computeAvailabilitySets": "avail-", + "computeCloudServices": "cld-", + "computeDiskEncryptionSets": "des", + "computeDisks": "disk", + "computeDisksOs": "osdisk", + "computeGalleries": "gal", + "computeSnapshots": "snap-", + "computeVirtualMachines": "vm", + "computeVirtualMachineScaleSets": "vmss-", + "containerInstanceContainerGroups": "ci", + "containerRegistryRegistries": "cr", + "containerServiceManagedClusters": "aks-", + "databricksWorkspaces": "dbw-", + "dataFactoryFactories": "adf-", + "dataLakeAnalyticsAccounts": "dla", + "dataLakeStoreAccounts": "dls", + "dataMigrationServices": "dms-", + "dBforMySQLServers": "mysql-", + "dBforPostgreSQLServers": "psql-", + "devicesIotHubs": "iot-", + "devicesProvisioningServices": "provs-", + "devicesProvisioningServicesCertificates": "pcert-", + "documentDBDatabaseAccounts": "cosmos-", + "eventGridDomains": "evgd-", + "eventGridDomainsTopics": "evgt-", + "eventGridEventSubscriptions": "evgs-", + "eventHubNamespaces": "evhns-", + "eventHubNamespacesEventHubs": "evh-", + "hdInsightClustersHadoop": "hadoop-", + "hdInsightClustersHbase": "hbase-", + "hdInsightClustersKafka": "kafka-", + "hdInsightClustersMl": "mls-", + "hdInsightClustersSpark": "spark-", + "hdInsightClustersStorm": "storm-", + "hybridComputeMachines": "arcs-", + "insightsActionGroups": "ag-", + "insightsComponents": "appi-", + "keyVaultVaults": "kv-", + "kubernetesConnectedClusters": "arck", + "kustoClusters": "dec", + "kustoClustersDatabases": "dedb", + "logicIntegrationAccounts": "ia-", + "logicWorkflows": "logic-", + "machineLearningServicesWorkspaces": "mlw-", + "managedIdentityUserAssignedIdentities": "id-", + "managementManagementGroups": "mg-", + "migrateAssessmentProjects": "migr-", + "networkApplicationGateways": "agw-", + "networkApplicationSecurityGroups": "asg-", + "networkAzureFirewalls": "afw-", + "networkBastionHosts": "bas-", + "networkConnections": "con-", + "networkDnsZones": "dnsz-", + "networkExpressRouteCircuits": "erc-", + "networkFirewallPolicies": "afwp-", + "networkFirewallPoliciesWebApplication": "waf", + "networkFirewallPoliciesRuleGroups": "wafrg", + "networkFrontDoors": "fd-", + "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", + "networkLoadBalancersExternal": "lbe-", + "networkLoadBalancersInternal": "lbi-", + "networkLoadBalancersInboundNatRules": "rule-", + "networkLocalNetworkGateways": "lgw-", + "networkNatGateways": "ng-", + "networkNetworkInterfaces": "nic-", + "networkNetworkSecurityGroups": "nsg-", + "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", + "networkNetworkWatchers": "nw-", + "networkPrivateDnsZones": "pdnsz-", + "networkPrivateLinkServices": "pl-", + "networkPublicIPAddresses": "pip-", + "networkPublicIPPrefixes": "ippre-", + "networkRouteFilters": "rf-", + "networkRouteTables": "rt-", + "networkRouteTablesRoutes": "udr-", + "networkTrafficManagerProfiles": "traf-", + "networkVirtualNetworkGateways": "vgw-", + "networkVirtualNetworks": "vnet-", + "networkVirtualNetworksSubnets": "snet-", + "networkVirtualNetworksVirtualNetworkPeerings": "peer-", + "networkVirtualWans": "vwan-", + "networkVpnGateways": "vpng-", + "networkVpnGatewaysVpnConnections": "vcn-", + "networkVpnGatewaysVpnSites": "vst-", + "notificationHubsNamespaces": "ntfns-", + "notificationHubsNamespacesNotificationHubs": "ntf-", + "operationalInsightsWorkspaces": "log-", + "portalDashboards": "dash-", + "powerBIDedicatedCapacities": "pbi-", + "purviewAccounts": "pview-", + "recoveryServicesVaults": "rsv-", + "resourcesResourceGroups": "rg-", + "searchSearchServices": "srch-", + "serviceBusNamespaces": "sb-", + "serviceBusNamespacesQueues": "sbq-", + "serviceBusNamespacesTopics": "sbt-", + "serviceEndPointPolicies": "se-", + "serviceFabricClusters": "sf-", + "signalRServiceSignalR": "sigr", + "sqlManagedInstances": "sqlmi-", + "sqlServers": "sql-", + "sqlServersDataWarehouse": "sqldw-", + "sqlServersDatabases": "sqldb-", + "sqlServersDatabasesStretch": "sqlstrdb-", + "storageStorageAccounts": "st", + "storageStorageAccountsVm": "stvm", + "storSimpleManagers": "ssimp", + "streamAnalyticsCluster": "asa-", + "synapseWorkspaces": "syn", + "synapseWorkspacesAnalyticsWorkspaces": "synw", + "synapseWorkspacesSqlPoolsDedicated": "syndp", + "synapseWorkspacesSqlPoolsSpark": "synsp", + "timeSeriesInsightsEnvironments": "tsi-", + "webServerFarms": "plan-", + "webSitesAppService": "app-", + "webSitesAppServiceEnvironment": "ase-", + "webSitesFunctions": "func-", + "webStaticSites": "stapp-" +} \ No newline at end of file diff --git a/infra/app/api.bicep b/infra/app/api.bicep new file mode 100644 index 0000000..68559b9 --- /dev/null +++ b/infra/app/api.bicep @@ -0,0 +1,46 @@ +param name string +param location string = resourceGroup().location +param tags object = {} +param applicationInsightsName string = '' +param appServicePlanId string +param appSettings object = {} +param runtimeName string +param runtimeVersion string +param serviceName string = 'api' +param storageAccountName string +param deploymentStorageContainerName string +param virtualNetworkSubnetId string = '' +param instanceMemoryMB int = 2048 +param maximumInstanceCount int = 100 +param identityId string = '' +param identityClientId string = '' + +var applicationInsightsIdentity = 'ClientId=${identityClientId};Authorization=AAD' + +module api '../core/host/functions-flexconsumption.bicep' = { + name: '${serviceName}-functions-module' + params: { + name: name + location: location + tags: union(tags, { 'azd-service-name': serviceName }) + identityType: 'UserAssigned' + identityId: identityId + appSettings: union(appSettings, + { + AzureWebJobsStorage__clientId : identityClientId + APPLICATIONINSIGHTS_AUTHENTICATION_STRING: applicationInsightsIdentity + }) + applicationInsightsName: applicationInsightsName + appServicePlanId: appServicePlanId + runtimeName: runtimeName + runtimeVersion: runtimeVersion + storageAccountName: storageAccountName + deploymentStorageContainerName: deploymentStorageContainerName + virtualNetworkSubnetId: virtualNetworkSubnetId + instanceMemoryMB: instanceMemoryMB + maximumInstanceCount: maximumInstanceCount + } +} + +output SERVICE_API_NAME string = api.outputs.name +output SERVICE_API_IDENTITY_PRINCIPAL_ID string = api.outputs.identityPrincipalId diff --git a/infra/app/storage-Access.bicep b/infra/app/storage-Access.bicep new file mode 100644 index 0000000..9d7b7ec --- /dev/null +++ b/infra/app/storage-Access.bicep @@ -0,0 +1,20 @@ +param principalID string +param roleDefinitionID string +param storageAccountName string + +resource storageAccount 'Microsoft.Storage/storageAccounts@2021-09-01' existing = { + name: storageAccountName +} + +// Allow access from API to storage account using a managed identity and least priv Storage roles +resource storageRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { + name: guid(storageAccount.id, principalID, roleDefinitionID) + scope: storageAccount + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionID) + principalId: principalID + principalType: 'ServicePrincipal' // Workaround for https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-template#new-service-principal + } +} + +output ROLE_ASSIGNMENT_NAME string = storageRoleAssignment.name diff --git a/infra/app/storage-PrivateEndpoint.bicep b/infra/app/storage-PrivateEndpoint.bicep new file mode 100644 index 0000000..b2ae5e2 --- /dev/null +++ b/infra/app/storage-PrivateEndpoint.bicep @@ -0,0 +1,89 @@ +// Parameters +@description('Specifies the name of the virtual network.') +param virtualNetworkName string + +@description('Specifies the name of the subnet which contains the virtual machine.') +param subnetName string + +@description('Specifies the resource name of the Storage resource with an endpoint.') +param resourceName string + +@description('Specifies the location.') +param location string = resourceGroup().location + +param tags object = {} + +// Virtual Network +resource vnet 'Microsoft.Network/virtualNetworks@2021-08-01' existing = { + name: virtualNetworkName +} + +resource storageAccount 'Microsoft.Storage/storageAccounts@2021-09-01' existing = { + name: resourceName +} + +var blobPrivateDNSZoneName = format('privatelink.blob.{0}', environment().suffixes.storage) +var blobPrivateDnsZoneVirtualNetworkLinkName = format('{0}-link-{1}', resourceName, take(toLower(uniqueString(resourceName, virtualNetworkName)), 4)) + +// Private DNS Zones +resource blobPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { + name: blobPrivateDNSZoneName + location: 'global' + tags: tags + properties: {} + dependsOn: [ + vnet + ] +} + +// Virtual Network Links +resource blobPrivateDnsZoneVirtualNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { + parent: blobPrivateDnsZone + name: blobPrivateDnsZoneVirtualNetworkLinkName + location: 'global' + tags: tags + properties: { + registrationEnabled: false + virtualNetwork: { + id: vnet.id + } + } +} + +// Private Endpoints +resource blobPrivateEndpoint 'Microsoft.Network/privateEndpoints@2021-08-01' = { + name: 'blob-private-endpoint' + location: location + tags: tags + properties: { + privateLinkServiceConnections: [ + { + name: 'blobPrivateLinkConnection' + properties: { + privateLinkServiceId: storageAccount.id + groupIds: [ + 'blob' + ] + } + } + ] + subnet: { + id: '${vnet.id}/subnets/${subnetName}' + } + } +} + +resource blobPrivateDnsZoneGroupName 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-01-01' = { + parent: blobPrivateEndpoint + name: 'blobPrivateDnsZoneGroup' + properties: { + privateDnsZoneConfigs: [ + { + name: 'storageBlobARecord' + properties: { + privateDnsZoneId: blobPrivateDnsZone.id + } + } + ] + } +} diff --git a/infra/app/vnet.bicep b/infra/app/vnet.bicep new file mode 100644 index 0000000..1a24c11 --- /dev/null +++ b/infra/app/vnet.bicep @@ -0,0 +1,75 @@ +@description('Specifies the name of the virtual network.') +param vNetName string + +@description('Specifies the location.') +param location string = resourceGroup().location + +@description('Specifies the name of the subnet for the Service Bus private endpoint.') +param peSubnetName string = 'private-endpoints-subnet' + +@description('Specifies the name of the subnet for Function App virtual network integration.') +param appSubnetName string = 'app' + +param tags object = {} + +resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-05-01' = { + name: vNetName + location: location + tags: tags + properties: { + addressSpace: { + addressPrefixes: [ + '10.0.0.0/16' + ] + } + encryption: { + enabled: false + enforcement: 'AllowUnencrypted' + } + subnets: [ + { + name: peSubnetName + id: resourceId('Microsoft.Network/virtualNetworks/subnets', vNetName, 'private-endpoints-subnet') + properties: { + addressPrefixes: [ + '10.0.1.0/24' + ] + delegations: [] + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Enabled' + } + type: 'Microsoft.Network/virtualNetworks/subnets' + } + { + name: appSubnetName + id: resourceId('Microsoft.Network/virtualNetworks/subnets', vNetName, 'app') + properties: { + addressPrefixes: [ + '10.0.2.0/24' + ] + delegations: [ + { + name: 'delegation' + id: resourceId('Microsoft.Network/virtualNetworks/subnets/delegations', vNetName, 'app', 'delegation') + properties: { + //Microsoft.App/environments is the correct delegation for Flex Consumption VNet integration + serviceName: 'Microsoft.App/environments' + } + type: 'Microsoft.Network/virtualNetworks/subnets/delegations' + } + ] + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Enabled' + } + type: 'Microsoft.Network/virtualNetworks/subnets' + } + ] + virtualNetworkPeerings: [] + enableDdosProtection: false + } +} + +output peSubnetName string = virtualNetwork.properties.subnets[0].name +output peSubnetID string = virtualNetwork.properties.subnets[0].id +output appSubnetName string = virtualNetwork.properties.subnets[1].name +output appSubnetID string = virtualNetwork.properties.subnets[1].id diff --git a/infra/core/host/appserviceplan.bicep b/infra/core/host/appserviceplan.bicep new file mode 100644 index 0000000..9ab72a8 --- /dev/null +++ b/infra/core/host/appserviceplan.bicep @@ -0,0 +1,20 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param kind string = '' +param reserved bool = true +param sku object + +resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = { + name: name + location: location + tags: tags + sku: sku + kind: kind + properties: { + reserved: reserved + } +} + +output id string = appServicePlan.id diff --git a/infra/core/host/functions-flexconsumption.bicep b/infra/core/host/functions-flexconsumption.bicep new file mode 100644 index 0000000..3417910 --- /dev/null +++ b/infra/core/host/functions-flexconsumption.bicep @@ -0,0 +1,87 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +// Reference Properties +param applicationInsightsName string = '' +param appServicePlanId string +param storageAccountName string +param virtualNetworkSubnetId string = '' +@allowed(['SystemAssigned', 'UserAssigned']) +param identityType string +@description('User assigned identity name') +param identityId string + +// Runtime Properties +@allowed([ + 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' +]) +param runtimeName string +@allowed(['3.10', '3.11', '7.4', '8.0', '10', '11', '17', '20']) +param runtimeVersion string +param kind string = 'functionapp,linux' + +// Microsoft.Web/sites/config +param appSettings object = {} +param instanceMemoryMB int = 2048 +param maximumInstanceCount int = 100 +param deploymentStorageContainerName string + +resource stg 'Microsoft.Storage/storageAccounts@2022-09-01' existing = { + name: storageAccountName +} + +resource functions 'Microsoft.Web/sites@2023-12-01' = { + name: name + location: location + tags: tags + kind: kind + identity: { + type: identityType + userAssignedIdentities: { + '${identityId}': {} + } + } + properties: { + serverFarmId: appServicePlanId + functionAppConfig: { + deployment: { + storage: { + type: 'blobContainer' + value: '${stg.properties.primaryEndpoints.blob}${deploymentStorageContainerName}' + authentication: { + type: identityType == 'SystemAssigned' ? 'SystemAssignedIdentity' : 'UserAssignedIdentity' + userAssignedIdentityResourceId: identityType == 'UserAssigned' ? identityId : '' + } + } + } + scaleAndConcurrency: { + instanceMemoryMB: instanceMemoryMB + maximumInstanceCount: maximumInstanceCount + } + runtime: { + name: runtimeName + version: runtimeVersion + } + } + virtualNetworkSubnetId: !empty(virtualNetworkSubnetId) ? virtualNetworkSubnetId : null + } + + resource configAppSettings 'config' = { + name: 'appsettings' + properties: union(appSettings, + { + AzureWebJobsStorage__accountName: stg.name + AzureWebJobsStorage__credential : 'managedidentity' + APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString + }) + } +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { + name: applicationInsightsName +} + +output name string = functions.name +output uri string = 'https://${functions.properties.defaultHostName}' +output identityPrincipalId string = identityType == 'SystemAssigned' ? functions.identity.principalId : '' diff --git a/infra/core/identity/userAssignedIdentity.bicep b/infra/core/identity/userAssignedIdentity.bicep new file mode 100644 index 0000000..0d4e02e --- /dev/null +++ b/infra/core/identity/userAssignedIdentity.bicep @@ -0,0 +1,14 @@ +param identityName string +param location string +param tags object = {} + +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' = { + name: identityName + location: location + tags: tags +} + +output identityId string = userAssignedIdentity.id +output identityName string = userAssignedIdentity.name +output identityPrincipalId string = userAssignedIdentity.properties.principalId +output identityClientId string = userAssignedIdentity.properties.clientId diff --git a/infra/core/monitor/appinsights-access.bicep b/infra/core/monitor/appinsights-access.bicep new file mode 100644 index 0000000..f151b10 --- /dev/null +++ b/infra/core/monitor/appinsights-access.bicep @@ -0,0 +1,21 @@ +param principalID string +param roleDefinitionID string +param appInsightsName string + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { + name: appInsightsName +} + +// Allow access from API to app insights using a managed identity and least priv role +resource appInsightsRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { + name: guid(applicationInsights.id, principalID, roleDefinitionID) + scope: applicationInsights + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionID) + principalId: principalID + principalType: 'ServicePrincipal' // Workaround for https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-template#new-service-principal + } +} + +output ROLE_ASSIGNMENT_NAME string = appInsightsRoleAssignment.name + diff --git a/infra/core/monitor/applicationinsights.bicep b/infra/core/monitor/applicationinsights.bicep new file mode 100644 index 0000000..f6d9ee5 --- /dev/null +++ b/infra/core/monitor/applicationinsights.bicep @@ -0,0 +1,22 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param logAnalyticsWorkspaceId string +param disableLocalAuth bool = false + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { + name: name + location: location + tags: tags + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalyticsWorkspaceId + DisableLocalAuth: disableLocalAuth + } +} + +output connectionString string = applicationInsights.properties.ConnectionString +output instrumentationKey string = applicationInsights.properties.InstrumentationKey +output name string = applicationInsights.name diff --git a/infra/core/monitor/loganalytics.bicep b/infra/core/monitor/loganalytics.bicep new file mode 100644 index 0000000..770544c --- /dev/null +++ b/infra/core/monitor/loganalytics.bicep @@ -0,0 +1,21 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { + name: name + location: location + tags: tags + properties: any({ + retentionInDays: 30 + features: { + searchVersion: 1 + } + sku: { + name: 'PerGB2018' + } + }) +} + +output id string = logAnalytics.id +output name string = logAnalytics.name diff --git a/infra/core/monitor/monitoring.bicep b/infra/core/monitor/monitoring.bicep new file mode 100644 index 0000000..791c5eb --- /dev/null +++ b/infra/core/monitor/monitoring.bicep @@ -0,0 +1,31 @@ +param logAnalyticsName string +param applicationInsightsName string +param location string = resourceGroup().location +param tags object = {} +param disableLocalAuth bool = false + +module logAnalytics 'loganalytics.bicep' = { + name: 'loganalytics' + params: { + name: logAnalyticsName + location: location + tags: tags + } +} + +module applicationInsights 'applicationinsights.bicep' = { + name: 'applicationinsights' + params: { + name: applicationInsightsName + location: location + tags: tags + logAnalyticsWorkspaceId: logAnalytics.outputs.id + disableLocalAuth: disableLocalAuth + } +} + +output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString +output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey +output applicationInsightsName string = applicationInsights.outputs.name +output logAnalyticsWorkspaceId string = logAnalytics.outputs.id +output logAnalyticsWorkspaceName string = logAnalytics.outputs.name diff --git a/infra/core/storage/storage-account.bicep b/infra/core/storage/storage-account.bicep new file mode 100644 index 0000000..17375d2 --- /dev/null +++ b/infra/core/storage/storage-account.bicep @@ -0,0 +1,43 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param allowBlobPublicAccess bool = false +@allowed(['Enabled', 'Disabled']) +param publicNetworkAccess string = 'Enabled' +param containers array = [] +param kind string = 'StorageV2' +param minimumTlsVersion string = 'TLS1_2' +param sku object = { name: 'Standard_LRS' } +param networkAcls object = { + bypass: 'AzureServices' + defaultAction: 'Allow' +} + +resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = { + name: name + location: location + tags: tags + kind: kind + sku: sku + properties: { + minimumTlsVersion: minimumTlsVersion + allowBlobPublicAccess: allowBlobPublicAccess + publicNetworkAccess: publicNetworkAccess + allowSharedKeyAccess: false + networkAcls: networkAcls + } + + resource blobServices 'blobServices' = if (!empty(containers)) { + name: 'default' + resource container 'containers' = [for container in containers: { + name: container.name + properties: { + publicAccess: container.?publicAccess ?? 'None' + } + }] + } +} + +output name string = storage.name +output primaryEndpoints object = storage.properties.primaryEndpoints diff --git a/infra/main.bicep b/infra/main.bicep new file mode 100644 index 0000000..3b5c775 --- /dev/null +++ b/infra/main.bicep @@ -0,0 +1,171 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(64) +@description('Name of the the environment which is used to generate a short unique hash used in all resources.') +param environmentName string + +@minLength(1) +@description('Primary location for all resources') +@allowed(['australiaeast', 'eastasia', 'eastus', 'eastus2', 'northeurope', 'southcentralus', 'southeastasia', 'swedencentral', 'uksouth', 'westus2', 'eastus2euap']) +@metadata({ + azd: { + type: 'location' + } +}) +param location string +param vnetEnabled bool +param apiServiceName string = '' +param apiUserAssignedIdentityName string = '' +param applicationInsightsName string = '' +param appServicePlanName string = '' +param logAnalyticsName string = '' +param resourceGroupName string = '' +param storageAccountName string = '' +param vNetName string = '' +param disableLocalAuth bool = true + +var abbrs = loadJsonContent('./abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } +var functionAppName = !empty(apiServiceName) ? apiServiceName : '${abbrs.webSitesFunctions}api-${resourceToken}' +var deploymentStorageContainerName = 'app-package-${take(functionAppName, 32)}-${take(toLower(uniqueString(functionAppName, resourceToken)), 7)}' + +// Organize resources in a resource group +resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' + location: location + tags: tags +} + +// User assigned managed identity to be used by the function app to reach storage and service bus +module apiUserAssignedIdentity './core/identity/userAssignedIdentity.bicep' = { + name: 'apiUserAssignedIdentity' + scope: rg + params: { + location: location + tags: tags + identityName: !empty(apiUserAssignedIdentityName) ? apiUserAssignedIdentityName : '${abbrs.managedIdentityUserAssignedIdentities}api-${resourceToken}' + } +} + +// The application backend is a function app +module appServicePlan './core/host/appserviceplan.bicep' = { + name: 'appserviceplan' + scope: rg + params: { + name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}' + location: location + tags: tags + sku: { + name: 'FC1' + tier: 'FlexConsumption' + } + } +} + +module api './app/api.bicep' = { + name: 'api' + scope: rg + params: { + name: functionAppName + location: location + tags: tags + applicationInsightsName: monitoring.outputs.applicationInsightsName + appServicePlanId: appServicePlan.outputs.id + runtimeName: 'python' + runtimeVersion: '3.11' + storageAccountName: storage.outputs.name + deploymentStorageContainerName: deploymentStorageContainerName + identityId: apiUserAssignedIdentity.outputs.identityId + identityClientId: apiUserAssignedIdentity.outputs.identityClientId + appSettings: { + } + virtualNetworkSubnetId: !vnetEnabled ? '' : serviceVirtualNetwork.outputs.appSubnetID + } +} + +// Backing storage for Azure functions api +module storage './core/storage/storage-account.bicep' = { + name: 'storage' + scope: rg + params: { + name: !empty(storageAccountName) ? storageAccountName : '${abbrs.storageStorageAccounts}${resourceToken}' + location: location + tags: tags + containers: [{name: deploymentStorageContainerName}, {name: 'snippets'}] + publicNetworkAccess: vnetEnabled ? 'Disabled' : 'Enabled' + networkAcls: !vnetEnabled ? {} : { + defaultAction: 'Deny' + } + } +} + +var storageRoleDefinitionId = 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' //Storage Blob Data Owner role + +// Allow access from api to storage account using a managed identity +module storageRoleAssignmentApi 'app/storage-Access.bicep' = { + name: 'storageRoleAssignmentapi' + scope: rg + params: { + storageAccountName: storage.outputs.name + roleDefinitionID: storageRoleDefinitionId + principalID: apiUserAssignedIdentity.outputs.identityPrincipalId + } +} + +// Virtual Network & private endpoint to blob storage +module serviceVirtualNetwork 'app/vnet.bicep' = if (vnetEnabled) { + name: 'serviceVirtualNetwork' + scope: rg + params: { + location: location + tags: tags + vNetName: !empty(vNetName) ? vNetName : '${abbrs.networkVirtualNetworks}${resourceToken}' + } +} + +module storagePrivateEndpoint 'app/storage-PrivateEndpoint.bicep' = if (vnetEnabled) { + name: 'servicePrivateEndpoint' + scope: rg + params: { + location: location + tags: tags + virtualNetworkName: !empty(vNetName) ? vNetName : '${abbrs.networkVirtualNetworks}${resourceToken}' + subnetName: !vnetEnabled ? '' : serviceVirtualNetwork.outputs.peSubnetName + resourceName: storage.outputs.name + } +} + +// Monitor application with Azure Monitor +module monitoring './core/monitor/monitoring.bicep' = { + name: 'monitoring' + scope: rg + params: { + location: location + tags: tags + logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' + applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + disableLocalAuth: disableLocalAuth + } +} + +var monitoringRoleDefinitionId = '3913510d-42f4-4e42-8a64-420c390055eb' // Monitoring Metrics Publisher role ID + +// Allow access from api to application insights using a managed identity +module appInsightsRoleAssignmentApi './core/monitor/appinsights-access.bicep' = { + name: 'appInsightsRoleAssignmentapi' + scope: rg + params: { + appInsightsName: monitoring.outputs.applicationInsightsName + roleDefinitionID: monitoringRoleDefinitionId + principalID: apiUserAssignedIdentity.outputs.identityPrincipalId + } +} + +// App outputs +output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString +output AZURE_LOCATION string = location +output AZURE_TENANT_ID string = tenant().tenantId +output SERVICE_API_NAME string = api.outputs.SERVICE_API_NAME +output AZURE_FUNCTION_NAME string = api.outputs.SERVICE_API_NAME diff --git a/infra/main.parameters.json b/infra/main.parameters.json new file mode 100644 index 0000000..57b5029 --- /dev/null +++ b/infra/main.parameters.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "vnetEnabled": { + "value": "${VNET_ENABLED}" + } + } +} \ No newline at end of file From 67cde1502baaa7d678b4805beff663c058141d1e Mon Sep 17 00:00:00 2001 From: Thiago Almeida Date: Fri, 4 Apr 2025 08:06:55 -0700 Subject: [PATCH 3/4] Updating metadata --- README.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index dd08afb..0e6b063 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,16 @@ From d9e4ad14c68278ef449817b4dc0fdadfab7c4dda Mon Sep 17 00:00:00 2001 From: Thiago Almeida Date: Fri, 4 Apr 2025 08:09:11 -0700 Subject: [PATCH 4/4] Fixing misspelling --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0e6b063..97989e1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@