├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── bicep-audit.yml ├── .vscode └── launch.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── bicep ├── infra │ ├── container-apps-environment.bicep │ ├── container-registry.bicep │ ├── deploy.sh │ ├── log-analytics-workspace.bicep │ ├── main.bicep │ ├── main.json │ ├── main.parameters.json │ ├── managed-identity.bicep │ ├── service-bus.bicep │ └── virtual-network.bicep └── jobs │ ├── container-apps-job.bicep │ ├── container-registry.bicep │ ├── deploy.sh │ ├── main.bicep │ └── main.parameters.json ├── images ├── architecture.png ├── executionhistory.png ├── executionlogs.png ├── joblogs.png ├── messageflow.png └── resources.png ├── scripts ├── 00-variables.sh ├── 01-create-job.sh ├── 02-start-job.sh └── 03-view-managed-identity.sh ├── src ├── .env ├── .local ├── 00-variables.sh ├── 01-build-docker-images.sh ├── 02-run-docker-container.sh ├── 03-push-docker-image.sh ├── Dockerfile ├── requirements.txt ├── sbprocessor.py ├── sbreceiver.py └── sbsender.py └── visio └── aca-jobs.vsdx /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /.github/workflows/bicep-audit.yml: -------------------------------------------------------------------------------- 1 | name: Validate bicep templates 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - "**/*.bicep" 8 | pull_request: 9 | branches: 10 | - main 11 | paths: 12 | - "**/*.bicep" 13 | workflow_dispatch: 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | security-events: write 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Run Microsoft Security DevOps Analysis 25 | uses: microsoft/security-devops-action@preview 26 | id: msdo 27 | continue-on-error: true 28 | with: 29 | tools: templateanalyzer 30 | 31 | - name: Upload alerts to Security tab 32 | uses: github/codeql-action/upload-sarif@v3 33 | if: github.repository_owner == 'Azure-Samples' 34 | with: 35 | sarif_file: ${{ steps.msdo.outputs.sarifFile }} 36 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal", 13 | "justMyCode": false 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 | 4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | 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]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /bicep/infra/container-apps-environment.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name of the Azure Container Apps Environment.') 3 | param name string 4 | 5 | @description('Specifies the location.') 6 | param location string = resourceGroup().location 7 | 8 | @description('Specifies the resource tags.') 9 | param tags object 10 | 11 | @description('Specifies whether the environment only has an internal load balancer. These environments do not have a public static IP resource. They must provide infrastructureSubnetId if enabling this property') 12 | param internal bool = false 13 | 14 | @description('Specifies the IP range in CIDR notation assigned to the Docker bridge, network. Must not overlap with any other provided IP ranges.') 15 | param dockerBridgeCidr string 16 | 17 | @description('Specifies the IP range in CIDR notation that can be reserved for environment infrastructure IP addresses. Must not overlap with any other provided IP ranges.') 18 | param platformReservedCidr string 19 | 20 | @description('Specifies an IP address from the IP range defined by platformReservedCidr that will be reserved for the internal DNS server.') 21 | param platformReservedDnsIP string 22 | 23 | @description('Specifies whether the Azure Container Apps environment should be zone-redundant.') 24 | param zoneRedundant bool = true 25 | 26 | @description('Specifies the resource id of the infrastructure subnet.') 27 | param infrastructureSubnetId string 28 | 29 | @description('Specifies the name of the Log Analytics workspace.') 30 | param workspaceName string 31 | 32 | @description('Specifies the Azure Monitor instrumentation key used by Dapr to export Service to Service communication telemetry.') 33 | param daprAIInstrumentationKey string = '' 34 | 35 | @description('Specifies the configuration of Dapr component.') 36 | param daprAIConnectionString string = '' 37 | 38 | @description('Specifies the certificate password.') 39 | @secure() 40 | param certificatePassword string = '' 41 | 42 | @description('Specifies the PFX or PEM certificate value.') 43 | param certificateValue string = '' 44 | 45 | @description('Specifies the DNS suffix for the environment domain.') 46 | param dnsSuffix string = '' 47 | 48 | @description('Specifies workload profiles configured for the Managed Environment.') 49 | param workloadProfiles array = [] 50 | 51 | // Resources 52 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' existing = { 53 | name: workspaceName 54 | } 55 | 56 | resource environment 'Microsoft.App/managedEnvironments@2023-04-01-preview' = { 57 | name: name 58 | location: location 59 | tags: tags 60 | properties: { 61 | customDomainConfiguration: empty(certificatePassword) && empty(certificateValue) && empty(dnsSuffix)? null : { 62 | certificatePassword: certificatePassword 63 | certificateValue: certificateValue 64 | dnsSuffix: dnsSuffix 65 | } 66 | daprAIInstrumentationKey: daprAIInstrumentationKey 67 | daprAIConnectionString: daprAIConnectionString 68 | vnetConfiguration: { 69 | internal: internal 70 | infrastructureSubnetId: infrastructureSubnetId 71 | dockerBridgeCidr: dockerBridgeCidr 72 | platformReservedCidr: platformReservedCidr 73 | platformReservedDnsIP: platformReservedDnsIP 74 | } 75 | appLogsConfiguration: { 76 | destination: 'log-analytics' 77 | logAnalyticsConfiguration: { 78 | customerId: logAnalyticsWorkspace.properties.customerId 79 | sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey 80 | } 81 | } 82 | zoneRedundant: zoneRedundant 83 | workloadProfiles: workloadProfiles 84 | } 85 | } 86 | 87 | // Outputs 88 | output id string = environment.id 89 | output name string = environment.name 90 | output daprConfiguration object = environment.properties.daprConfiguration 91 | output kedaConfiguration object = environment.properties.kedaConfiguration 92 | output appLogsConfiguration object = environment.properties.appLogsConfiguration 93 | -------------------------------------------------------------------------------- /bicep/infra/container-registry.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Name of your Azure Container Registry') 3 | @minLength(5) 4 | @maxLength(50) 5 | param name string = 'acr${uniqueString(resourceGroup().id)}' 6 | 7 | @description('Enable admin user that have push / pull permission to the registry.') 8 | param adminUserEnabled bool = false 9 | 10 | @description('Tier of your Azure Container Registry.') 11 | @allowed([ 12 | 'Basic' 13 | 'Standard' 14 | 'Premium' 15 | ]) 16 | param sku string = 'Premium' 17 | 18 | @description('Specifies the resource id of the Log Analytics workspace.') 19 | param workspaceId string 20 | 21 | @description('Specifies the workspace data retention in days.') 22 | param retentionInDays int = 60 23 | 24 | @description('Specifies the location.') 25 | param location string = resourceGroup().location 26 | 27 | @description('Specifies the resource tags.') 28 | param tags object 29 | 30 | // Variables 31 | var diagnosticSettingsName = 'diagnosticSettings' 32 | var logCategories = [ 33 | 'ContainerRegistryRepositoryEvents' 34 | 'ContainerRegistryLoginEvents' 35 | ] 36 | var metricCategories = [ 37 | 'AllMetrics' 38 | ] 39 | var logs = [for category in logCategories: { 40 | category: category 41 | enabled: true 42 | retentionPolicy: { 43 | enabled: true 44 | days: retentionInDays 45 | } 46 | }] 47 | var metrics = [for category in metricCategories: { 48 | category: category 49 | enabled: true 50 | retentionPolicy: { 51 | enabled: true 52 | days: retentionInDays 53 | } 54 | }] 55 | 56 | // Resources 57 | resource containerRegistry 'Microsoft.ContainerRegistry/registries@2021-12-01-preview' = { 58 | name: name 59 | location: location 60 | tags: tags 61 | sku: { 62 | name: sku 63 | } 64 | properties: { 65 | adminUserEnabled: adminUserEnabled 66 | } 67 | } 68 | 69 | resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 70 | name: diagnosticSettingsName 71 | scope: containerRegistry 72 | properties: { 73 | workspaceId: workspaceId 74 | logs: logs 75 | metrics: metrics 76 | } 77 | } 78 | 79 | // Outputs 80 | output id string = containerRegistry.id 81 | output name string = containerRegistry.name 82 | -------------------------------------------------------------------------------- /bicep/infra/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Variables 4 | location='northeurope' 5 | deploymentName='main' 6 | prefix='Gundam' 7 | resourceGroupName="${prefix}RG" 8 | 9 | # Commands 10 | validateTemplate=1 11 | useWhatIf=1 12 | deploy=1 13 | 14 | # Template 15 | template="main.bicep" 16 | parameters="main.parameters.json" 17 | 18 | # Subscription id, subscription name, and tenant id of the current subscription 19 | subscriptionId=$(az account show --query id --output tsv) 20 | subscriptionName=$(az account show --query name --output tsv) 21 | tenantId=$(az account show --query tenantId --output tsv) 22 | 23 | # Check if the resource group already exists 24 | echo "Checking if [$resourceGroupName] resource group actually exists in the [$subscriptionName] subscription..." 25 | 26 | az group show --name $resourceGroupName &>/dev/null 27 | 28 | if [[ $? != 0 ]]; then 29 | echo "No [$resourceGroupName] resource group actually exists in the [$subscriptionName] subscription" 30 | echo "Creating [$resourceGroupName] resource group in the [$subscriptionName] subscription..." 31 | 32 | # Create the resource group 33 | az group create --name $resourceGroupName --location $location 1>/dev/null 34 | 35 | if [[ $? == 0 ]]; then 36 | echo "[$resourceGroupName] resource group successfully created in the [$subscriptionName] subscription" 37 | else 38 | echo "Failed to create [$resourceGroupName] resource group in the [$subscriptionName] subscription" 39 | exit 40 | fi 41 | else 42 | echo "[$resourceGroupName] resource group already exists in the [$subscriptionName] subscription" 43 | fi 44 | 45 | # Validate the Bicep template 46 | if [[ $validateTemplate == 1 ]]; then 47 | if [[ $useWhatIf == 1 ]]; then 48 | # Execute a deployment What-If operation at resource group scope. 49 | echo "Previewing changes deployed by [$template] Bicep template..." 50 | az deployment group what-if \ 51 | --resource-group $resourceGroupName \ 52 | --template-file $template \ 53 | --no-pretty-print \ 54 | --output jsonc \ 55 | --parameters $parameters \ 56 | --parameters location=$location \ 57 | prefix=$prefix 58 | 59 | if [[ $? == 0 ]]; then 60 | echo "[$template] Bicep template validation succeeded" 61 | else 62 | echo "Failed to validate [$template] Bicep template" 63 | exit 64 | fi 65 | else 66 | # Validate the Bicep template 67 | echo "Validating [$template] Bicep template..." 68 | output=$(az deployment group validate \ 69 | --resource-group $resourceGroupName \ 70 | --template-file $template \ 71 | --no-pretty-print \ 72 | --output jsonc \ 73 | --parameters $parameters \ 74 | --parameters location=$location \ 75 | prefix=$prefix) 76 | 77 | if [[ $? == 0 ]]; then 78 | echo "[$template] Bicep template validation succeeded" 79 | else 80 | echo "Failed to validate [$template] Bicep template" 81 | echo $output 82 | exit 83 | fi 84 | fi 85 | fi 86 | 87 | # Deploy the Bicep template 88 | if [[ $deploy == 1 ]]; then 89 | az deployment group create \ 90 | --name $deploymentName \ 91 | --resource-group $resourceGroupName \ 92 | --template-file $template \ 93 | --parameters $parameters \ 94 | --parameters location=$location \ 95 | prefix=$prefix 1>/dev/null 96 | 97 | if [[ $? == 0 ]]; then 98 | echo "[$deploymentName] deployment successfully created in the [$resourceGroupName] resource group" 99 | else 100 | echo "Failed to create [$deploymentName] deployment in the [$resourceGroupName] resource group" 101 | exit -1 102 | fi 103 | fi -------------------------------------------------------------------------------- /bicep/infra/log-analytics-workspace.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name of the Log Analytics workspace.') 3 | param name string 4 | 5 | @description('Specifies the service tier of the workspace: Free, Standalone, PerNode, Per-GB.') 6 | @allowed([ 7 | 'Free' 8 | 'Standalone' 9 | 'PerNode' 10 | 'PerGB2018' 11 | ]) 12 | param sku string = 'PerNode' 13 | 14 | @description('Specifies the workspace data retention in days. -1 means Unlimited retention for the Unlimited Sku. 730 days is the maximum allowed for all other Skus.') 15 | param retentionInDays int = 60 16 | 17 | @description('Specifies the location.') 18 | param location string = resourceGroup().location 19 | 20 | @description('Specifies the resource tags.') 21 | param tags object 22 | 23 | // Resources 24 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { 25 | name: name 26 | tags: tags 27 | location: location 28 | properties: { 29 | sku: { 30 | name: sku 31 | } 32 | retentionInDays: retentionInDays 33 | } 34 | } 35 | 36 | //Outputs 37 | output id string = logAnalyticsWorkspace.id 38 | output name string = logAnalyticsWorkspace.name 39 | output customerId string = logAnalyticsWorkspace.properties.customerId 40 | -------------------------------------------------------------------------------- /bicep/infra/main.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name prefix.') 3 | param prefix string = '$uniqueString(resourceGroup().id)' 4 | 5 | @description('Specifies whether name resources are in CamelCase, UpperCamelCase, or KebabCase.') 6 | @allowed([ 7 | 'CamelCase' 8 | 'UpperCamelCase' 9 | 'KebabCase' 10 | ]) 11 | param letterCaseType string = 'UpperCamelCase' 12 | 13 | @description('Specifies the location.') 14 | param location string = resourceGroup().location 15 | 16 | @description('Specifies the resource tags.') 17 | param tags object = { 18 | IaC: 'Bicep' 19 | Demo: 'Azure Container Apps Jobs' 20 | } 21 | 22 | @description('Specifies the name of the Azure Container Apps Environment.') 23 | param containerAppEnvironmentName string = letterCaseType == 'UpperCamelCase' ? '${toUpper(first(prefix))}${toLower(substring(prefix, 1, length(prefix) - 1))}Environment' : letterCaseType == 'CamelCase' ? '${toLower(prefix)}Environment' : '${toLower(prefix)}-environment' 24 | 25 | @description('Specifies whether the environment only has an internal load balancer. These environments do not have a public static IP resource. They must provide infrastructureSubnetId if enabling this property') 26 | param internal bool = false 27 | 28 | @description('Specifies the IP range in CIDR notation assigned to the Docker bridge, network. Must not overlap with any other provided IP ranges.') 29 | param dockerBridgeCidr string = '10.2.0.1/16' 30 | 31 | @description('Specifies the IP range in CIDR notation that can be reserved for environment infrastructure IP addresses. Must not overlap with any other provided IP ranges.') 32 | param platformReservedCidr string = '10.1.0.0/16' 33 | 34 | @description('Specifies an IP address from the IP range defined by platformReservedCidr that will be reserved for the internal DNS server.') 35 | param platformReservedDnsIP string = '10.1.0.2' 36 | 37 | @description('Specifies whether the Azure Container Apps environment should be zone-redundant.') 38 | param zoneRedundant bool = true 39 | 40 | @description('Specifies the name of the Log Analytics workspace.') 41 | param logAnalyticsWorkspaceName string = letterCaseType == 'UpperCamelCase' ? '${toUpper(first(prefix))}${toLower(substring(prefix, 1, length(prefix) - 1))}Workspace' : letterCaseType == 'CamelCase' ? '${toLower(prefix)}Workspace' : '${toLower(prefix)}-workspace' 42 | 43 | @description('Specifies the workspace data retention in days. -1 means Unlimited retention for the Unlimited Sku. 730 days is the maximum allowed for all other Skus.') 44 | param logAnalyticsRetentionInDays int = 30 45 | 46 | @description('Specifies the service tier of the workspace: Free, Standalone, PerNode, Per-GB.') 47 | @allowed([ 48 | 'Free' 49 | 'Standalone' 50 | 'PerNode' 51 | 'PerGB2018' 52 | ]) 53 | param logAnalyticsSku string = 'PerNode' 54 | 55 | @description('Specifies the name of the virtual network.') 56 | param virtualNetworkName string = letterCaseType == 'UpperCamelCase' ? '${toUpper(first(prefix))}${toLower(substring(prefix, 1, length(prefix) - 1))}Vnet' : letterCaseType == 'CamelCase' ? '${toLower(prefix)}Vnet' : '${toLower(prefix)}-vnet' 57 | 58 | @description('Specifies the address prefixes of the virtual network.') 59 | param virtualNetworkAddressPrefixes string = '10.0.0.0/8' 60 | 61 | @description('Specifies the name of a subnet for the Azure Container Apps environment infrastructure components. This subnet must be in the same VNET as the subnet defined in runtimeSubnetId. Must not overlap with any other provided IP ranges.') 62 | param infrastructureSubnetName string = 'InfrastructureSubnet' 63 | 64 | @description('Specifies the address prefix of a subnet for the Azure Container Apps environment infrastructure components. This subnet must be in the same VNET as the subnet defined in runtimeSubnetId. Must not overlap with any other provided IP ranges.') 65 | param infrastructureSubnetAddressPrefix string = '10.0.0.0/21' 66 | 67 | @description('Specifies the name of the subnet hosting the worker nodes of the user agent pool of the AKS cluster.') 68 | param privateLinkServiceSubnetName string = 'PrivateSubnet' 69 | 70 | @description('Specifies the address prefix of the subnet hosting the worker nodes of the user agent pool of the AKS cluster.') 71 | param privateLinkServiceSubnetAddressPrefix string = '10.0.16.0/21' 72 | 73 | @description('Specifies the name of the Service Bus namespace.') 74 | param serviceBusNamespace string = letterCaseType == 'UpperCamelCase' ? '${toUpper(first(prefix))}${toLower(substring(prefix, 1, length(prefix) - 1))}ServiceBus' : letterCaseType == 'CamelCase' ? '${toLower(prefix)}ServiceBus' : '${toLower(prefix)}-servicebus' 75 | 76 | @description('Enabling this property creates a Premium Service Bus Namespace in regions supported availability zones.') 77 | param serviceBusZoneRedundant bool = true 78 | 79 | @description('Specifies the name of Service Bus namespace SKU.') 80 | @allowed([ 81 | 'Basic' 82 | 'Premium' 83 | 'Standard' 84 | ]) 85 | param serviceBusSkuName string = 'Premium' 86 | 87 | @description('Specifies the messaging units for the Service Bus namespace. For Premium tier, capacity are 1,2 and 4.') 88 | param serviceBusCapacity int = 1 89 | 90 | @description('Specifies the name of the Service Bus queue.') 91 | param serviceBusQueueNames array = [ 'parameters', 'results' ] 92 | 93 | @description('Specifies the lock duration of the queue.') 94 | param serviceBusQueueLockDuration string = 'PT5M' 95 | 96 | @description('Specifies the maximum delivery count of the queue.') 97 | param serviceBusQueueMaxDeliveryCount int = 10 98 | 99 | @description('Specifies whether duplication is enabled on the queue.') 100 | param serviceBusQueueRequiresDuplicateDetection bool = false 101 | 102 | @description('Specifies whether session is enabled on the queue.') 103 | param serviceBusQueueRequiresSession bool = false 104 | 105 | @description('Specifies whether dead lettering is enabled on the queue.') 106 | param serviceBusQueueDeadLetteringOnMessageExpiration bool = false 107 | 108 | @description('Specifies the duplicate detection history time of the queue.') 109 | param serviceBusQueueDuplicateDetectionHistoryTimeWindow string = 'PT10M' 110 | 111 | @description('Specifies the name of the private link to the storage account.') 112 | param serviceBusNamespacePrivateEndpointName string = letterCaseType == 'UpperCamelCase' ? '${toUpper(first(prefix))}${toLower(substring(prefix, 1, length(prefix) - 1))}ServiceBusNamespacePrivateEndpoint' : letterCaseType == 'CamelCase' ? '${toLower(prefix)}ServiceBusNamespacePrivateEndpoint' : '${toLower(prefix)}-service-bus-namespace-private-endpoint' 113 | 114 | @description('Specifies the name of the user-defined managed identity.') 115 | param managedIdentityName string = letterCaseType == 'UpperCamelCase' ? '${toUpper(first(prefix))}${toLower(substring(prefix, 1, length(prefix) - 1))}JobManagedIdentity' : letterCaseType == 'CamelCase' ? '${toLower(prefix)}JobManagedIdentity' : '${toLower(prefix)}-job-managed-identity' 116 | 117 | @description('Specifies the name of the Azure Container Registry') 118 | @minLength(5) 119 | @maxLength(50) 120 | param acrName string = letterCaseType == 'UpperCamelCase' ? '${toUpper(first(prefix))}${toLower(substring(prefix, 1, length(prefix) - 1))}Acr' : letterCaseType == 'CamelCase' ? '${toLower(prefix)}Acr' : '${toLower(prefix)}-acr' 121 | 122 | @description('Enable admin user that have push / pull permission to the registry.') 123 | param acrAdminUserEnabled bool = false 124 | 125 | @description('Specifies the name of the private link to the Azure Container Registry.') 126 | param acrPrivateEndpointName string = letterCaseType == 'UpperCamelCase' ? '${toUpper(first(prefix))}${toLower(substring(prefix, 1, length(prefix) - 1))}AcrPrivateEndpoint' : letterCaseType == 'CamelCase' ? '${toLower(prefix)}AcrPrivateEndpoint' : '${toLower(prefix)}-acr-private-endpoint' 127 | 128 | @description('Specifies the SKU name of the Azure Container Registry.') 129 | @allowed([ 130 | 'Basic' 131 | 'Standard' 132 | 'Premium' 133 | ]) 134 | param acrSkuName string = 'Premium' 135 | 136 | // Resources 137 | module workspace 'log-analytics-workspace.bicep' = { 138 | name: 'workspace' 139 | scope: resourceGroup() 140 | params: { 141 | name: logAnalyticsWorkspaceName 142 | sku: logAnalyticsSku 143 | retentionInDays: logAnalyticsRetentionInDays 144 | location: location 145 | tags: tags 146 | } 147 | } 148 | 149 | module namespace 'service-bus.bicep' = { 150 | name: 'namespace' 151 | scope: resourceGroup() 152 | params: { 153 | name: serviceBusNamespace 154 | location: location 155 | tags: tags 156 | zoneRedundant: serviceBusZoneRedundant 157 | skuName: serviceBusSkuName 158 | capacity: serviceBusCapacity 159 | queueNames: serviceBusQueueNames 160 | lockDuration: serviceBusQueueLockDuration 161 | maxDeliveryCount: serviceBusQueueMaxDeliveryCount 162 | requiresDuplicateDetection: serviceBusQueueRequiresDuplicateDetection 163 | requiresSession: serviceBusQueueRequiresSession 164 | deadLetteringOnMessageExpiration: serviceBusQueueDeadLetteringOnMessageExpiration 165 | duplicateDetectionHistoryTimeWindow: serviceBusQueueDuplicateDetectionHistoryTimeWindow 166 | workspaceId: workspace.outputs.id 167 | } 168 | } 169 | 170 | module network 'virtual-network.bicep' = { 171 | name: 'network' 172 | scope: resourceGroup() 173 | params: { 174 | name: virtualNetworkName 175 | location: location 176 | tags: tags 177 | addressPrefixes: virtualNetworkAddressPrefixes 178 | infrastructureSubnetName: infrastructureSubnetName 179 | infrastructureSubnetAddressPrefix: infrastructureSubnetAddressPrefix 180 | privateLinkServiceSubnetName: privateLinkServiceSubnetName 181 | privateLinkServiceSubnetAddressPrefix: privateLinkServiceSubnetAddressPrefix 182 | workspaceId: workspace.outputs.id 183 | serviceBusNamespacePrivateEndpointEnabled: serviceBusSkuName == 'Premium' 184 | serviceBusNamespacePrivateEndpointName: serviceBusNamespacePrivateEndpointName 185 | serviceBusNamespaceId: namespace.outputs.id 186 | acrPrivateEndpointEnabled: acrSkuName == 'Premium' 187 | acrPrivateEndpointName: acrPrivateEndpointName 188 | acrId: acr.outputs.id 189 | } 190 | } 191 | 192 | module environment 'container-apps-environment.bicep' = { 193 | name: 'environment' 194 | scope: resourceGroup() 195 | params: { 196 | name: containerAppEnvironmentName 197 | location: location 198 | tags: tags 199 | internal: internal 200 | dockerBridgeCidr: dockerBridgeCidr 201 | platformReservedCidr: platformReservedCidr 202 | platformReservedDnsIP: platformReservedDnsIP 203 | zoneRedundant: zoneRedundant 204 | workspaceName: logAnalyticsWorkspaceName 205 | infrastructureSubnetId: network.outputs.infrastructureSubnetId 206 | } 207 | } 208 | 209 | module managedIdentity 'managed-identity.bicep' = { 210 | name: 'managedIdentity' 211 | scope: resourceGroup() 212 | params: { 213 | name: managedIdentityName 214 | location: location 215 | serviceBusNamespace: namespace.outputs.name 216 | acrName: acr.outputs.name 217 | tags: tags 218 | } 219 | } 220 | 221 | module acr 'container-registry.bicep' = { 222 | name: 'containerRegistry' 223 | params: { 224 | name: acrName 225 | sku: acrSkuName 226 | adminUserEnabled: acrAdminUserEnabled 227 | workspaceId: workspace.outputs.id 228 | retentionInDays: logAnalyticsRetentionInDays 229 | location: location 230 | tags: tags 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /bicep/infra/main.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_generator": { 6 | "name": "bicep", 7 | "version": "0.18.4.5664", 8 | "templateHash": "15046971136878174535" 9 | } 10 | }, 11 | "parameters": { 12 | "prefix": { 13 | "type": "string", 14 | "defaultValue": "$uniqueString(resourceGroup().id)", 15 | "metadata": { 16 | "description": "Specifies the name prefix." 17 | } 18 | }, 19 | "letterCaseType": { 20 | "type": "string", 21 | "defaultValue": "UpperCamelCase", 22 | "allowedValues": [ 23 | "CamelCase", 24 | "UpperCamelCase", 25 | "KebabCase" 26 | ], 27 | "metadata": { 28 | "description": "Specifies whether name resources are in CamelCase, UpperCamelCase, or KebabCase." 29 | } 30 | }, 31 | "location": { 32 | "type": "string", 33 | "defaultValue": "[resourceGroup().location]", 34 | "metadata": { 35 | "description": "Specifies the location." 36 | } 37 | }, 38 | "tags": { 39 | "type": "object", 40 | "defaultValue": { 41 | "IaC": "Bicep", 42 | "Demo": "Azure Container Apps Jobs" 43 | }, 44 | "metadata": { 45 | "description": "Specifies the resource tags." 46 | } 47 | }, 48 | "containerAppEnvironmentName": { 49 | "type": "string", 50 | "defaultValue": "[if(equals(parameters('letterCaseType'), 'UpperCamelCase'), format('{0}{1}Environment', toUpper(first(parameters('prefix'))), toLower(substring(parameters('prefix'), 1, sub(length(parameters('prefix')), 1)))), if(equals(parameters('letterCaseType'), 'CamelCase'), format('{0}Environment', toLower(parameters('prefix'))), format('{0}-environment', toLower(parameters('prefix')))))]", 51 | "metadata": { 52 | "description": "Specifies the name of the Azure Container Apps Environment." 53 | } 54 | }, 55 | "internal": { 56 | "type": "bool", 57 | "defaultValue": false, 58 | "metadata": { 59 | "description": "Specifies whether the environment only has an internal load balancer. These environments do not have a public static IP resource. They must provide infrastructureSubnetId if enabling this property" 60 | } 61 | }, 62 | "dockerBridgeCidr": { 63 | "type": "string", 64 | "defaultValue": "10.2.0.1/16", 65 | "metadata": { 66 | "description": "Specifies the IP range in CIDR notation assigned to the Docker bridge, network. Must not overlap with any other provided IP ranges." 67 | } 68 | }, 69 | "platformReservedCidr": { 70 | "type": "string", 71 | "defaultValue": "10.1.0.0/16", 72 | "metadata": { 73 | "description": "Specifies the IP range in CIDR notation that can be reserved for environment infrastructure IP addresses. Must not overlap with any other provided IP ranges." 74 | } 75 | }, 76 | "platformReservedDnsIP": { 77 | "type": "string", 78 | "defaultValue": "10.1.0.2", 79 | "metadata": { 80 | "description": "Specifies an IP address from the IP range defined by platformReservedCidr that will be reserved for the internal DNS server." 81 | } 82 | }, 83 | "zoneRedundant": { 84 | "type": "bool", 85 | "defaultValue": true, 86 | "metadata": { 87 | "description": "Specifies whether the Azure Container Apps environment should be zone-redundant." 88 | } 89 | }, 90 | "logAnalyticsWorkspaceName": { 91 | "type": "string", 92 | "defaultValue": "[if(equals(parameters('letterCaseType'), 'UpperCamelCase'), format('{0}{1}Workspace', toUpper(first(parameters('prefix'))), toLower(substring(parameters('prefix'), 1, sub(length(parameters('prefix')), 1)))), if(equals(parameters('letterCaseType'), 'CamelCase'), format('{0}Workspace', toLower(parameters('prefix'))), format('{0}-workspace', toLower(parameters('prefix')))))]", 93 | "metadata": { 94 | "description": "Specifies the name of the Log Analytics workspace." 95 | } 96 | }, 97 | "logAnalyticsRetentionInDays": { 98 | "type": "int", 99 | "defaultValue": 30, 100 | "metadata": { 101 | "description": "Specifies the workspace data retention in days. -1 means Unlimited retention for the Unlimited Sku. 730 days is the maximum allowed for all other Skus." 102 | } 103 | }, 104 | "logAnalyticsSku": { 105 | "type": "string", 106 | "defaultValue": "PerNode", 107 | "allowedValues": [ 108 | "Free", 109 | "Standalone", 110 | "PerNode", 111 | "PerGB2018" 112 | ], 113 | "metadata": { 114 | "description": "Specifies the service tier of the workspace: Free, Standalone, PerNode, Per-GB." 115 | } 116 | }, 117 | "virtualNetworkName": { 118 | "type": "string", 119 | "defaultValue": "[if(equals(parameters('letterCaseType'), 'UpperCamelCase'), format('{0}{1}Vnet', toUpper(first(parameters('prefix'))), toLower(substring(parameters('prefix'), 1, sub(length(parameters('prefix')), 1)))), if(equals(parameters('letterCaseType'), 'CamelCase'), format('{0}Vnet', toLower(parameters('prefix'))), format('{0}-vnet', toLower(parameters('prefix')))))]", 120 | "metadata": { 121 | "description": "Specifies the name of the virtual network." 122 | } 123 | }, 124 | "virtualNetworkAddressPrefixes": { 125 | "type": "string", 126 | "defaultValue": "10.0.0.0/8", 127 | "metadata": { 128 | "description": "Specifies the address prefixes of the virtual network." 129 | } 130 | }, 131 | "infrastructureSubnetName": { 132 | "type": "string", 133 | "defaultValue": "InfrastructureSubnet", 134 | "metadata": { 135 | "description": "Specifies the name of a subnet for the Azure Container Apps environment infrastructure components. This subnet must be in the same VNET as the subnet defined in runtimeSubnetId. Must not overlap with any other provided IP ranges." 136 | } 137 | }, 138 | "infrastructureSubnetAddressPrefix": { 139 | "type": "string", 140 | "defaultValue": "10.0.0.0/21", 141 | "metadata": { 142 | "description": "Specifies the address prefix of a subnet for the Azure Container Apps environment infrastructure components. This subnet must be in the same VNET as the subnet defined in runtimeSubnetId. Must not overlap with any other provided IP ranges." 143 | } 144 | }, 145 | "privateLinkServiceSubnetName": { 146 | "type": "string", 147 | "defaultValue": "PrivateSubnet", 148 | "metadata": { 149 | "description": "Specifies the name of the subnet hosting the worker nodes of the user agent pool of the AKS cluster." 150 | } 151 | }, 152 | "privateLinkServiceSubnetAddressPrefix": { 153 | "type": "string", 154 | "defaultValue": "10.0.16.0/21", 155 | "metadata": { 156 | "description": "Specifies the address prefix of the subnet hosting the worker nodes of the user agent pool of the AKS cluster." 157 | } 158 | }, 159 | "serviceBusNamespace": { 160 | "type": "string", 161 | "defaultValue": "[if(equals(parameters('letterCaseType'), 'UpperCamelCase'), format('{0}{1}ServiceBus', toUpper(first(parameters('prefix'))), toLower(substring(parameters('prefix'), 1, sub(length(parameters('prefix')), 1)))), if(equals(parameters('letterCaseType'), 'CamelCase'), format('{0}ServiceBus', toLower(parameters('prefix'))), format('{0}-servicebus', toLower(parameters('prefix')))))]", 162 | "metadata": { 163 | "description": "Specifies the name of the Service Bus namespace." 164 | } 165 | }, 166 | "serviceBusZoneRedundant": { 167 | "type": "bool", 168 | "defaultValue": true, 169 | "metadata": { 170 | "description": "Enabling this property creates a Premium Service Bus Namespace in regions supported availability zones." 171 | } 172 | }, 173 | "serviceBusSkuName": { 174 | "type": "string", 175 | "defaultValue": "Premium", 176 | "allowedValues": [ 177 | "Basic", 178 | "Premium", 179 | "Standard" 180 | ], 181 | "metadata": { 182 | "description": "Specifies the name of Service Bus namespace SKU." 183 | } 184 | }, 185 | "serviceBusCapacity": { 186 | "type": "int", 187 | "defaultValue": 1, 188 | "metadata": { 189 | "description": "Specifies the messaging units for the Service Bus namespace. For Premium tier, capacity are 1,2 and 4." 190 | } 191 | }, 192 | "serviceBusQueueNames": { 193 | "type": "array", 194 | "defaultValue": [ 195 | "parameters", 196 | "results" 197 | ], 198 | "metadata": { 199 | "description": "Specifies the name of the Service Bus queue." 200 | } 201 | }, 202 | "serviceBusQueueLockDuration": { 203 | "type": "string", 204 | "defaultValue": "PT5M", 205 | "metadata": { 206 | "description": "Specifies the lock duration of the queue." 207 | } 208 | }, 209 | "serviceBusQueueMaxDeliveryCount": { 210 | "type": "int", 211 | "defaultValue": 10, 212 | "metadata": { 213 | "description": "Specifies the maximum delivery count of the queue." 214 | } 215 | }, 216 | "serviceBusQueueRequiresDuplicateDetection": { 217 | "type": "bool", 218 | "defaultValue": false, 219 | "metadata": { 220 | "description": "Specifies whether duplication is enabled on the queue." 221 | } 222 | }, 223 | "serviceBusQueueRequiresSession": { 224 | "type": "bool", 225 | "defaultValue": false, 226 | "metadata": { 227 | "description": "Specifies whether session is enabled on the queue." 228 | } 229 | }, 230 | "serviceBusQueueDeadLetteringOnMessageExpiration": { 231 | "type": "bool", 232 | "defaultValue": false, 233 | "metadata": { 234 | "description": "Specifies whether dead lettering is enabled on the queue." 235 | } 236 | }, 237 | "serviceBusQueueDuplicateDetectionHistoryTimeWindow": { 238 | "type": "string", 239 | "defaultValue": "PT10M", 240 | "metadata": { 241 | "description": "Specifies the duplicate detection history time of the queue." 242 | } 243 | }, 244 | "serviceBusNamespacePrivateEndpointName": { 245 | "type": "string", 246 | "defaultValue": "[if(equals(parameters('letterCaseType'), 'UpperCamelCase'), format('{0}{1}ServiceBusNamespacePrivateEndpoint', toUpper(first(parameters('prefix'))), toLower(substring(parameters('prefix'), 1, sub(length(parameters('prefix')), 1)))), if(equals(parameters('letterCaseType'), 'CamelCase'), format('{0}ServiceBusNamespacePrivateEndpoint', toLower(parameters('prefix'))), format('{0}-service-bus-namespace-private-endpoint', toLower(parameters('prefix')))))]", 247 | "metadata": { 248 | "description": "Specifies the name of the private link to the storage account." 249 | } 250 | }, 251 | "managedIdentityName": { 252 | "type": "string", 253 | "defaultValue": "[if(equals(parameters('letterCaseType'), 'UpperCamelCase'), format('{0}{1}JobManagedIdentity', toUpper(first(parameters('prefix'))), toLower(substring(parameters('prefix'), 1, sub(length(parameters('prefix')), 1)))), if(equals(parameters('letterCaseType'), 'CamelCase'), format('{0}JobManagedIdentity', toLower(parameters('prefix'))), format('{0}-job-managed-identity', toLower(parameters('prefix')))))]", 254 | "metadata": { 255 | "description": "Specifies the name of the user-defined managed identity." 256 | } 257 | }, 258 | "acrName": { 259 | "type": "string", 260 | "defaultValue": "[if(equals(parameters('letterCaseType'), 'UpperCamelCase'), format('{0}{1}Acr', toUpper(first(parameters('prefix'))), toLower(substring(parameters('prefix'), 1, sub(length(parameters('prefix')), 1)))), if(equals(parameters('letterCaseType'), 'CamelCase'), format('{0}Acr', toLower(parameters('prefix'))), format('{0}-acr', toLower(parameters('prefix')))))]", 261 | "maxLength": 50, 262 | "minLength": 5, 263 | "metadata": { 264 | "description": "Specifies the name of the Azure Container Registry" 265 | } 266 | }, 267 | "acrAdminUserEnabled": { 268 | "type": "bool", 269 | "defaultValue": false, 270 | "metadata": { 271 | "description": "Enable admin user that have push / pull permission to the registry." 272 | } 273 | }, 274 | "acrPrivateEndpointName": { 275 | "type": "string", 276 | "defaultValue": "[if(equals(parameters('letterCaseType'), 'UpperCamelCase'), format('{0}{1}AcrPrivateEndpoint', toUpper(first(parameters('prefix'))), toLower(substring(parameters('prefix'), 1, sub(length(parameters('prefix')), 1)))), if(equals(parameters('letterCaseType'), 'CamelCase'), format('{0}AcrPrivateEndpoint', toLower(parameters('prefix'))), format('{0}-acr-private-endpoint', toLower(parameters('prefix')))))]", 277 | "metadata": { 278 | "description": "Specifies the name of the private link to the Azure Container Registry." 279 | } 280 | }, 281 | "acrSkuName": { 282 | "type": "string", 283 | "defaultValue": "Premium", 284 | "allowedValues": [ 285 | "Basic", 286 | "Standard", 287 | "Premium" 288 | ], 289 | "metadata": { 290 | "description": "Specifies the SKU name of the Azure Container Registry." 291 | } 292 | } 293 | }, 294 | "resources": [ 295 | { 296 | "type": "Microsoft.Resources/deployments", 297 | "apiVersion": "2022-09-01", 298 | "name": "workspace", 299 | "properties": { 300 | "expressionEvaluationOptions": { 301 | "scope": "inner" 302 | }, 303 | "mode": "Incremental", 304 | "parameters": { 305 | "name": { 306 | "value": "[parameters('logAnalyticsWorkspaceName')]" 307 | }, 308 | "sku": { 309 | "value": "[parameters('logAnalyticsSku')]" 310 | }, 311 | "retentionInDays": { 312 | "value": "[parameters('logAnalyticsRetentionInDays')]" 313 | }, 314 | "location": { 315 | "value": "[parameters('location')]" 316 | }, 317 | "tags": { 318 | "value": "[parameters('tags')]" 319 | } 320 | }, 321 | "template": { 322 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 323 | "contentVersion": "1.0.0.0", 324 | "metadata": { 325 | "_generator": { 326 | "name": "bicep", 327 | "version": "0.18.4.5664", 328 | "templateHash": "1345435483846805772" 329 | } 330 | }, 331 | "parameters": { 332 | "name": { 333 | "type": "string", 334 | "metadata": { 335 | "description": "Specifies the name of the Log Analytics workspace." 336 | } 337 | }, 338 | "sku": { 339 | "type": "string", 340 | "defaultValue": "PerNode", 341 | "allowedValues": [ 342 | "Free", 343 | "Standalone", 344 | "PerNode", 345 | "PerGB2018" 346 | ], 347 | "metadata": { 348 | "description": "Specifies the service tier of the workspace: Free, Standalone, PerNode, Per-GB." 349 | } 350 | }, 351 | "retentionInDays": { 352 | "type": "int", 353 | "defaultValue": 60, 354 | "metadata": { 355 | "description": "Specifies the workspace data retention in days. -1 means Unlimited retention for the Unlimited Sku. 730 days is the maximum allowed for all other Skus." 356 | } 357 | }, 358 | "location": { 359 | "type": "string", 360 | "defaultValue": "[resourceGroup().location]", 361 | "metadata": { 362 | "description": "Specifies the location." 363 | } 364 | }, 365 | "tags": { 366 | "type": "object", 367 | "metadata": { 368 | "description": "Specifies the resource tags." 369 | } 370 | } 371 | }, 372 | "resources": [ 373 | { 374 | "type": "Microsoft.OperationalInsights/workspaces", 375 | "apiVersion": "2021-12-01-preview", 376 | "name": "[parameters('name')]", 377 | "tags": "[parameters('tags')]", 378 | "location": "[parameters('location')]", 379 | "properties": { 380 | "sku": { 381 | "name": "[parameters('sku')]" 382 | }, 383 | "retentionInDays": "[parameters('retentionInDays')]" 384 | } 385 | } 386 | ], 387 | "outputs": { 388 | "id": { 389 | "type": "string", 390 | "value": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('name'))]" 391 | }, 392 | "name": { 393 | "type": "string", 394 | "value": "[parameters('name')]" 395 | }, 396 | "customerId": { 397 | "type": "string", 398 | "value": "[reference(resourceId('Microsoft.OperationalInsights/workspaces', parameters('name')), '2021-12-01-preview').customerId]" 399 | } 400 | } 401 | } 402 | } 403 | }, 404 | { 405 | "type": "Microsoft.Resources/deployments", 406 | "apiVersion": "2022-09-01", 407 | "name": "namespace", 408 | "properties": { 409 | "expressionEvaluationOptions": { 410 | "scope": "inner" 411 | }, 412 | "mode": "Incremental", 413 | "parameters": { 414 | "name": { 415 | "value": "[parameters('serviceBusNamespace')]" 416 | }, 417 | "location": { 418 | "value": "[parameters('location')]" 419 | }, 420 | "tags": { 421 | "value": "[parameters('tags')]" 422 | }, 423 | "zoneRedundant": { 424 | "value": "[parameters('serviceBusZoneRedundant')]" 425 | }, 426 | "skuName": { 427 | "value": "[parameters('serviceBusSkuName')]" 428 | }, 429 | "capacity": { 430 | "value": "[parameters('serviceBusCapacity')]" 431 | }, 432 | "queueNames": { 433 | "value": "[parameters('serviceBusQueueNames')]" 434 | }, 435 | "lockDuration": { 436 | "value": "[parameters('serviceBusQueueLockDuration')]" 437 | }, 438 | "maxDeliveryCount": { 439 | "value": "[parameters('serviceBusQueueMaxDeliveryCount')]" 440 | }, 441 | "requiresDuplicateDetection": { 442 | "value": "[parameters('serviceBusQueueRequiresDuplicateDetection')]" 443 | }, 444 | "requiresSession": { 445 | "value": "[parameters('serviceBusQueueRequiresSession')]" 446 | }, 447 | "deadLetteringOnMessageExpiration": { 448 | "value": "[parameters('serviceBusQueueDeadLetteringOnMessageExpiration')]" 449 | }, 450 | "duplicateDetectionHistoryTimeWindow": { 451 | "value": "[parameters('serviceBusQueueDuplicateDetectionHistoryTimeWindow')]" 452 | }, 453 | "workspaceId": { 454 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'workspace'), '2022-09-01').outputs.id.value]" 455 | } 456 | }, 457 | "template": { 458 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 459 | "contentVersion": "1.0.0.0", 460 | "metadata": { 461 | "_generator": { 462 | "name": "bicep", 463 | "version": "0.18.4.5664", 464 | "templateHash": "6642034885011448787" 465 | } 466 | }, 467 | "parameters": { 468 | "name": { 469 | "type": "string", 470 | "metadata": { 471 | "description": "Specifies the name of the Service Bus namespace." 472 | } 473 | }, 474 | "zoneRedundant": { 475 | "type": "bool", 476 | "defaultValue": true, 477 | "metadata": { 478 | "description": "Enabling this property creates a Premium Service Bus Namespace in regions supported availability zones." 479 | } 480 | }, 481 | "skuName": { 482 | "type": "string", 483 | "defaultValue": "Standard", 484 | "allowedValues": [ 485 | "Basic", 486 | "Premium", 487 | "Standard" 488 | ], 489 | "metadata": { 490 | "description": "Specifies the name of Service Bus namespace SKU." 491 | } 492 | }, 493 | "capacity": { 494 | "type": "int", 495 | "defaultValue": 1, 496 | "metadata": { 497 | "description": "Specifies the messaging units for the Service Bus namespace. For Premium tier, capacity are 1,2 and 4." 498 | } 499 | }, 500 | "queueNames": { 501 | "type": "array", 502 | "defaultValue": [], 503 | "metadata": { 504 | "description": "Specifies the name of the Service Bus queue." 505 | } 506 | }, 507 | "lockDuration": { 508 | "type": "string", 509 | "defaultValue": "PT5M", 510 | "metadata": { 511 | "description": "Specifies the lock duration of the queue." 512 | } 513 | }, 514 | "maxDeliveryCount": { 515 | "type": "int", 516 | "defaultValue": 10, 517 | "metadata": { 518 | "description": "Specifies the maximum delivery count of the queue." 519 | } 520 | }, 521 | "requiresDuplicateDetection": { 522 | "type": "bool", 523 | "defaultValue": false, 524 | "metadata": { 525 | "description": "Specifies whether duplication is enabled on the queue." 526 | } 527 | }, 528 | "requiresSession": { 529 | "type": "bool", 530 | "defaultValue": false, 531 | "metadata": { 532 | "description": "Specifies whether session is enabled on the queue." 533 | } 534 | }, 535 | "deadLetteringOnMessageExpiration": { 536 | "type": "bool", 537 | "defaultValue": false, 538 | "metadata": { 539 | "description": "Specifies whether dead lettering is enabled on the queue." 540 | } 541 | }, 542 | "duplicateDetectionHistoryTimeWindow": { 543 | "type": "string", 544 | "defaultValue": "PT10M", 545 | "metadata": { 546 | "description": "Specifies the duplicate detection history time of the queue." 547 | } 548 | }, 549 | "workspaceId": { 550 | "type": "string", 551 | "metadata": { 552 | "description": "Specifies the resource id of the Log Analytics workspace." 553 | } 554 | }, 555 | "retentionInDays": { 556 | "type": "int", 557 | "defaultValue": 60, 558 | "metadata": { 559 | "description": "Specifies the workspace data retention in days." 560 | } 561 | }, 562 | "location": { 563 | "type": "string", 564 | "defaultValue": "[resourceGroup().location]", 565 | "metadata": { 566 | "description": "Specifies the location." 567 | } 568 | }, 569 | "tags": { 570 | "type": "object", 571 | "metadata": { 572 | "description": "Specifies the resource tags." 573 | } 574 | } 575 | }, 576 | "variables": { 577 | "copy": [ 578 | { 579 | "name": "logs", 580 | "count": "[length(variables('logCategories'))]", 581 | "input": { 582 | "category": "[variables('logCategories')[copyIndex('logs')]]", 583 | "enabled": true, 584 | "retentionPolicy": { 585 | "enabled": true, 586 | "days": "[parameters('retentionInDays')]" 587 | } 588 | } 589 | }, 590 | { 591 | "name": "metrics", 592 | "count": "[length(variables('metricCategories'))]", 593 | "input": { 594 | "category": "[variables('metricCategories')[copyIndex('metrics')]]", 595 | "enabled": true, 596 | "retentionPolicy": { 597 | "enabled": true, 598 | "days": "[parameters('retentionInDays')]" 599 | } 600 | } 601 | } 602 | ], 603 | "diagnosticSettingsName": "diagnosticSettings", 604 | "logCategories": [ 605 | "OperationalLogs", 606 | "VNetAndIPFilteringLogs", 607 | "RuntimeAuditLogs", 608 | "ApplicationMetricsLogs" 609 | ], 610 | "metricCategories": [ 611 | "AllMetrics" 612 | ], 613 | "serviceBusEndpoint": "[format('{0}/AuthorizationRules/RootManageSharedAccessKey', resourceId('Microsoft.ServiceBus/namespaces', parameters('name')))]" 614 | }, 615 | "resources": [ 616 | { 617 | "type": "Microsoft.ServiceBus/namespaces", 618 | "apiVersion": "2022-10-01-preview", 619 | "name": "[parameters('name')]", 620 | "location": "[parameters('location')]", 621 | "tags": "[parameters('tags')]", 622 | "sku": { 623 | "name": "[parameters('skuName')]", 624 | "capacity": "[parameters('capacity')]" 625 | }, 626 | "properties": { 627 | "zoneRedundant": "[if(equals(parameters('skuName'), 'Premium'), parameters('zoneRedundant'), false())]" 628 | } 629 | }, 630 | { 631 | "copy": { 632 | "name": "queue", 633 | "count": "[length(parameters('queueNames'))]" 634 | }, 635 | "type": "Microsoft.ServiceBus/namespaces/queues", 636 | "apiVersion": "2022-10-01-preview", 637 | "name": "[format('{0}/{1}', parameters('name'), parameters('queueNames')[copyIndex()])]", 638 | "properties": { 639 | "lockDuration": "[parameters('lockDuration')]", 640 | "maxSizeInMegabytes": 1024, 641 | "requiresDuplicateDetection": "[parameters('requiresDuplicateDetection')]", 642 | "requiresSession": "[parameters('requiresSession')]", 643 | "deadLetteringOnMessageExpiration": "[parameters('deadLetteringOnMessageExpiration')]", 644 | "duplicateDetectionHistoryTimeWindow": "[parameters('duplicateDetectionHistoryTimeWindow')]", 645 | "maxDeliveryCount": "[parameters('maxDeliveryCount')]" 646 | }, 647 | "dependsOn": [ 648 | "[resourceId('Microsoft.ServiceBus/namespaces', parameters('name'))]" 649 | ] 650 | }, 651 | { 652 | "type": "Microsoft.Insights/diagnosticSettings", 653 | "apiVersion": "2021-05-01-preview", 654 | "scope": "[format('Microsoft.ServiceBus/namespaces/{0}', parameters('name'))]", 655 | "name": "[variables('diagnosticSettingsName')]", 656 | "properties": { 657 | "workspaceId": "[parameters('workspaceId')]", 658 | "logs": "[variables('logs')]", 659 | "metrics": "[variables('metrics')]" 660 | }, 661 | "dependsOn": [ 662 | "[resourceId('Microsoft.ServiceBus/namespaces', parameters('name'))]" 663 | ] 664 | } 665 | ], 666 | "outputs": { 667 | "id": { 668 | "type": "string", 669 | "value": "[resourceId('Microsoft.ServiceBus/namespaces', parameters('name'))]" 670 | }, 671 | "name": { 672 | "type": "string", 673 | "value": "[parameters('name')]" 674 | }, 675 | "connectionString": { 676 | "type": "string", 677 | "value": "[listKeys(variables('serviceBusEndpoint'), '2022-10-01-preview').primaryConnectionString]" 678 | }, 679 | "queues": { 680 | "type": "array", 681 | "copy": { 682 | "count": "[length(parameters('queueNames'))]", 683 | "input": { 684 | "name": "[parameters('name')]", 685 | "id": "[resourceId('Microsoft.ServiceBus/namespaces/queues', parameters('name'), parameters('queueNames')[copyIndex()])]" 686 | } 687 | } 688 | } 689 | } 690 | } 691 | }, 692 | "dependsOn": [ 693 | "[resourceId('Microsoft.Resources/deployments', 'workspace')]" 694 | ] 695 | }, 696 | { 697 | "type": "Microsoft.Resources/deployments", 698 | "apiVersion": "2022-09-01", 699 | "name": "network", 700 | "properties": { 701 | "expressionEvaluationOptions": { 702 | "scope": "inner" 703 | }, 704 | "mode": "Incremental", 705 | "parameters": { 706 | "name": { 707 | "value": "[parameters('virtualNetworkName')]" 708 | }, 709 | "location": { 710 | "value": "[parameters('location')]" 711 | }, 712 | "tags": { 713 | "value": "[parameters('tags')]" 714 | }, 715 | "addressPrefixes": { 716 | "value": "[parameters('virtualNetworkAddressPrefixes')]" 717 | }, 718 | "infrastructureSubnetName": { 719 | "value": "[parameters('infrastructureSubnetName')]" 720 | }, 721 | "infrastructureSubnetAddressPrefix": { 722 | "value": "[parameters('infrastructureSubnetAddressPrefix')]" 723 | }, 724 | "privateLinkServiceSubnetName": { 725 | "value": "[parameters('privateLinkServiceSubnetName')]" 726 | }, 727 | "privateLinkServiceSubnetAddressPrefix": { 728 | "value": "[parameters('privateLinkServiceSubnetAddressPrefix')]" 729 | }, 730 | "workspaceId": { 731 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'workspace'), '2022-09-01').outputs.id.value]" 732 | }, 733 | "serviceBusNamespacePrivateEndpointEnabled": { 734 | "value": "[equals(parameters('serviceBusSkuName'), 'Premium')]" 735 | }, 736 | "serviceBusNamespacePrivateEndpointName": { 737 | "value": "[parameters('serviceBusNamespacePrivateEndpointName')]" 738 | }, 739 | "serviceBusNamespaceId": { 740 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'namespace'), '2022-09-01').outputs.id.value]" 741 | }, 742 | "acrPrivateEndpointEnabled": { 743 | "value": "[equals(parameters('acrSkuName'), 'Premium')]" 744 | }, 745 | "acrPrivateEndpointName": { 746 | "value": "[parameters('acrPrivateEndpointName')]" 747 | }, 748 | "acrId": { 749 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'containerRegistry'), '2022-09-01').outputs.id.value]" 750 | } 751 | }, 752 | "template": { 753 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 754 | "contentVersion": "1.0.0.0", 755 | "metadata": { 756 | "_generator": { 757 | "name": "bicep", 758 | "version": "0.18.4.5664", 759 | "templateHash": "7146871086920983355" 760 | } 761 | }, 762 | "parameters": { 763 | "name": { 764 | "type": "string", 765 | "metadata": { 766 | "description": "Specifies the name of the Azure Container Apps Environment." 767 | } 768 | }, 769 | "addressPrefixes": { 770 | "type": "string", 771 | "defaultValue": "10.0.0.0/8", 772 | "metadata": { 773 | "description": "Specifies the address prefixes of the virtual network." 774 | } 775 | }, 776 | "infrastructureSubnetName": { 777 | "type": "string", 778 | "defaultValue": "InfrastructureSubnet", 779 | "metadata": { 780 | "description": "Specifies the name of a subnet for the Azure Container Apps environment infrastructure components. This subnet must be in the same VNET as the subnet defined in runtimeSubnetId. Must not overlap with any other provided IP ranges." 781 | } 782 | }, 783 | "infrastructureSubnetAddressPrefix": { 784 | "type": "string", 785 | "defaultValue": "10.0.0.0/21", 786 | "metadata": { 787 | "description": "Specifies the address prefix of a subnet for the Azure Container Apps environment infrastructure components. This subnet must be in the same VNET as the subnet defined in runtimeSubnetId. Must not overlap with any other provided IP ranges." 788 | } 789 | }, 790 | "privateLinkServiceSubnetName": { 791 | "type": "string", 792 | "defaultValue": "PlsSubnet", 793 | "metadata": { 794 | "description": "Specifies the name of the subnet hosting the worker nodes of the user agent pool of the AKS cluster." 795 | } 796 | }, 797 | "privateLinkServiceSubnetAddressPrefix": { 798 | "type": "string", 799 | "defaultValue": "10.0.16.0/21", 800 | "metadata": { 801 | "description": "Specifies the address prefix of the subnet hosting the worker nodes of the user agent pool of the AKS cluster." 802 | } 803 | }, 804 | "serviceBusNamespacePrivateEndpointEnabled": { 805 | "type": "bool", 806 | "defaultValue": false, 807 | "metadata": { 808 | "description": "Specifies whether enabling the private link to the Azure Service Bus namespace." 809 | } 810 | }, 811 | "serviceBusNamespacePrivateEndpointName": { 812 | "type": "string", 813 | "defaultValue": "ServiceBusNamespacePrivateEndpoint", 814 | "metadata": { 815 | "description": "Specifies the name of the private link to the Azure Service Bus namespace." 816 | } 817 | }, 818 | "serviceBusNamespaceId": { 819 | "type": "string", 820 | "metadata": { 821 | "description": "Specifies the resource id of the Azure Service Bus namespace." 822 | } 823 | }, 824 | "acrPrivateEndpointEnabled": { 825 | "type": "bool", 826 | "defaultValue": false, 827 | "metadata": { 828 | "description": "Specifies whether enabling the private link to the Azure Container Registry." 829 | } 830 | }, 831 | "acrPrivateEndpointName": { 832 | "type": "string", 833 | "defaultValue": "AcrPrivateEndpoint", 834 | "metadata": { 835 | "description": "Specifies the name of the private link to the Azure Container Registry." 836 | } 837 | }, 838 | "acrId": { 839 | "type": "string", 840 | "metadata": { 841 | "description": "Specifies the resource id of the Azure Container Registry." 842 | } 843 | }, 844 | "workspaceId": { 845 | "type": "string", 846 | "metadata": { 847 | "description": "Specifies the resource id of the Log Analytics workspace." 848 | } 849 | }, 850 | "retentionInDays": { 851 | "type": "int", 852 | "defaultValue": 60, 853 | "metadata": { 854 | "description": "Specifies the workspace data retention in days." 855 | } 856 | }, 857 | "location": { 858 | "type": "string", 859 | "defaultValue": "[resourceGroup().location]", 860 | "metadata": { 861 | "description": "Specifies the location." 862 | } 863 | }, 864 | "tags": { 865 | "type": "object", 866 | "metadata": { 867 | "description": "Specifies the resource tags." 868 | } 869 | } 870 | }, 871 | "variables": { 872 | "copy": [ 873 | { 874 | "name": "vnetLogs", 875 | "count": "[length(variables('vnetLogCategories'))]", 876 | "input": { 877 | "category": "[variables('vnetLogCategories')[copyIndex('vnetLogs')]]", 878 | "enabled": true, 879 | "retentionPolicy": { 880 | "enabled": true, 881 | "days": "[parameters('retentionInDays')]" 882 | } 883 | } 884 | }, 885 | { 886 | "name": "vnetMetrics", 887 | "count": "[length(variables('vnetMetricCategories'))]", 888 | "input": { 889 | "category": "[variables('vnetMetricCategories')[copyIndex('vnetMetrics')]]", 890 | "enabled": true, 891 | "retentionPolicy": { 892 | "enabled": true, 893 | "days": "[parameters('retentionInDays')]" 894 | } 895 | } 896 | } 897 | ], 898 | "diagnosticSettingsName": "diagnosticSettings", 899 | "vnetLogCategories": [ 900 | "VMProtectionAlerts" 901 | ], 902 | "vnetMetricCategories": [ 903 | "AllMetrics" 904 | ] 905 | }, 906 | "resources": [ 907 | { 908 | "type": "Microsoft.Network/virtualNetworks", 909 | "apiVersion": "2022-07-01", 910 | "name": "[parameters('name')]", 911 | "location": "[parameters('location')]", 912 | "tags": "[parameters('tags')]", 913 | "properties": { 914 | "addressSpace": { 915 | "addressPrefixes": [ 916 | "[parameters('addressPrefixes')]" 917 | ] 918 | }, 919 | "subnets": [ 920 | { 921 | "name": "[parameters('infrastructureSubnetName')]", 922 | "properties": { 923 | "addressPrefix": "[parameters('infrastructureSubnetAddressPrefix')]", 924 | "delegations": [], 925 | "privateEndpointNetworkPolicies": "Disabled", 926 | "privateLinkServiceNetworkPolicies": "Enabled" 927 | } 928 | }, 929 | { 930 | "name": "[parameters('privateLinkServiceSubnetName')]", 931 | "properties": { 932 | "addressPrefix": "[parameters('privateLinkServiceSubnetAddressPrefix')]", 933 | "delegations": [], 934 | "privateEndpointNetworkPolicies": "Disabled", 935 | "privateLinkServiceNetworkPolicies": "Disabled" 936 | } 937 | } 938 | ], 939 | "virtualNetworkPeerings": [], 940 | "enableDdosProtection": false 941 | } 942 | }, 943 | { 944 | "condition": "[parameters('serviceBusNamespacePrivateEndpointEnabled')]", 945 | "type": "Microsoft.Network/privateDnsZones", 946 | "apiVersion": "2020-06-01", 947 | "name": "[format('privatelink.{0}', if(equals(toLower(environment().name), 'azureusgovernment'), 'servicebus.usgovcloudapi.net', 'servicebus.windows.net'))]", 948 | "location": "global", 949 | "tags": "[parameters('tags')]" 950 | }, 951 | { 952 | "condition": "[parameters('acrPrivateEndpointEnabled')]", 953 | "type": "Microsoft.Network/privateDnsZones", 954 | "apiVersion": "2020-06-01", 955 | "name": "[format('privatelink.{0}', if(equals(toLower(environment().name), 'azureusgovernment'), 'azurecr.us', 'azurecr.io'))]", 956 | "location": "global", 957 | "tags": "[parameters('tags')]" 958 | }, 959 | { 960 | "condition": "[parameters('serviceBusNamespacePrivateEndpointEnabled')]", 961 | "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", 962 | "apiVersion": "2020-06-01", 963 | "name": "[format('{0}/{1}', format('privatelink.{0}', if(equals(toLower(environment().name), 'azureusgovernment'), 'servicebus.usgovcloudapi.net', 'servicebus.windows.net')), format('link_to_{0}', toLower(parameters('name'))))]", 964 | "location": "global", 965 | "properties": { 966 | "registrationEnabled": false, 967 | "virtualNetwork": { 968 | "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('name'))]" 969 | } 970 | }, 971 | "dependsOn": [ 972 | "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.{0}', if(equals(toLower(environment().name), 'azureusgovernment'), 'servicebus.usgovcloudapi.net', 'servicebus.windows.net')))]", 973 | "[resourceId('Microsoft.Network/virtualNetworks', parameters('name'))]" 974 | ] 975 | }, 976 | { 977 | "condition": "[parameters('acrPrivateEndpointEnabled')]", 978 | "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", 979 | "apiVersion": "2020-06-01", 980 | "name": "[format('{0}/{1}', format('privatelink.{0}', if(equals(toLower(environment().name), 'azureusgovernment'), 'azurecr.us', 'azurecr.io')), format('link_to_{0}', toLower(parameters('name'))))]", 981 | "location": "global", 982 | "properties": { 983 | "registrationEnabled": false, 984 | "virtualNetwork": { 985 | "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('name'))]" 986 | } 987 | }, 988 | "dependsOn": [ 989 | "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.{0}', if(equals(toLower(environment().name), 'azureusgovernment'), 'azurecr.us', 'azurecr.io')))]", 990 | "[resourceId('Microsoft.Network/virtualNetworks', parameters('name'))]" 991 | ] 992 | }, 993 | { 994 | "condition": "[parameters('serviceBusNamespacePrivateEndpointEnabled')]", 995 | "type": "Microsoft.Network/privateEndpoints", 996 | "apiVersion": "2021-08-01", 997 | "name": "[parameters('serviceBusNamespacePrivateEndpointName')]", 998 | "location": "[parameters('location')]", 999 | "tags": "[parameters('tags')]", 1000 | "properties": { 1001 | "privateLinkServiceConnections": [ 1002 | { 1003 | "name": "[parameters('serviceBusNamespacePrivateEndpointName')]", 1004 | "properties": { 1005 | "privateLinkServiceId": "[parameters('serviceBusNamespaceId')]", 1006 | "groupIds": [ 1007 | "namespace" 1008 | ] 1009 | } 1010 | } 1011 | ], 1012 | "subnet": { 1013 | "id": "[format('{0}/subnets/{1}', resourceId('Microsoft.Network/virtualNetworks', parameters('name')), parameters('privateLinkServiceSubnetName'))]" 1014 | } 1015 | }, 1016 | "dependsOn": [ 1017 | "[resourceId('Microsoft.Network/virtualNetworks', parameters('name'))]" 1018 | ] 1019 | }, 1020 | { 1021 | "condition": "[parameters('serviceBusNamespacePrivateEndpointEnabled')]", 1022 | "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", 1023 | "apiVersion": "2021-08-01", 1024 | "name": "[format('{0}/{1}', parameters('serviceBusNamespacePrivateEndpointName'), 'PrivateDnsZoneGroupName')]", 1025 | "properties": { 1026 | "privateDnsZoneConfigs": [ 1027 | { 1028 | "name": "dnsConfig", 1029 | "properties": { 1030 | "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.{0}', if(equals(toLower(environment().name), 'azureusgovernment'), 'servicebus.usgovcloudapi.net', 'servicebus.windows.net')))]" 1031 | } 1032 | } 1033 | ] 1034 | }, 1035 | "dependsOn": [ 1036 | "[resourceId('Microsoft.Network/privateEndpoints', parameters('serviceBusNamespacePrivateEndpointName'))]", 1037 | "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.{0}', if(equals(toLower(environment().name), 'azureusgovernment'), 'servicebus.usgovcloudapi.net', 'servicebus.windows.net')))]" 1038 | ] 1039 | }, 1040 | { 1041 | "condition": "[parameters('acrPrivateEndpointEnabled')]", 1042 | "type": "Microsoft.Network/privateEndpoints", 1043 | "apiVersion": "2022-09-01", 1044 | "name": "[parameters('acrPrivateEndpointName')]", 1045 | "location": "[parameters('location')]", 1046 | "tags": "[parameters('tags')]", 1047 | "properties": { 1048 | "privateLinkServiceConnections": [ 1049 | { 1050 | "name": "[parameters('acrPrivateEndpointName')]", 1051 | "properties": { 1052 | "privateLinkServiceId": "[parameters('acrId')]", 1053 | "groupIds": [ 1054 | "registry" 1055 | ] 1056 | } 1057 | } 1058 | ], 1059 | "subnet": { 1060 | "id": "[format('{0}/subnets/{1}', resourceId('Microsoft.Network/virtualNetworks', parameters('name')), parameters('privateLinkServiceSubnetName'))]" 1061 | } 1062 | }, 1063 | "dependsOn": [ 1064 | "[resourceId('Microsoft.Network/virtualNetworks', parameters('name'))]" 1065 | ] 1066 | }, 1067 | { 1068 | "condition": "[parameters('acrPrivateEndpointEnabled')]", 1069 | "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", 1070 | "apiVersion": "2022-09-01", 1071 | "name": "[format('{0}/{1}', parameters('acrPrivateEndpointName'), 'acrPrivateDnsZoneGroup')]", 1072 | "properties": { 1073 | "privateDnsZoneConfigs": [ 1074 | { 1075 | "name": "dnsConfig", 1076 | "properties": { 1077 | "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.{0}', if(equals(toLower(environment().name), 'azureusgovernment'), 'azurecr.us', 'azurecr.io')))]" 1078 | } 1079 | } 1080 | ] 1081 | }, 1082 | "dependsOn": [ 1083 | "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.{0}', if(equals(toLower(environment().name), 'azureusgovernment'), 'azurecr.us', 'azurecr.io')))]", 1084 | "[resourceId('Microsoft.Network/privateEndpoints', parameters('acrPrivateEndpointName'))]" 1085 | ] 1086 | }, 1087 | { 1088 | "type": "Microsoft.Insights/diagnosticSettings", 1089 | "apiVersion": "2021-05-01-preview", 1090 | "scope": "[format('Microsoft.Network/virtualNetworks/{0}', parameters('name'))]", 1091 | "name": "[variables('diagnosticSettingsName')]", 1092 | "properties": { 1093 | "workspaceId": "[parameters('workspaceId')]", 1094 | "logs": "[variables('vnetLogs')]", 1095 | "metrics": "[variables('vnetMetrics')]" 1096 | }, 1097 | "dependsOn": [ 1098 | "[resourceId('Microsoft.Network/virtualNetworks', parameters('name'))]" 1099 | ] 1100 | } 1101 | ], 1102 | "outputs": { 1103 | "id": { 1104 | "type": "string", 1105 | "value": "[resourceId('Microsoft.Network/virtualNetworks', parameters('name'))]" 1106 | }, 1107 | "name": { 1108 | "type": "string", 1109 | "value": "[parameters('name')]" 1110 | }, 1111 | "infrastructureSubnetId": { 1112 | "type": "string", 1113 | "value": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('name'), parameters('infrastructureSubnetName'))]" 1114 | }, 1115 | "privateLinkServiceSubnetId": { 1116 | "type": "string", 1117 | "value": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('name'), parameters('privateLinkServiceSubnetName'))]" 1118 | } 1119 | } 1120 | } 1121 | }, 1122 | "dependsOn": [ 1123 | "[resourceId('Microsoft.Resources/deployments', 'containerRegistry')]", 1124 | "[resourceId('Microsoft.Resources/deployments', 'namespace')]", 1125 | "[resourceId('Microsoft.Resources/deployments', 'workspace')]" 1126 | ] 1127 | }, 1128 | { 1129 | "type": "Microsoft.Resources/deployments", 1130 | "apiVersion": "2022-09-01", 1131 | "name": "environment", 1132 | "properties": { 1133 | "expressionEvaluationOptions": { 1134 | "scope": "inner" 1135 | }, 1136 | "mode": "Incremental", 1137 | "parameters": { 1138 | "name": { 1139 | "value": "[parameters('containerAppEnvironmentName')]" 1140 | }, 1141 | "location": { 1142 | "value": "[parameters('location')]" 1143 | }, 1144 | "tags": { 1145 | "value": "[parameters('tags')]" 1146 | }, 1147 | "internal": { 1148 | "value": "[parameters('internal')]" 1149 | }, 1150 | "dockerBridgeCidr": { 1151 | "value": "[parameters('dockerBridgeCidr')]" 1152 | }, 1153 | "platformReservedCidr": { 1154 | "value": "[parameters('platformReservedCidr')]" 1155 | }, 1156 | "platformReservedDnsIP": { 1157 | "value": "[parameters('platformReservedDnsIP')]" 1158 | }, 1159 | "zoneRedundant": { 1160 | "value": "[parameters('zoneRedundant')]" 1161 | }, 1162 | "workspaceName": { 1163 | "value": "[parameters('logAnalyticsWorkspaceName')]" 1164 | }, 1165 | "infrastructureSubnetId": { 1166 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'network'), '2022-09-01').outputs.infrastructureSubnetId.value]" 1167 | } 1168 | }, 1169 | "template": { 1170 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 1171 | "contentVersion": "1.0.0.0", 1172 | "metadata": { 1173 | "_generator": { 1174 | "name": "bicep", 1175 | "version": "0.18.4.5664", 1176 | "templateHash": "14449268650626830847" 1177 | } 1178 | }, 1179 | "parameters": { 1180 | "name": { 1181 | "type": "string", 1182 | "metadata": { 1183 | "description": "Specifies the name of the Azure Container Apps Environment." 1184 | } 1185 | }, 1186 | "location": { 1187 | "type": "string", 1188 | "defaultValue": "[resourceGroup().location]", 1189 | "metadata": { 1190 | "description": "Specifies the location." 1191 | } 1192 | }, 1193 | "tags": { 1194 | "type": "object", 1195 | "metadata": { 1196 | "description": "Specifies the resource tags." 1197 | } 1198 | }, 1199 | "internal": { 1200 | "type": "bool", 1201 | "defaultValue": false, 1202 | "metadata": { 1203 | "description": "Specifies whether the environment only has an internal load balancer. These environments do not have a public static IP resource. They must provide infrastructureSubnetId if enabling this property" 1204 | } 1205 | }, 1206 | "dockerBridgeCidr": { 1207 | "type": "string", 1208 | "metadata": { 1209 | "description": "Specifies the IP range in CIDR notation assigned to the Docker bridge, network. Must not overlap with any other provided IP ranges." 1210 | } 1211 | }, 1212 | "platformReservedCidr": { 1213 | "type": "string", 1214 | "metadata": { 1215 | "description": "Specifies the IP range in CIDR notation that can be reserved for environment infrastructure IP addresses. Must not overlap with any other provided IP ranges." 1216 | } 1217 | }, 1218 | "platformReservedDnsIP": { 1219 | "type": "string", 1220 | "metadata": { 1221 | "description": "Specifies an IP address from the IP range defined by platformReservedCidr that will be reserved for the internal DNS server." 1222 | } 1223 | }, 1224 | "zoneRedundant": { 1225 | "type": "bool", 1226 | "defaultValue": true, 1227 | "metadata": { 1228 | "description": "Specifies whether the Azure Container Apps environment should be zone-redundant." 1229 | } 1230 | }, 1231 | "infrastructureSubnetId": { 1232 | "type": "string", 1233 | "metadata": { 1234 | "description": "Specifies the resource id of the infrastructure subnet." 1235 | } 1236 | }, 1237 | "workspaceName": { 1238 | "type": "string", 1239 | "metadata": { 1240 | "description": "Specifies the name of the Log Analytics workspace." 1241 | } 1242 | }, 1243 | "daprAIInstrumentationKey": { 1244 | "type": "string", 1245 | "defaultValue": "", 1246 | "metadata": { 1247 | "description": "Specifies the Azure Monitor instrumentation key used by Dapr to export Service to Service communication telemetry." 1248 | } 1249 | }, 1250 | "daprAIConnectionString": { 1251 | "type": "string", 1252 | "defaultValue": "", 1253 | "metadata": { 1254 | "description": "Specifies the configuration of Dapr component." 1255 | } 1256 | }, 1257 | "certificatePassword": { 1258 | "type": "securestring", 1259 | "defaultValue": "", 1260 | "metadata": { 1261 | "description": "Specifies the certificate password." 1262 | } 1263 | }, 1264 | "certificateValue": { 1265 | "type": "string", 1266 | "defaultValue": "", 1267 | "metadata": { 1268 | "description": "Specifies the PFX or PEM certificate value." 1269 | } 1270 | }, 1271 | "dnsSuffix": { 1272 | "type": "string", 1273 | "defaultValue": "", 1274 | "metadata": { 1275 | "description": "Specifies the DNS suffix for the environment domain." 1276 | } 1277 | }, 1278 | "workloadProfiles": { 1279 | "type": "array", 1280 | "defaultValue": [], 1281 | "metadata": { 1282 | "description": "Specifies workload profiles configured for the Managed Environment." 1283 | } 1284 | } 1285 | }, 1286 | "resources": [ 1287 | { 1288 | "type": "Microsoft.App/managedEnvironments", 1289 | "apiVersion": "2023-04-01-preview", 1290 | "name": "[parameters('name')]", 1291 | "location": "[parameters('location')]", 1292 | "tags": "[parameters('tags')]", 1293 | "properties": { 1294 | "customDomainConfiguration": "[if(and(and(empty(parameters('certificatePassword')), empty(parameters('certificateValue'))), empty(parameters('dnsSuffix'))), null(), createObject('certificatePassword', parameters('certificatePassword'), 'certificateValue', parameters('certificateValue'), 'dnsSuffix', parameters('dnsSuffix')))]", 1295 | "daprAIInstrumentationKey": "[parameters('daprAIInstrumentationKey')]", 1296 | "daprAIConnectionString": "[parameters('daprAIConnectionString')]", 1297 | "vnetConfiguration": { 1298 | "internal": "[parameters('internal')]", 1299 | "infrastructureSubnetId": "[parameters('infrastructureSubnetId')]", 1300 | "dockerBridgeCidr": "[parameters('dockerBridgeCidr')]", 1301 | "platformReservedCidr": "[parameters('platformReservedCidr')]", 1302 | "platformReservedDnsIP": "[parameters('platformReservedDnsIP')]" 1303 | }, 1304 | "appLogsConfiguration": { 1305 | "destination": "log-analytics", 1306 | "logAnalyticsConfiguration": { 1307 | "customerId": "[reference(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspaceName')), '2021-12-01-preview').customerId]", 1308 | "sharedKey": "[listKeys(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspaceName')), '2021-12-01-preview').primarySharedKey]" 1309 | } 1310 | }, 1311 | "zoneRedundant": "[parameters('zoneRedundant')]", 1312 | "workloadProfiles": "[parameters('workloadProfiles')]" 1313 | } 1314 | } 1315 | ], 1316 | "outputs": { 1317 | "id": { 1318 | "type": "string", 1319 | "value": "[resourceId('Microsoft.App/managedEnvironments', parameters('name'))]" 1320 | }, 1321 | "name": { 1322 | "type": "string", 1323 | "value": "[parameters('name')]" 1324 | }, 1325 | "daprConfiguration": { 1326 | "type": "object", 1327 | "value": "[reference(resourceId('Microsoft.App/managedEnvironments', parameters('name')), '2023-04-01-preview').daprConfiguration]" 1328 | }, 1329 | "kedaConfiguration": { 1330 | "type": "object", 1331 | "value": "[reference(resourceId('Microsoft.App/managedEnvironments', parameters('name')), '2023-04-01-preview').kedaConfiguration]" 1332 | }, 1333 | "appLogsConfiguration": { 1334 | "type": "object", 1335 | "value": "[reference(resourceId('Microsoft.App/managedEnvironments', parameters('name')), '2023-04-01-preview').appLogsConfiguration]" 1336 | } 1337 | } 1338 | } 1339 | }, 1340 | "dependsOn": [ 1341 | "[resourceId('Microsoft.Resources/deployments', 'network')]" 1342 | ] 1343 | }, 1344 | { 1345 | "type": "Microsoft.Resources/deployments", 1346 | "apiVersion": "2022-09-01", 1347 | "name": "managedIdentity", 1348 | "properties": { 1349 | "expressionEvaluationOptions": { 1350 | "scope": "inner" 1351 | }, 1352 | "mode": "Incremental", 1353 | "parameters": { 1354 | "name": { 1355 | "value": "[parameters('managedIdentityName')]" 1356 | }, 1357 | "location": { 1358 | "value": "[parameters('location')]" 1359 | }, 1360 | "serviceBusNamespace": { 1361 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'namespace'), '2022-09-01').outputs.name.value]" 1362 | }, 1363 | "acrName": { 1364 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'containerRegistry'), '2022-09-01').outputs.name.value]" 1365 | }, 1366 | "tags": { 1367 | "value": "[parameters('tags')]" 1368 | } 1369 | }, 1370 | "template": { 1371 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 1372 | "contentVersion": "1.0.0.0", 1373 | "metadata": { 1374 | "_generator": { 1375 | "name": "bicep", 1376 | "version": "0.18.4.5664", 1377 | "templateHash": "7072514701931445872" 1378 | } 1379 | }, 1380 | "parameters": { 1381 | "name": { 1382 | "type": "string", 1383 | "metadata": { 1384 | "description": "Specifies the name of the user-defined managed identity." 1385 | } 1386 | }, 1387 | "serviceBusNamespace": { 1388 | "type": "string", 1389 | "metadata": { 1390 | "description": "Specifies the name of the Service Bus namespace." 1391 | } 1392 | }, 1393 | "acrName": { 1394 | "type": "string", 1395 | "metadata": { 1396 | "description": "Specifies the name of the Azure Container Registry" 1397 | } 1398 | }, 1399 | "location": { 1400 | "type": "string", 1401 | "defaultValue": "[resourceGroup().location]", 1402 | "metadata": { 1403 | "description": "Specifies the location." 1404 | } 1405 | }, 1406 | "tags": { 1407 | "type": "object", 1408 | "metadata": { 1409 | "description": "Specifies the resource tags." 1410 | } 1411 | } 1412 | }, 1413 | "variables": { 1414 | "serviceBusDataOwnerRoleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '090c5cfd-751d-490a-894a-3ce6f1109419')]", 1415 | "acrPullRoleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')]" 1416 | }, 1417 | "resources": [ 1418 | { 1419 | "type": "Microsoft.ManagedIdentity/userAssignedIdentities", 1420 | "apiVersion": "2023-01-31", 1421 | "name": "[parameters('name')]", 1422 | "location": "[parameters('location')]", 1423 | "tags": "[parameters('tags')]" 1424 | }, 1425 | { 1426 | "type": "Microsoft.Authorization/roleAssignments", 1427 | "apiVersion": "2022-04-01", 1428 | "scope": "[format('Microsoft.ServiceBus/namespaces/{0}', parameters('serviceBusNamespace'))]", 1429 | "name": "[guid(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), resourceId('Microsoft.ServiceBus/namespaces', parameters('serviceBusNamespace')), variables('serviceBusDataOwnerRoleDefinitionId'))]", 1430 | "properties": { 1431 | "roleDefinitionId": "[variables('serviceBusDataOwnerRoleDefinitionId')]", 1432 | "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), '2023-01-31').principalId]", 1433 | "principalType": "ServicePrincipal" 1434 | }, 1435 | "dependsOn": [ 1436 | "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name'))]" 1437 | ] 1438 | }, 1439 | { 1440 | "type": "Microsoft.Authorization/roleAssignments", 1441 | "apiVersion": "2022-04-01", 1442 | "scope": "[format('Microsoft.ContainerRegistry/registries/{0}', parameters('acrName'))]", 1443 | "name": "[guid(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), variables('acrPullRoleDefinitionId'))]", 1444 | "properties": { 1445 | "roleDefinitionId": "[variables('acrPullRoleDefinitionId')]", 1446 | "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), '2023-01-31').principalId]", 1447 | "principalType": "ServicePrincipal" 1448 | }, 1449 | "dependsOn": [ 1450 | "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name'))]" 1451 | ] 1452 | } 1453 | ], 1454 | "outputs": { 1455 | "id": { 1456 | "type": "string", 1457 | "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name'))]" 1458 | }, 1459 | "name": { 1460 | "type": "string", 1461 | "value": "[parameters('name')]" 1462 | }, 1463 | "clientId": { 1464 | "type": "string", 1465 | "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), '2023-01-31').clientId]" 1466 | }, 1467 | "principalId": { 1468 | "type": "string", 1469 | "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), '2023-01-31').principalId]" 1470 | } 1471 | } 1472 | } 1473 | }, 1474 | "dependsOn": [ 1475 | "[resourceId('Microsoft.Resources/deployments', 'containerRegistry')]", 1476 | "[resourceId('Microsoft.Resources/deployments', 'namespace')]" 1477 | ] 1478 | }, 1479 | { 1480 | "type": "Microsoft.Resources/deployments", 1481 | "apiVersion": "2022-09-01", 1482 | "name": "containerRegistry", 1483 | "properties": { 1484 | "expressionEvaluationOptions": { 1485 | "scope": "inner" 1486 | }, 1487 | "mode": "Incremental", 1488 | "parameters": { 1489 | "name": { 1490 | "value": "[parameters('acrName')]" 1491 | }, 1492 | "sku": { 1493 | "value": "[parameters('acrSkuName')]" 1494 | }, 1495 | "adminUserEnabled": { 1496 | "value": "[parameters('acrAdminUserEnabled')]" 1497 | }, 1498 | "workspaceId": { 1499 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'workspace'), '2022-09-01').outputs.id.value]" 1500 | }, 1501 | "retentionInDays": { 1502 | "value": "[parameters('logAnalyticsRetentionInDays')]" 1503 | }, 1504 | "location": { 1505 | "value": "[parameters('location')]" 1506 | }, 1507 | "tags": { 1508 | "value": "[parameters('tags')]" 1509 | } 1510 | }, 1511 | "template": { 1512 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 1513 | "contentVersion": "1.0.0.0", 1514 | "metadata": { 1515 | "_generator": { 1516 | "name": "bicep", 1517 | "version": "0.18.4.5664", 1518 | "templateHash": "13018784895517120517" 1519 | } 1520 | }, 1521 | "parameters": { 1522 | "name": { 1523 | "type": "string", 1524 | "defaultValue": "[format('acr{0}', uniqueString(resourceGroup().id))]", 1525 | "maxLength": 50, 1526 | "minLength": 5, 1527 | "metadata": { 1528 | "description": "Name of your Azure Container Registry" 1529 | } 1530 | }, 1531 | "adminUserEnabled": { 1532 | "type": "bool", 1533 | "defaultValue": false, 1534 | "metadata": { 1535 | "description": "Enable admin user that have push / pull permission to the registry." 1536 | } 1537 | }, 1538 | "sku": { 1539 | "type": "string", 1540 | "defaultValue": "Premium", 1541 | "allowedValues": [ 1542 | "Basic", 1543 | "Standard", 1544 | "Premium" 1545 | ], 1546 | "metadata": { 1547 | "description": "Tier of your Azure Container Registry." 1548 | } 1549 | }, 1550 | "workspaceId": { 1551 | "type": "string", 1552 | "metadata": { 1553 | "description": "Specifies the resource id of the Log Analytics workspace." 1554 | } 1555 | }, 1556 | "retentionInDays": { 1557 | "type": "int", 1558 | "defaultValue": 60, 1559 | "metadata": { 1560 | "description": "Specifies the workspace data retention in days." 1561 | } 1562 | }, 1563 | "location": { 1564 | "type": "string", 1565 | "defaultValue": "[resourceGroup().location]", 1566 | "metadata": { 1567 | "description": "Specifies the location." 1568 | } 1569 | }, 1570 | "tags": { 1571 | "type": "object", 1572 | "metadata": { 1573 | "description": "Specifies the resource tags." 1574 | } 1575 | } 1576 | }, 1577 | "variables": { 1578 | "copy": [ 1579 | { 1580 | "name": "logs", 1581 | "count": "[length(variables('logCategories'))]", 1582 | "input": { 1583 | "category": "[variables('logCategories')[copyIndex('logs')]]", 1584 | "enabled": true, 1585 | "retentionPolicy": { 1586 | "enabled": true, 1587 | "days": "[parameters('retentionInDays')]" 1588 | } 1589 | } 1590 | }, 1591 | { 1592 | "name": "metrics", 1593 | "count": "[length(variables('metricCategories'))]", 1594 | "input": { 1595 | "category": "[variables('metricCategories')[copyIndex('metrics')]]", 1596 | "enabled": true, 1597 | "retentionPolicy": { 1598 | "enabled": true, 1599 | "days": "[parameters('retentionInDays')]" 1600 | } 1601 | } 1602 | } 1603 | ], 1604 | "diagnosticSettingsName": "diagnosticSettings", 1605 | "logCategories": [ 1606 | "ContainerRegistryRepositoryEvents", 1607 | "ContainerRegistryLoginEvents" 1608 | ], 1609 | "metricCategories": [ 1610 | "AllMetrics" 1611 | ] 1612 | }, 1613 | "resources": [ 1614 | { 1615 | "type": "Microsoft.ContainerRegistry/registries", 1616 | "apiVersion": "2021-12-01-preview", 1617 | "name": "[parameters('name')]", 1618 | "location": "[parameters('location')]", 1619 | "tags": "[parameters('tags')]", 1620 | "sku": { 1621 | "name": "[parameters('sku')]" 1622 | }, 1623 | "properties": { 1624 | "adminUserEnabled": "[parameters('adminUserEnabled')]" 1625 | } 1626 | }, 1627 | { 1628 | "type": "Microsoft.Insights/diagnosticSettings", 1629 | "apiVersion": "2021-05-01-preview", 1630 | "scope": "[format('Microsoft.ContainerRegistry/registries/{0}', parameters('name'))]", 1631 | "name": "[variables('diagnosticSettingsName')]", 1632 | "properties": { 1633 | "workspaceId": "[parameters('workspaceId')]", 1634 | "logs": "[variables('logs')]", 1635 | "metrics": "[variables('metrics')]" 1636 | }, 1637 | "dependsOn": [ 1638 | "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]" 1639 | ] 1640 | } 1641 | ], 1642 | "outputs": { 1643 | "id": { 1644 | "type": "string", 1645 | "value": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]" 1646 | }, 1647 | "name": { 1648 | "type": "string", 1649 | "value": "[parameters('name')]" 1650 | } 1651 | } 1652 | } 1653 | }, 1654 | "dependsOn": [ 1655 | "[resourceId('Microsoft.Resources/deployments', 'workspace')]" 1656 | ] 1657 | } 1658 | ] 1659 | } -------------------------------------------------------------------------------- /bicep/infra/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | } 6 | } -------------------------------------------------------------------------------- /bicep/infra/managed-identity.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name of the user-defined managed identity.') 3 | param name string 4 | 5 | @description('Specifies the name of the Service Bus namespace.') 6 | param serviceBusNamespace string 7 | 8 | @description('Specifies the name of the Azure Container Registry') 9 | param acrName string 10 | 11 | @description('Specifies the location.') 12 | param location string = resourceGroup().location 13 | 14 | @description('Specifies the resource tags.') 15 | param tags object 16 | 17 | // Variables 18 | var serviceBusDataOwnerRoleDefinitionId = resourceId('Microsoft.Authorization/roleDefinitions', '090c5cfd-751d-490a-894a-3ce6f1109419') 19 | var acrPullRoleDefinitionId = resourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') 20 | 21 | // Existing Resources 22 | resource namespace 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' existing= { 23 | name: serviceBusNamespace 24 | } 25 | 26 | resource acr 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = { 27 | name: acrName 28 | } 29 | 30 | // Managed Identity 31 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 32 | name: name 33 | location: location 34 | tags: tags 35 | } 36 | 37 | // Role assignments 38 | resource serviceBusDataOwnerRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 39 | name: guid(managedIdentity.id, namespace.id, serviceBusDataOwnerRoleDefinitionId) 40 | scope: namespace 41 | properties: { 42 | roleDefinitionId: serviceBusDataOwnerRoleDefinitionId 43 | principalId: managedIdentity.properties.principalId 44 | principalType: 'ServicePrincipal' 45 | } 46 | } 47 | 48 | resource acrPullRoleDefinitionAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 49 | name: guid(managedIdentity.id, acr.id, acrPullRoleDefinitionId) 50 | scope: acr 51 | properties: { 52 | roleDefinitionId: acrPullRoleDefinitionId 53 | principalId: managedIdentity.properties.principalId 54 | principalType: 'ServicePrincipal' 55 | } 56 | } 57 | 58 | // Outputs 59 | output id string = managedIdentity.id 60 | output name string = managedIdentity.name 61 | output clientId string = managedIdentity.properties.clientId 62 | output principalId string = managedIdentity.properties.principalId 63 | -------------------------------------------------------------------------------- /bicep/infra/service-bus.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name of the Service Bus namespace.') 3 | param name string 4 | 5 | @description('Enabling this property creates a Premium Service Bus Namespace in regions supported availability zones.') 6 | param zoneRedundant bool = true 7 | 8 | @description('Specifies the name of Service Bus namespace SKU.') 9 | @allowed([ 10 | 'Basic' 11 | 'Premium' 12 | 'Standard' 13 | ]) 14 | param skuName string = 'Standard' 15 | 16 | @description('Specifies the messaging units for the Service Bus namespace. For Premium tier, capacity are 1,2 and 4.') 17 | param capacity int = 1 18 | 19 | @description('Specifies the name of the Service Bus queue.') 20 | param queueNames array = [] 21 | 22 | @description('Specifies the lock duration of the queue.') 23 | param lockDuration string = 'PT5M' 24 | 25 | @description('Specifies the maximum delivery count of the queue.') 26 | param maxDeliveryCount int = 10 27 | 28 | @description('Specifies whether duplication is enabled on the queue.') 29 | param requiresDuplicateDetection bool = false 30 | 31 | @description('Specifies whether session is enabled on the queue.') 32 | param requiresSession bool = false 33 | 34 | @description('Specifies whether dead lettering is enabled on the queue.') 35 | param deadLetteringOnMessageExpiration bool = false 36 | 37 | @description('Specifies the duplicate detection history time of the queue.') 38 | param duplicateDetectionHistoryTimeWindow string = 'PT10M' 39 | 40 | @description('Specifies the resource id of the Log Analytics workspace.') 41 | param workspaceId string 42 | 43 | @description('Specifies the workspace data retention in days.') 44 | param retentionInDays int = 60 45 | 46 | @description('Specifies the location.') 47 | param location string = resourceGroup().location 48 | 49 | @description('Specifies the resource tags.') 50 | param tags object 51 | 52 | // Variables 53 | var diagnosticSettingsName = 'diagnosticSettings' 54 | var logCategories = [ 55 | 'OperationalLogs' 56 | 'VNetAndIPFilteringLogs' 57 | 'RuntimeAuditLogs' 58 | 'ApplicationMetricsLogs' 59 | ] 60 | var metricCategories = [ 61 | 'AllMetrics' 62 | ] 63 | var logs = [for category in logCategories: { 64 | category: category 65 | enabled: true 66 | retentionPolicy: { 67 | enabled: true 68 | days: retentionInDays 69 | } 70 | }] 71 | var metrics = [for category in metricCategories: { 72 | category: category 73 | enabled: true 74 | retentionPolicy: { 75 | enabled: true 76 | days: retentionInDays 77 | } 78 | }] 79 | 80 | var serviceBusEndpoint = '${namespace.id}/AuthorizationRules/RootManageSharedAccessKey' 81 | var serviceBusConnectionString = listKeys(serviceBusEndpoint, namespace.apiVersion).primaryConnectionString 82 | 83 | // Resources 84 | resource namespace 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' = { 85 | name: name 86 | location: location 87 | tags: tags 88 | sku: { 89 | name: skuName 90 | capacity: capacity 91 | } 92 | properties: { 93 | zoneRedundant: skuName == 'Premium' ? zoneRedundant : false 94 | } 95 | } 96 | 97 | resource queue 'Microsoft.ServiceBus/namespaces/queues@2022-10-01-preview' = [for queueName in queueNames: { 98 | parent: namespace 99 | name: queueName 100 | properties: { 101 | lockDuration: lockDuration 102 | maxSizeInMegabytes: 1024 103 | requiresDuplicateDetection: requiresDuplicateDetection 104 | requiresSession: requiresSession 105 | deadLetteringOnMessageExpiration: deadLetteringOnMessageExpiration 106 | duplicateDetectionHistoryTimeWindow: duplicateDetectionHistoryTimeWindow 107 | maxDeliveryCount: maxDeliveryCount 108 | } 109 | }] 110 | 111 | resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 112 | name: diagnosticSettingsName 113 | scope: namespace 114 | properties: { 115 | workspaceId: workspaceId 116 | logs: logs 117 | metrics: metrics 118 | } 119 | } 120 | 121 | // Outputs 122 | output id string = namespace.id 123 | output name string = namespace.name 124 | output connectionString string = serviceBusConnectionString 125 | output queues array = [for (queueName, i) in queueNames: { 126 | name: name 127 | id: queue[i].id 128 | }] 129 | -------------------------------------------------------------------------------- /bicep/infra/virtual-network.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name of the Azure Container Apps Environment.') 3 | param name string 4 | 5 | @description('Specifies the address prefixes of the virtual network.') 6 | param addressPrefixes string = '10.0.0.0/8' 7 | 8 | @description('Specifies the name of a subnet for the Azure Container Apps environment infrastructure components. This subnet must be in the same VNET as the subnet defined in runtimeSubnetId. Must not overlap with any other provided IP ranges.') 9 | param infrastructureSubnetName string = 'InfrastructureSubnet' 10 | 11 | @description('Specifies the address prefix of a subnet for the Azure Container Apps environment infrastructure components. This subnet must be in the same VNET as the subnet defined in runtimeSubnetId. Must not overlap with any other provided IP ranges.') 12 | param infrastructureSubnetAddressPrefix string = '10.0.0.0/21' 13 | 14 | @description('Specifies the name of the subnet hosting the worker nodes of the user agent pool of the AKS cluster.') 15 | param privateLinkServiceSubnetName string = 'PlsSubnet' 16 | 17 | @description('Specifies the address prefix of the subnet hosting the worker nodes of the user agent pool of the AKS cluster.') 18 | param privateLinkServiceSubnetAddressPrefix string = '10.0.16.0/21' 19 | 20 | @description('Specifies whether enabling the private link to the Azure Service Bus namespace.') 21 | param serviceBusNamespacePrivateEndpointEnabled bool = false 22 | 23 | @description('Specifies the name of the private link to the Azure Service Bus namespace.') 24 | param serviceBusNamespacePrivateEndpointName string = 'ServiceBusNamespacePrivateEndpoint' 25 | 26 | @description('Specifies the resource id of the Azure Service Bus namespace.') 27 | param serviceBusNamespaceId string 28 | 29 | @description('Specifies whether enabling the private link to the Azure Container Registry.') 30 | param acrPrivateEndpointEnabled bool = false 31 | 32 | @description('Specifies the name of the private link to the Azure Container Registry.') 33 | param acrPrivateEndpointName string = 'AcrPrivateEndpoint' 34 | 35 | @description('Specifies the resource id of the Azure Container Registry.') 36 | param acrId string 37 | 38 | @description('Specifies the resource id of the Log Analytics workspace.') 39 | param workspaceId string 40 | 41 | @description('Specifies the workspace data retention in days.') 42 | param retentionInDays int = 60 43 | 44 | @description('Specifies the location.') 45 | param location string = resourceGroup().location 46 | 47 | @description('Specifies the resource tags.') 48 | param tags object 49 | 50 | // Variables 51 | var diagnosticSettingsName = 'diagnosticSettings' 52 | var vnetLogCategories = [ 53 | 'VMProtectionAlerts' 54 | ] 55 | var vnetMetricCategories = [ 56 | 'AllMetrics' 57 | ] 58 | var vnetLogs = [for category in vnetLogCategories: { 59 | category: category 60 | enabled: true 61 | retentionPolicy: { 62 | enabled: true 63 | days: retentionInDays 64 | } 65 | }] 66 | var vnetMetrics = [for category in vnetMetricCategories: { 67 | category: category 68 | enabled: true 69 | retentionPolicy: { 70 | enabled: true 71 | days: retentionInDays 72 | } 73 | }] 74 | 75 | // Resources 76 | resource vnet 'Microsoft.Network/virtualNetworks@2022-07-01' = { 77 | name: name 78 | location: location 79 | tags: tags 80 | properties: { 81 | addressSpace: { 82 | addressPrefixes: [ 83 | addressPrefixes 84 | ] 85 | } 86 | subnets: [ 87 | { 88 | name: infrastructureSubnetName 89 | properties: { 90 | addressPrefix: infrastructureSubnetAddressPrefix 91 | delegations: [] 92 | privateEndpointNetworkPolicies: 'Disabled' 93 | privateLinkServiceNetworkPolicies: 'Enabled' 94 | } 95 | } 96 | { 97 | name: privateLinkServiceSubnetName 98 | properties: { 99 | addressPrefix: privateLinkServiceSubnetAddressPrefix 100 | delegations: [] 101 | privateEndpointNetworkPolicies: 'Disabled' 102 | privateLinkServiceNetworkPolicies: 'Disabled' 103 | } 104 | } 105 | ] 106 | virtualNetworkPeerings: [] 107 | enableDdosProtection: false 108 | } 109 | } 110 | 111 | // Private DNS Zones 112 | resource serviceBusPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (serviceBusNamespacePrivateEndpointEnabled) { 113 | name: 'privatelink.${toLower(environment().name) == 'azureusgovernment' ? 'servicebus.usgovcloudapi.net' : 'servicebus.windows.net'}' 114 | location: 'global' 115 | tags: tags 116 | } 117 | 118 | resource acrPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (acrPrivateEndpointEnabled) { 119 | name: 'privatelink.${toLower(environment().name) == 'azureusgovernment' ? 'azurecr.us' : 'azurecr.io'}' 120 | location: 'global' 121 | tags: tags 122 | } 123 | 124 | // Virtual Network Links 125 | resource serviceBusPrivateDnsZoneVirtualNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (serviceBusNamespacePrivateEndpointEnabled) { 126 | parent:serviceBusPrivateDnsZone 127 | name: 'link_to_${toLower(name)}' 128 | location: 'global' 129 | properties: { 130 | registrationEnabled: false 131 | virtualNetwork: { 132 | id: vnet.id 133 | } 134 | } 135 | } 136 | 137 | resource acrPrivateDnsZoneVirtualNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (acrPrivateEndpointEnabled) { 138 | parent: acrPrivateDnsZone 139 | name: 'link_to_${toLower(name)}' 140 | location: 'global' 141 | properties: { 142 | registrationEnabled: false 143 | virtualNetwork: { 144 | id: vnet.id 145 | } 146 | } 147 | } 148 | 149 | // Private Endpoints and Private DNS Zone Groups 150 | resource serviceBusNamespacePrivateEndpoint 'Microsoft.Network/privateEndpoints@2021-08-01' = if (serviceBusNamespacePrivateEndpointEnabled) { 151 | name: serviceBusNamespacePrivateEndpointName 152 | location: location 153 | tags: tags 154 | properties: { 155 | privateLinkServiceConnections: [ 156 | { 157 | name: serviceBusNamespacePrivateEndpointName 158 | properties: { 159 | privateLinkServiceId: serviceBusNamespaceId 160 | groupIds: [ 161 | 'namespace' 162 | ] 163 | } 164 | } 165 | ] 166 | subnet: { 167 | id: '${vnet.id}/subnets/${privateLinkServiceSubnetName}' 168 | } 169 | } 170 | } 171 | 172 | resource serviceBusNamespacePrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2021-08-01' = if (serviceBusNamespacePrivateEndpointEnabled) { 173 | parent: serviceBusNamespacePrivateEndpoint 174 | name: 'PrivateDnsZoneGroupName' 175 | properties: { 176 | privateDnsZoneConfigs: [ 177 | { 178 | name: 'dnsConfig' 179 | properties: { 180 | privateDnsZoneId: serviceBusPrivateDnsZone.id 181 | } 182 | } 183 | ] 184 | } 185 | } 186 | 187 | resource acrPrivateEndpoint 'Microsoft.Network/privateEndpoints@2022-09-01' = if (acrPrivateEndpointEnabled) { 188 | name: acrPrivateEndpointName 189 | location: location 190 | tags: tags 191 | properties: { 192 | privateLinkServiceConnections: [ 193 | { 194 | name: acrPrivateEndpointName 195 | properties: { 196 | privateLinkServiceId: acrId 197 | groupIds: [ 198 | 'registry' 199 | ] 200 | } 201 | } 202 | ] 203 | subnet: { 204 | id: '${vnet.id}/subnets/${privateLinkServiceSubnetName}' 205 | } 206 | } 207 | } 208 | 209 | resource acrPrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-09-01' = if (acrPrivateEndpointEnabled) { 210 | parent: acrPrivateEndpoint 211 | name: 'acrPrivateDnsZoneGroup' 212 | properties: { 213 | privateDnsZoneConfigs: [ 214 | { 215 | name: 'dnsConfig' 216 | properties: { 217 | privateDnsZoneId: acrPrivateDnsZone.id 218 | } 219 | } 220 | ] 221 | } 222 | } 223 | 224 | // Diagnostic Settings 225 | resource vnetDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 226 | name: diagnosticSettingsName 227 | scope: vnet 228 | properties: { 229 | workspaceId: workspaceId 230 | logs: vnetLogs 231 | metrics: vnetMetrics 232 | } 233 | } 234 | 235 | // Outputs 236 | // Outputs 237 | output id string = vnet.id 238 | output name string = vnet.name 239 | output infrastructureSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, infrastructureSubnetName) 240 | output privateLinkServiceSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, privateLinkServiceSubnetName) 241 | -------------------------------------------------------------------------------- /bicep/jobs/container-apps-job.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name of the Azure Container Apps Job.') 3 | param name string 4 | 5 | @description('Specifies the location.') 6 | param location string = resourceGroup().location 7 | 8 | @description('Specifies the resource tags.') 9 | param tags object 10 | 11 | @description('Specifies the resource id of the user-defined managed identity.') 12 | param managedIdentityId string 13 | 14 | @description('Specifies the resource id of the Azure Container Apps Environment.') 15 | param environmentId string 16 | 17 | @description('Specifies a workload profile name to pin for container app execution.') 18 | param workloadProfileName string = '' 19 | 20 | @description('Specifies a list of volume definitions for the Container App.') 21 | param volumes array = [] 22 | 23 | @description('Specifies the Collection of private container registry credentials used by a Container apps job. For more information, see https://learn.microsoft.com/en-us/azure/templates/microsoft.app/jobs?pivots=deployment-language-bicep#registrycredentials') 24 | param registries array = [] 25 | 26 | @description('Specifies the maximum number of retries before failing the job.') 27 | param replicaRetryLimit int = 1 28 | 29 | @description('Specifies the maximum number of seconds a Container Apps job replica is allowed to run.') 30 | param replicaTimeout int = 60 31 | 32 | @description('Specifies the Collection of secrets used by a Container Apps job. For more information, see https://learn.microsoft.com/en-us/azure/templates/microsoft.app/jobs?pivots=deployment-language-bicep#secret') 33 | param secrets array = [] 34 | 35 | @description('Specified the trigger type for the Azure Container Apps Job.') 36 | @allowed([ 37 | 'Event' 38 | 'Manual' 39 | 'Schedule' 40 | ]) 41 | param triggerType string = 'Manual' 42 | 43 | @description('Specifies the Cron formatted repeating schedule ("* * * * *") of a Cron Job.') 44 | param cronExpression string = '* * * * *' 45 | 46 | @description('Specifies the number of parallel replicas of a Container Apps job that can run at a given time.') 47 | param parallelism int = 1 48 | 49 | @description('Specifies the minimum number of successful replica completions before overall Container Apps job completion.') 50 | param replicaCompletionCount int = 1 51 | 52 | @description('Specifies the minimum number of job executions to run per polling interval.') 53 | param minExecutions int = 1 54 | 55 | @description('Specifies the maximum number of job executions to run per polling interval.') 56 | param maxExecutions int = 10 57 | 58 | @description('Specifies the polling interval in seconds.') 59 | param pollingInterval int = 60 60 | 61 | @description('Specifies the scaling rules. In event-driven jobs, each Container Apps job typically processes a single event, and a scaling rule determines the number of job replicas to run.') 62 | param rules array = [] 63 | 64 | @description('Specifies the container start command arguments.') 65 | param args array = [] 66 | 67 | @description('Specifies the container start command.') 68 | param command array = [] 69 | 70 | @description('Specifies the container environment variables.') 71 | param env array = [] 72 | 73 | @description('Specifies the container image.') 74 | param containerImage string 75 | 76 | @description('Specifies the container name.') 77 | param containerName string = 'main' 78 | 79 | @description('Specifies the Required CPU in cores, e.g. 0.5 for the first Azure Container Apps Job. Specify a decimal value as a string. For example, specify 0.5 for 1/2 core.') 80 | param cpu string = '0.25' 81 | 82 | @description('Specifies the Required memory in gigabytes for the second Azure Container Apps Job. E.g. 1.5 Specify a decimal value as a string. For example, specify 1.5 for 1.5 GB.') 83 | param memory string = '0.5Gi' 84 | 85 | @description('Specifies the container volume mounts.') 86 | param volumeMounts array = [] 87 | 88 | // Resources 89 | resource job 'Microsoft.App/jobs@2023-04-01-preview' = { 90 | name: toLower(name) 91 | location: location 92 | tags: tags 93 | identity: { 94 | type: 'UserAssigned' 95 | userAssignedIdentities: { 96 | '${managedIdentityId}': {} 97 | } 98 | } 99 | properties: { 100 | configuration: { 101 | manualTriggerConfig: triggerType == 'Manual' ? { 102 | replicaCompletionCount: replicaCompletionCount 103 | parallelism: parallelism 104 | } : null 105 | scheduleTriggerConfig: triggerType == 'Schedule' ? { 106 | cronExpression: cronExpression 107 | replicaCompletionCount: replicaCompletionCount 108 | parallelism: parallelism 109 | } : null 110 | eventTriggerConfig: triggerType == 'Event' ? { 111 | replicaCompletionCount: replicaCompletionCount 112 | parallelism: parallelism 113 | scale: { 114 | maxExecutions: maxExecutions 115 | minExecutions: minExecutions 116 | pollingInterval: pollingInterval 117 | rules: rules 118 | } 119 | } : null 120 | registries: registries 121 | replicaRetryLimit: replicaRetryLimit 122 | replicaTimeout: replicaTimeout 123 | secrets: secrets 124 | triggerType: triggerType 125 | } 126 | environmentId: environmentId 127 | template: { 128 | containers: [ 129 | { 130 | args: args 131 | command: command 132 | env: env 133 | image: containerImage 134 | name: containerName 135 | resources: { 136 | cpu: json(cpu) 137 | memory: memory 138 | } 139 | volumeMounts: volumeMounts 140 | } 141 | ] 142 | volumes: volumes 143 | } 144 | workloadProfileName: workloadProfileName 145 | } 146 | } 147 | 148 | // Outputs 149 | output id string = job.id 150 | output name string = job.name 151 | -------------------------------------------------------------------------------- /bicep/jobs/container-registry.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name of the Azure Container Registry.') 3 | param name string 4 | 5 | @description('Specifies the principal id of the user-defined managed identity.') 6 | param managedIdentityPrincipalId string 7 | 8 | // Variables 9 | var acrPullRoleDefinitionId = resourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') 10 | 11 | // Resources 12 | resource acr 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = { 13 | name: name 14 | } 15 | 16 | resource acrPullRoleDefinitionAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 17 | name: guid(managedIdentityPrincipalId, acr.id, acrPullRoleDefinitionId) 18 | scope: acr 19 | properties: { 20 | roleDefinitionId: acrPullRoleDefinitionId 21 | principalId: managedIdentityPrincipalId 22 | principalType: 'ServicePrincipal' 23 | } 24 | } 25 | 26 | //Outputs 27 | output id string = acr.id 28 | output name string = acr.name 29 | output loginServer string = acr.properties.loginServer 30 | -------------------------------------------------------------------------------- /bicep/jobs/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Variables 4 | location='northeurope' 5 | deploymentName='main' 6 | prefix='Gundam' 7 | resourceGroupName="${prefix}RG" 8 | 9 | # Commands 10 | validateTemplate=1 11 | useWhatIf=1 12 | deploy=1 13 | 14 | # Template 15 | template="main.bicep" 16 | parameters="main.parameters.json" 17 | 18 | # Subscription id, subscription name, and tenant id of the current subscription 19 | subscriptionId=$(az account show --query id --output tsv) 20 | subscriptionName=$(az account show --query name --output tsv) 21 | tenantId=$(az account show --query tenantId --output tsv) 22 | 23 | # Check if the resource group already exists 24 | echo "Checking if [$resourceGroupName] resource group actually exists in the [$subscriptionName] subscription..." 25 | 26 | az group show --name $resourceGroupName &>/dev/null 27 | 28 | if [[ $? != 0 ]]; then 29 | echo "No [$resourceGroupName] resource group actually exists in the [$subscriptionName] subscription" 30 | echo "Creating [$resourceGroupName] resource group in the [$subscriptionName] subscription..." 31 | 32 | # Create the resource group 33 | az group create --name $resourceGroupName --location $location 1>/dev/null 34 | 35 | if [[ $? == 0 ]]; then 36 | echo "[$resourceGroupName] resource group successfully created in the [$subscriptionName] subscription" 37 | else 38 | echo "Failed to create [$resourceGroupName] resource group in the [$subscriptionName] subscription" 39 | exit 40 | fi 41 | else 42 | echo "[$resourceGroupName] resource group already exists in the [$subscriptionName] subscription" 43 | fi 44 | 45 | # Validate the Bicep template 46 | if [[ $validateTemplate == 1 ]]; then 47 | if [[ $useWhatIf == 1 ]]; then 48 | # Execute a deployment What-If operation at resource group scope. 49 | echo "Previewing changes deployed by [$template] Bicep template..." 50 | az deployment group what-if \ 51 | --resource-group $resourceGroupName \ 52 | --template-file $template \ 53 | --no-pretty-print \ 54 | --output jsonc \ 55 | --parameters $parameters \ 56 | --parameters location=$location \ 57 | prefix=$prefix 58 | 59 | if [[ $? == 0 ]]; then 60 | echo "[$template] Bicep template validation succeeded" 61 | else 62 | echo "Failed to validate [$template] Bicep template" 63 | exit 64 | fi 65 | else 66 | # Validate the Bicep template 67 | echo "Validating [$template] Bicep template..." 68 | output=$(az deployment group validate \ 69 | --resource-group $resourceGroupName \ 70 | --template-file $template \ 71 | --output jsonc \ 72 | --parameters $parameters \ 73 | --parameters location=$location \ 74 | prefix=$prefix) 75 | 76 | if [[ $? == 0 ]]; then 77 | echo "[$template] Bicep template validation succeeded" 78 | else 79 | echo "Failed to validate [$template] Bicep template" 80 | echo $output 81 | exit 82 | fi 83 | fi 84 | fi 85 | 86 | # Deploy the Bicep template 87 | if [[ $deploy == 1 ]]; then 88 | az deployment group create \ 89 | --name $deploymentName \ 90 | --resource-group $resourceGroupName \ 91 | --template-file $template \ 92 | --parameters $parameters \ 93 | --parameters location=$location \ 94 | prefix=$prefix 1>/dev/null 95 | 96 | if [[ $? == 0 ]]; then 97 | echo "[$deploymentName] deployment successfully created in the [$resourceGroupName] resource group" 98 | else 99 | echo "Failed to create [$deploymentName] deployment in the [$resourceGroupName] resource group" 100 | exit -1 101 | fi 102 | fi -------------------------------------------------------------------------------- /bicep/jobs/main.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name prefix.') 3 | param prefix string = '$uniqueString(resourceGroup().id)' 4 | 5 | @description('Specifies whether name resources are in CamelCase, UpperCamelCase, or KebabCase.') 6 | @allowed([ 7 | 'CamelCase' 8 | 'UpperCamelCase' 9 | 'KebabCase' 10 | ]) 11 | param letterCaseType string = 'UpperCamelCase' 12 | 13 | @description('Specifies the location.') 14 | param location string = resourceGroup().location 15 | 16 | @description('Specifies the resource tags.') 17 | param tags object = { 18 | IaC: 'Bicep' 19 | Demo: 'Azure Container Apps Jobs' 20 | } 21 | 22 | @description('Specifies the name of the Azure Container Apps Environment.') 23 | param containerAppEnvironmentName string = letterCaseType == 'UpperCamelCase' ? '${toUpper(first(prefix))}${toLower(substring(prefix, 1, length(prefix) - 1))}Environment' : letterCaseType == 'CamelCase' ? '${toLower(prefix)}Environment' : '${toLower(prefix)}-environment' 24 | 25 | @description('Specifies the Azure Container Registry name.') 26 | param acrName string = letterCaseType == 'UpperCamelCase' ? '${toUpper(first(prefix))}${toLower(substring(prefix, 1, length(prefix) - 1))}Acr' : letterCaseType == 'CamelCase' ? '${toLower(prefix)}Acr' : '${toLower(prefix)}-acr' 27 | 28 | @description('Specifies the name of the Service Bus namespace.') 29 | param serviceBusNamespace string = letterCaseType == 'UpperCamelCase' ? '${toUpper(first(prefix))}${toLower(substring(prefix, 1, length(prefix) - 1))}ServiceBus' : letterCaseType == 'CamelCase' ? '${toLower(prefix)}ServiceBus' : '${toLower(prefix)}-servicebus' 30 | 31 | @description('Specifies the name of the user-defined managed identity.') 32 | param managedIdentityName string = letterCaseType == 'UpperCamelCase' ? '${toUpper(first(prefix))}${toLower(substring(prefix, 1, length(prefix) - 1))}JobManagedIdentity' : letterCaseType == 'CamelCase' ? '${toLower(prefix)}JobManagedIdentity' : '${toLower(prefix)}-job-managed-identity' 33 | 34 | @description('Specifies the name of the parameters Azure Service Bus queue.') 35 | param parametersServiceBusQueueName string = 'parameters' 36 | 37 | @description('Specifies the name of the results Azure Service Bus queue.') 38 | param resultsServiceBusQueueName string = 'results' 39 | 40 | @description('Specifies the name of the sender job.') 41 | param senderJobName string = letterCaseType == 'UpperCamelCase' ? '${toUpper(first(prefix))}${toLower(substring(prefix, 1, length(prefix) - 1))}Sender' : letterCaseType == 'CamelCase' ? '${toLower(prefix)}Sender' : '${toLower(prefix)}-sender' 42 | 43 | @description('Specifies the name of the processor job.') 44 | param processorJobName string = letterCaseType == 'UpperCamelCase' ? '${toUpper(first(prefix))}${toLower(substring(prefix, 1, length(prefix) - 1))}Processor' : letterCaseType == 'CamelCase' ? '${toLower(prefix)}Processor' : '${toLower(prefix)}-processor' 45 | 46 | @description('Specifies the name of the receiver job.') 47 | param receiverJobName string = letterCaseType == 'UpperCamelCase' ? '${toUpper(first(prefix))}${toLower(substring(prefix, 1, length(prefix) - 1))}Receiver' : letterCaseType == 'CamelCase' ? '${toLower(prefix)}Receiver' : '${toLower(prefix)}-receiver' 48 | 49 | @description('Specifies the name (e.g., sbsender) of the container image of the sender job.') 50 | param senderImageName string = 'sbsender' 51 | 52 | @description('Specifies the name (e.g., sbprocessor) of the container image of the processor job.') 53 | param processorImageName string = 'sbprocessor' 54 | 55 | @description('Specifies the name (e.g., sbreceiver) of the container image of the receiver job.') 56 | param receiverImageName string = 'sbreceiver' 57 | 58 | @description('Specifies the tag (e.g., v1) of the container image of the sender job.') 59 | param senderImageTag string = 'v1' 60 | 61 | @description('Specifies the tag (e.g., v1) of the container image of the processor job.') 62 | param processorImageTag string = 'v1' 63 | 64 | @description('Specifies the tag (e.g., v1) of the container image of the receiver job.') 65 | param receiverImageTag string = 'v1' 66 | 67 | @description('Maximum number of replicas of the sender job to run per execution.') 68 | param senderParallelism int = 1 69 | 70 | @description('Maximum number of replicas of the sender job to run per execution.') 71 | param processorParallelism int = 5 72 | 73 | @description('Maximum number of replicas of the sender job to run per execution.') 74 | param receiverParallelism int = 5 75 | 76 | @description('Specifies the minimum number of job executions to run per polling interval.') 77 | param minExecutions int = 1 78 | 79 | @description('Specifies the maximum number of job executions to run per polling interval.') 80 | param maxExecutions int = 10 81 | 82 | @description('Specifies the polling interval in seconds.') 83 | param pollingInterval int = 30 84 | 85 | @description('Specifies the maximum number of retries before the replica fails..') 86 | param replicaRetryLimit int = 1 87 | 88 | @description('Specifies the maximum number of seconds a replica can execute.') 89 | param replicaTimeout int = 300 90 | 91 | // Existing Resources 92 | resource environment 'Microsoft.App/managedEnvironments@2023-04-01-preview' existing = { 93 | name: containerAppEnvironmentName 94 | } 95 | 96 | resource acr 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = { 97 | name: acrName 98 | } 99 | 100 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { 101 | name: managedIdentityName 102 | } 103 | 104 | resource namespace 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' existing = { 105 | name: serviceBusNamespace 106 | } 107 | 108 | // Variables 109 | var serviceBusEndpoint = '${namespace.id}/AuthorizationRules/RootManageSharedAccessKey' 110 | var serviceBusConnectionString = listKeys(serviceBusEndpoint, namespace.apiVersion).primaryConnectionString 111 | 112 | // Modules 113 | module senderJob 'container-apps-job.bicep' = { 114 | name: 'senderJob' 115 | params: { 116 | name: toLower(senderJobName) 117 | location: location 118 | managedIdentityId: managedIdentity.id 119 | containerImage: '${acr.properties.loginServer}/${senderImageName}:${senderImageTag}' 120 | triggerType: 'Manual' 121 | parallelism: senderParallelism 122 | replicaCompletionCount: senderParallelism 123 | replicaRetryLimit: replicaRetryLimit 124 | replicaTimeout: replicaTimeout 125 | environmentId: environment.id 126 | tags: tags 127 | registries: [ 128 | { 129 | server: acr.properties.loginServer 130 | identity: managedIdentity.id 131 | } 132 | ] 133 | env: [ 134 | { 135 | name: 'AZURE_CLIENT_ID' 136 | value: managedIdentity.properties.clientId 137 | } 138 | { 139 | name: 'FULLY_QUALIFIED_NAMESPACE' 140 | value: toLower('${serviceBusNamespace}.servicebus.windows.net') 141 | } 142 | { 143 | name: 'INPUT_QUEUE_NAME' 144 | value: parametersServiceBusQueueName 145 | } 146 | { 147 | name: 'MIN_NUMBER' 148 | value: '1' 149 | } 150 | { 151 | name: 'MAX_NUMBER' 152 | value: '10' 153 | } 154 | { 155 | name: 'MESSAGE_COUNT' 156 | value: '100' 157 | } 158 | { 159 | name: 'SEND_TYPE' 160 | value: 'list' 161 | } 162 | ] 163 | } 164 | } 165 | 166 | module processorJob 'container-apps-job.bicep' = { 167 | name: 'processorJob' 168 | params: { 169 | name: processorJobName 170 | location: location 171 | managedIdentityId: managedIdentity.id 172 | containerImage: '${acr.properties.loginServer}/${processorImageName}:${processorImageTag}' 173 | triggerType: 'Schedule' 174 | cronExpression: '*/5 * * * *' 175 | parallelism: processorParallelism 176 | replicaCompletionCount: processorParallelism 177 | replicaRetryLimit: replicaRetryLimit 178 | replicaTimeout: replicaTimeout 179 | environmentId: environment.id 180 | tags: tags 181 | registries: [ 182 | { 183 | server: acr.properties.loginServer 184 | identity: managedIdentity.id 185 | } 186 | ] 187 | env: [ 188 | { 189 | name: 'AZURE_CLIENT_ID' 190 | value: managedIdentity.properties.clientId 191 | } 192 | { 193 | name: 'FULLY_QUALIFIED_NAMESPACE' 194 | value: toLower('${serviceBusNamespace}.servicebus.windows.net') 195 | } 196 | { 197 | name: 'INPUT_QUEUE_NAME' 198 | value: parametersServiceBusQueueName 199 | } 200 | { 201 | name: 'OUTPUT_QUEUE_NAME' 202 | value: resultsServiceBusQueueName 203 | } 204 | { 205 | name: 'MAX_MESSAGE_COUNT' 206 | value: '20' 207 | } 208 | { 209 | name: 'MAX_WAIT_TIME' 210 | value: '5' 211 | } 212 | ] 213 | } 214 | } 215 | 216 | module receiverJob 'container-apps-job.bicep' = { 217 | name: 'receiverJob' 218 | params: { 219 | name: receiverJobName 220 | location: location 221 | managedIdentityId: managedIdentity.id 222 | containerImage: '${acr.properties.loginServer}/${receiverImageName}:${receiverImageTag}' 223 | triggerType: 'Event' 224 | maxExecutions: maxExecutions 225 | minExecutions: minExecutions 226 | pollingInterval: pollingInterval 227 | parallelism: receiverParallelism 228 | replicaCompletionCount: receiverParallelism 229 | replicaRetryLimit: replicaRetryLimit 230 | replicaTimeout: replicaTimeout 231 | environmentId: environment.id 232 | tags: tags 233 | registries: [ 234 | { 235 | server: acr.properties.loginServer 236 | identity: managedIdentity.id 237 | } 238 | ] 239 | env: [ 240 | { 241 | name: 'AZURE_CLIENT_ID' 242 | value: managedIdentity.properties.clientId 243 | } 244 | { 245 | name: 'FULLY_QUALIFIED_NAMESPACE' 246 | value: toLower('${serviceBusNamespace}.servicebus.windows.net') 247 | } 248 | { 249 | name: 'OUTPUT_QUEUE_NAME' 250 | value: resultsServiceBusQueueName 251 | } 252 | { 253 | name: 'MAX_MESSAGE_COUNT' 254 | value: '20' 255 | } 256 | { 257 | name: 'MAX_WAIT_TIME' 258 | value: '5' 259 | } 260 | ] 261 | secrets: [ 262 | { 263 | name: 'service-bus-connection-string' 264 | value: serviceBusConnectionString 265 | } 266 | ] 267 | rules: [ 268 | { 269 | name: 'azure-servicebus-queue-rule' 270 | type: 'azure-servicebus' 271 | metadata: { 272 | messageCount: '5' 273 | namespace: serviceBusNamespace 274 | queueName: resultsServiceBusQueueName 275 | } 276 | auth: [ 277 | { 278 | secretRef: 'service-bus-connection-string' 279 | triggerParameter: 'connection' 280 | } 281 | ] 282 | } 283 | ] 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /bicep/jobs/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | } 6 | } -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-jobs/2d9e50554a1ad635a3eb3ac17136e80bdf6db500/images/architecture.png -------------------------------------------------------------------------------- /images/executionhistory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-jobs/2d9e50554a1ad635a3eb3ac17136e80bdf6db500/images/executionhistory.png -------------------------------------------------------------------------------- /images/executionlogs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-jobs/2d9e50554a1ad635a3eb3ac17136e80bdf6db500/images/executionlogs.png -------------------------------------------------------------------------------- /images/joblogs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-jobs/2d9e50554a1ad635a3eb3ac17136e80bdf6db500/images/joblogs.png -------------------------------------------------------------------------------- /images/messageflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-jobs/2d9e50554a1ad635a3eb3ac17136e80bdf6db500/images/messageflow.png -------------------------------------------------------------------------------- /images/resources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-jobs/2d9e50554a1ad635a3eb3ac17136e80bdf6db500/images/resources.png -------------------------------------------------------------------------------- /scripts/00-variables.sh: -------------------------------------------------------------------------------- 1 | # Variables 2 | prefix="Gundam" 3 | name="${prefix}Receiver" 4 | managedIdentityName="${prefix}JobManagedIdentity" 5 | serviceBusNamespace="${prefix}ServiceBus" 6 | resourceGroupName="${prefix}RG" 7 | environmentName="${prefix}Environment" 8 | imageName="paolosalvatori.azurecr.io/sbsender:v1" 9 | acrName="${prefix}Acr" 10 | replicaTimeout=300 11 | replicaRetryLimit=1 12 | cpu="0.25" 13 | memory="0.5Gi" 14 | 15 | # Queues 16 | inputQueueName="input" 17 | outputQueueName="output" 18 | 19 | # Sender Job 20 | senderJobName="${prefix,,}azcliSender" 21 | senderJobTriggerType="Manual" 22 | senderJobReplicaCompletionCount=1 23 | senderJobParallelism=1 24 | senderJobImageName="sbsender" 25 | senderJobImageTag="v1" 26 | senderJobMinNumber=1 27 | senderJobMaxNumber=10 28 | senderJobMessageCount=100 29 | senderJobSendType="list" 30 | 31 | # Processor Job 32 | processorJobName="${prefix,,}azcliprocessor" 33 | processorJobTriggerType="Schedule" 34 | processorJobReplicaCompletionCount=5 35 | processorJobParallelism=5 36 | processorJobCronExpression="*/5 * * * *" 37 | processorJobImageName="sbprocessor" 38 | processorJobImageTag="v1" 39 | processorMaxMessageCount=20 40 | processorMaxWaitTime=5 41 | 42 | # Receiver Job 43 | receiverJobName="${prefix,,}azclireceiver" 44 | receiverJobTriggerType="Event" 45 | receiverJobReplicaCompletionCount=5 46 | receiverJobParallelism=5 47 | receiverJobImageName="sbreceiver" 48 | receiverJobImageTag="v1" 49 | receiverMinExecutions=1 50 | receiverMaxExecutions=10 51 | receiverMaxMessageCount=20 52 | receiverMaxWaitTime=5 53 | receiverMessageCount=5 54 | 55 | # Bicep generated entities 56 | bicepSenderJobName="${prefix,,}sender" 57 | bicepProcessorJobName="${prefix,,}processor" 58 | bicepReceiverJobName="${prefix,,}receiver" -------------------------------------------------------------------------------- /scripts/01-create-job.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Variables 4 | source ./00-variables.sh 5 | 6 | # Print the menu 7 | echo "=====================================" 8 | echo "Create Azure Container App Job (1-4): " 9 | echo "=====================================" 10 | options=( 11 | "Sender Job" 12 | "Processor Job" 13 | "Receiver Job" 14 | "Quit") 15 | name="" 16 | # Select an option 17 | COLUMNS=0 18 | select option in "${options[@]}"; do 19 | case $option in 20 | "Sender Job") 21 | # Retrieve the resource id of the user-assigned managed identity 22 | echo "Retrieving the resource id for the [$managedIdentityName] managed identity..." 23 | managedIdentityId=$(az identity show \ 24 | --name $managedIdentityName \ 25 | --resource-group $resourceGroupName \ 26 | --query id \ 27 | --output tsv) 28 | 29 | if [[ -n $managedIdentityId ]]; then 30 | echo "[$managedIdentityId] resource id for the [$managedIdentityName] managed identity successfully retrieved" 31 | else 32 | echo "Failed to retrieve the resource id for the [$managedIdentityName] managed identity" 33 | exit 34 | fi 35 | 36 | # Retrieve the client id of the user-assigned managed identity 37 | echo "Retrieving the client id for the [$managedIdentityName] managed identity..." 38 | clientId=$(az identity show \ 39 | --name $managedIdentityName \ 40 | --resource-group $resourceGroupName \ 41 | --query clientId \ 42 | --output tsv) 43 | 44 | if [[ -n $clientId ]]; then 45 | echo "[$clientId] client id for the [$managedIdentityName] managed identity successfully retrieved" 46 | else 47 | echo "Failed to retrieve the client id for the [$managedIdentityName] managed identity" 48 | exit 49 | fi 50 | 51 | # Retrieve the ACR loginServer 52 | echo "Retrieving the loginServer for the [$acrName] container registry..." 53 | loginServer=$(az acr show \ 54 | --name $acrName \ 55 | --resource-group $resourceGroupName \ 56 | --query loginServer \ 57 | --output tsv) 58 | 59 | if [[ -n $loginServer ]]; then 60 | echo "[$loginServer] loginServer for the [$acrName] container registry successfully retrieved" 61 | else 62 | echo "Failed to retrieve the loginServer for the [$acrName] container registry" 63 | exit 64 | fi 65 | 66 | # Create the sender job 67 | echo "Retrieving the [${senderJobName,,}] job..." 68 | az containerapp job show \ 69 | --name ${senderJobName,,} \ 70 | --resource-group $resourceGroupName &>/dev/null 71 | if [[ $? != 0 ]]; then 72 | echo "No [${senderJobName,,}] job actually exists in the [$environmentName] environment" 73 | echo "Creating [${senderJobName,,}] job in the [$environmentName] environment..." 74 | az containerapp job create \ 75 | --name ${senderJobName,,} \ 76 | --resource-group $resourceGroupName \ 77 | --environment $environmentName \ 78 | --trigger-type $senderJobTriggerType \ 79 | --replica-timeout $replicaTimeout \ 80 | --replica-retry-limit $replicaRetryLimit \ 81 | --replica-completion-count $senderJobReplicaCompletionCount \ 82 | --parallelism $senderJobParallelism \ 83 | --image "${loginServer}/${senderJobImageName}:${senderJobImageTag}" \ 84 | --cpu $cpu \ 85 | --memory $memory \ 86 | --registry-identity $managedIdentityId \ 87 | --registry-server $loginServer \ 88 | --env-vars \ 89 | AZURE_CLIENT_ID=$clientId \ 90 | FULLY_QUALIFIED_NAMESPACE="${serviceBusNamespace}.servicebus.windows.net" \ 91 | INPUT_QUEUE_NAME=$inputQueueName \ 92 | MIN_NUMBER="$senderJobMinNumber" \ 93 | MAX_NUMBER="$senderJobMaxNumber" \ 94 | MESSAGE_COUNT="$senderJobMessageCount" \ 95 | SEND_TYPE="$senderJobSendType" 1>/dev/null 96 | if [[ $? == 0 ]]; then 97 | echo "[$environmentName] environment successfully created in the [$subscriptionName] subscription" 98 | else 99 | echo "Failed to create [$environmentName] environment in the [$subscriptionName] subscription" 100 | exit 101 | fi 102 | else 103 | echo "[$environmentName] environment already contains a [${senderJobName,,}] job" 104 | fi 105 | break 106 | ;; 107 | "Processor Job") 108 | # Retrieve the resource id of the user-assigned managed identity 109 | echo "Retrieving the resource id for the [$managedIdentityName] managed identity..." 110 | managedIdentityId=$(az identity show \ 111 | --name $managedIdentityName \ 112 | --resource-group $resourceGroupName \ 113 | --query id \ 114 | --output tsv) 115 | 116 | if [[ -n $managedIdentityId ]]; then 117 | echo "[$managedIdentityId] resource id for the [$managedIdentityName] managed identity successfully retrieved" 118 | else 119 | echo "Failed to retrieve the resource id for the [$managedIdentityName] managed identity" 120 | exit 121 | fi 122 | 123 | # Retrieve the client id of the user-assigned managed identity 124 | echo "Retrieving the client id for the [$managedIdentityName] managed identity..." 125 | clientId=$(az identity show \ 126 | --name $managedIdentityName \ 127 | --resource-group $resourceGroupName \ 128 | --query clientId \ 129 | --output tsv) 130 | 131 | if [[ -n $clientId ]]; then 132 | echo "[$clientId] client id for the [$managedIdentityName] managed identity successfully retrieved" 133 | else 134 | echo "Failed to retrieve the client id for the [$managedIdentityName] managed identity" 135 | exit 136 | fi 137 | 138 | # Retrieve the ACR loginServer 139 | echo "Retrieving the loginServer for the [$acrName] container registry..." 140 | loginServer=$(az acr show \ 141 | --name $acrName \ 142 | --resource-group $resourceGroupName \ 143 | --query loginServer \ 144 | --output tsv) 145 | 146 | if [[ -n $loginServer ]]; then 147 | echo "[$loginServer] loginServer for the [$acrName] container registry successfully retrieved" 148 | else 149 | echo "Failed to retrieve the loginServer for the [$acrName] container registry" 150 | exit 151 | fi 152 | 153 | # Create the processor job 154 | echo "Retrieving the [${processorJobName,,}] job..." 155 | az containerapp job show \ 156 | --name ${processorJobName,,} \ 157 | --resource-group $resourceGroupName &>/dev/null 158 | if [[ $? != 0 ]]; then 159 | echo "No [${processorJobName,,}] job actually exists in the [$environmentName] environment" 160 | echo "Creating [${processorJobName,,}] job in the [$environmentName] environment..." 161 | az containerapp job create \ 162 | --name ${processorJobName,,} \ 163 | --resource-group $resourceGroupName \ 164 | --environment $environmentName \ 165 | --trigger-type $processorJobTriggerType \ 166 | --replica-timeout $replicaTimeout \ 167 | --replica-retry-limit $replicaRetryLimit \ 168 | --replica-completion-count $processorJobReplicaCompletionCount \ 169 | --parallelism $processorJobParallelism \ 170 | --cron-expression "$processorJobCronExpression" \ 171 | --image "${loginServer}/${processorJobImageName}:${processorJobImageTag}" \ 172 | --cpu $cpu \ 173 | --memory $memory \ 174 | --registry-identity $managedIdentityId \ 175 | --registry-server $loginServer \ 176 | --env-vars \ 177 | AZURE_CLIENT_ID=$clientId \ 178 | FULLY_QUALIFIED_NAMESPACE="${serviceBusNamespace}.servicebus.windows.net" \ 179 | INPUT_QUEUE_NAME=$inputQueueName \ 180 | OUTPUT_QUEUE_NAME=$outputQueueName \ 181 | MAX_MESSAGE_COUNT="$processorMaxMessageCount" \ 182 | MAX_WAIT_TIME="$processorMaxWaitTime" 1>/dev/null 183 | if [[ $? == 0 ]]; then 184 | echo "[$environmentName] environment successfully created in the [$subscriptionName] subscription" 185 | else 186 | echo "Failed to create [$environmentName] environment in the [$subscriptionName] subscription" 187 | exit 188 | fi 189 | else 190 | echo "[$environmentName] environment already contains a [${processorJobName,,}] job" 191 | fi 192 | break 193 | ;; 194 | "Receiver Job") 195 | # Retrieve the resource id of the user-assigned managed identity 196 | echo "Retrieving the resource id for the [$managedIdentityName] managed identity..." 197 | managedIdentityId=$(az identity show \ 198 | --name $managedIdentityName \ 199 | --resource-group $resourceGroupName \ 200 | --query id \ 201 | --output tsv) 202 | 203 | if [[ -n $managedIdentityId ]]; then 204 | echo "[$managedIdentityId] resource id for the [$managedIdentityName] managed identity successfully retrieved" 205 | else 206 | echo "Failed to retrieve the resource id for the [$managedIdentityName] managed identity" 207 | exit 208 | fi 209 | 210 | # Retrieve the client id of the user-assigned managed identity 211 | echo "Retrieving the client id for the [$managedIdentityName] managed identity..." 212 | clientId=$(az identity show \ 213 | --name $managedIdentityName \ 214 | --resource-group $resourceGroupName \ 215 | --query clientId \ 216 | --output tsv) 217 | 218 | if [[ -n $clientId ]]; then 219 | echo "[$clientId] client id for the [$managedIdentityName] managed identity successfully retrieved" 220 | else 221 | echo "Failed to retrieve the client id for the [$managedIdentityName] managed identity" 222 | exit 223 | fi 224 | 225 | # Retrieve the ACR loginServer 226 | echo "Retrieving the loginServer for the [$acrName] container registry..." 227 | loginServer=$(az acr show \ 228 | --name $acrName \ 229 | --resource-group $resourceGroupName \ 230 | --query loginServer \ 231 | --output tsv) 232 | 233 | if [[ -n $loginServer ]]; then 234 | echo "[$loginServer] loginServer for the [$acrName] container registry successfully retrieved" 235 | else 236 | echo "Failed to retrieve the loginServer for the [$acrName] container registry" 237 | exit 238 | fi 239 | 240 | # Retrieve the Service Bus namespace connection string 241 | echo "Retrieving the connection string for the [$serviceBusNamespace] Service Bus namespace..." 242 | serviceBusConnectionString=$(az servicebus namespace authorization-rule keys list \ 243 | --name RootManageSharedAccessKey \ 244 | --namespace-name $serviceBusNamespace \ 245 | --resource-group $resourceGroupName \ 246 | --query primaryConnectionString \ 247 | --output tsv) 248 | if [[ -n $serviceBusConnectionString ]]; then 249 | echo "[$serviceBusConnectionString] connection string for the [$serviceBusNamespace] Service Bus namespace successfully retrieved" 250 | else 251 | echo "Failed to retrieve the connection string for the [$serviceBusNamespace] Service Bus namespace" 252 | exit 253 | fi 254 | 255 | # Create the receiver job 256 | echo "Retrieving the [${receiverJobName,,}] job..." 257 | az containerapp job show \ 258 | --name ${receiverJobName,,} \ 259 | --resource-group $resourceGroupName &>/dev/null 260 | if [[ $? != 0 ]]; then 261 | echo "No [${receiverJobName,,}] job actually exists in the [$environmentName] environment" 262 | echo "Creating [${receiverJobName,,}] job in the [$environmentName] environment..." 263 | az containerapp job create \ 264 | --name ${receiverJobName,,} \ 265 | --resource-group $resourceGroupName \ 266 | --environment $environmentName \ 267 | --trigger-type $receiverJobTriggerType \ 268 | --replica-timeout $replicaTimeout \ 269 | --replica-retry-limit $replicaRetryLimit \ 270 | --replica-completion-count $receiverJobReplicaCompletionCount \ 271 | --parallelism $receiverJobParallelism \ 272 | --image "${loginServer}/${receiverJobImageName}:${receiverJobImageTag}" \ 273 | --cpu $cpu \ 274 | --memory $memory \ 275 | --min-executions $receiverMinExecutions \ 276 | --max-executions $receiverMaxExecutions \ 277 | --registry-identity $managedIdentityId \ 278 | --registry-server $loginServer \ 279 | --secrets service-bus-connection-string="$serviceBusConnectionString" \ 280 | --env-vars \ 281 | AZURE_CLIENT_ID=$clientId \ 282 | FULLY_QUALIFIED_NAMESPACE="${serviceBusNamespace}.servicebus.windows.net" \ 283 | OUTPUT_QUEUE_NAME=$outputQueueName \ 284 | MAX_MESSAGE_COUNT="$receiverMaxMessageCount" \ 285 | MAX_WAIT_TIME="$receiverMaxWaitTime" \ 286 | --scale-rule-name azure-servicebus-queue-rule \ 287 | --scale-rule-type azure-servicebus \ 288 | --scale-rule-metadata "queueName=$outputQueueName" \ 289 | "namespace=$serviceBusNamespace" \ 290 | "messageCount=$receiverMessageCount" \ 291 | --scale-rule-auth "connection=service-bus-connection-string" 1>/dev/null 292 | if [[ $? == 0 ]]; then 293 | echo "[$environmentName] environment successfully created in the [$subscriptionName] subscription" 294 | else 295 | echo "Failed to create [$environmentName] environment in the [$subscriptionName] subscription" 296 | exit 297 | fi 298 | else 299 | echo "[$environmentName] environment already contains a [${receiverJobName,,}] job" 300 | fi 301 | break 302 | ;; 303 | "Quit") 304 | exit 305 | ;; 306 | *) echo "invalid option $REPLY" ;; 307 | esac 308 | done 309 | -------------------------------------------------------------------------------- /scripts/02-start-job.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Variables 4 | source ./00-variables.sh 5 | 6 | # Print the menu 7 | echo "====================================" 8 | echo "Start Azure Container App Job (1-7): " 9 | echo "====================================" 10 | options=( 11 | "Bicep Sender Job" 12 | "Bicep Processor Job" 13 | "Bicep Receiver Job" 14 | "Azure CLI Sender Job" 15 | "Azure CLI Processor Job" 16 | "Azure CLI Receiver Job" 17 | "Quit" 18 | ) 19 | name="" 20 | # Select an option 21 | COLUMNS=0 22 | select option in "${options[@]}"; do 23 | case $option in 24 | "Bicep Sender Job") 25 | name=$bicepSenderJobName 26 | break 27 | ;; 28 | "Bicep Processor Job") 29 | name=$bicepProcessorJobName 30 | break 31 | ;; 32 | "Bicep Receiver Job") 33 | name=$bicepReceiverJobName 34 | break 35 | ;; 36 | "Azure CLI Sender Job") 37 | name=$senderJobName 38 | break 39 | ;; 40 | "Azure CLI Processor Job") 41 | name=$processorJobName 42 | break 43 | ;; 44 | "Azure CLI Receiver Job") 45 | name=$receiverJobName 46 | break 47 | ;; 48 | "Quit") 49 | exit 50 | ;; 51 | *) echo "invalid option $REPLY" ;; 52 | esac 53 | done 54 | 55 | # Start job 56 | echo "Starting the [$name] job..." 57 | az containerapp job start --name $name --resource-group $resourceGroupName 58 | if [[ $? == 0 ]]; then 59 | echo "[$name] job successfully started" 60 | else 61 | echo "Failed to start the [$name] job" 62 | fi -------------------------------------------------------------------------------- /scripts/03-view-managed-identity.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # For more information on Azure Container App Managed Identity, see: 4 | # - https://learn.microsoft.com/en-us/azure/container-apps/managed-identity?tabs=portal%2Cpython 5 | # - https://learn.microsoft.com/en-us/azure/container-apps/managed-identity-image-pull?tabs=azure-cli&pivots=azure-portal 6 | # - https://azuresdkdocs.blob.core.windows.net/$web/dotnet/Azure.Identity/1.0.0/api/index.html 7 | # - https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/managed-identities-status 8 | # - https://pypi.org/project/azure-identity/ 9 | 10 | # Variables 11 | source ./00-variables.sh 12 | 13 | # Print the menu 14 | echo "========================================================" 15 | echo "View Managed Identity for Azure Container App Job (1-7): " 16 | echo "========================================================" 17 | options=("Bicep Sender Job" 18 | "Bicep Processor Job" 19 | "Bicep Receiver Job" 20 | "Azure CLI Sender Job" 21 | "Azure CLI Processor Job" 22 | "Azure CLI Receiver Job" 23 | "Quit") 24 | name="" 25 | # Select an option 26 | COLUMNS=0 27 | select option in "${options[@]}"; do 28 | case $option in 29 | "Bicep Sender Job") 30 | name=$bicepSenderJobName 31 | break 32 | ;; 33 | "Bicep Processor Job") 34 | name=$bicepProcessorJobName 35 | break 36 | ;; 37 | "Bicep Receiver Job") 38 | name=$bicepReceiverJobName 39 | break 40 | ;; 41 | "Azure CLI Sender Job") 42 | name=$senderJobName 43 | break 44 | ;; 45 | "Azure CLI Processor Job") 46 | name=$processorJobName 47 | break 48 | ;; 49 | "Azure CLI Receiver Job") 50 | name=$receiverJobName 51 | break 52 | ;; 53 | "Quit") 54 | exit 55 | ;; 56 | *) echo "invalid option $REPLY" ;; 57 | esac 58 | done 59 | 60 | # View Azure Containers App Job Managed Identity 61 | echo "Viewing the [$name] job managed identity..." 62 | az containerapp job identity show --name $name -g $resourceGroupName -------------------------------------------------------------------------------- /src/.env: -------------------------------------------------------------------------------- 1 | FULLY_QUALIFIED_NAMESPACE=paolosalvatori.servicebus.windows.net 2 | INPUT_QUEUE_NAME=inputqueue 3 | OUTPUT_QUEUE_NAME=outputqueue 4 | MAX_WAIT_TIME=5 5 | MAX_MESSAGE_COUNT=20 6 | MIN_NUMBER=1 7 | MAX_NUMBER=20 8 | MESSAGE_COUNT=100 9 | SEND_TYPE=list -------------------------------------------------------------------------------- /src/.local: -------------------------------------------------------------------------------- 1 | AZURE_CLIENT_ID= 2 | AZURE_CLIENT_SECRET= 3 | AZURE_TENANT_ID= -------------------------------------------------------------------------------- /src/00-variables.sh: -------------------------------------------------------------------------------- 1 | # Variables 2 | prefix="Gundam" 3 | acrName="${prefix}Acr" 4 | acrResourceGrougName="${prefix}RG" 5 | location="northeurope" 6 | attachAcr=false 7 | senderImageName="sbsender" 8 | processorImageName="sbprocessor" 9 | receiverImageName="sbreceiver" 10 | images=($senderImageName $processorImageName $receiverImageName) 11 | filenames=(sbsender.py sbprocessor.py sbreceiver.py) 12 | tag="v1" 13 | 14 | # Azure Subscription and Tenant 15 | subscriptionId=$(az account show --query id --output tsv) 16 | subscriptionName=$(az account show --query name --output tsv) 17 | tenantId=$(az account show --query tenantId --output tsv) 18 | -------------------------------------------------------------------------------- /src/01-build-docker-images.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Variables 4 | source ./00-variables.sh 5 | 6 | # Use a for loop to build the docker images using the array index 7 | for index in ${!images[@]}; do 8 | # Build the docker image 9 | docker build -t ${images[$index]}:$tag -f Dockerfile --build-arg FILENAME=${filenames[$index]} . 10 | done -------------------------------------------------------------------------------- /src/02-run-docker-container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Variables 4 | source ./00-variables.sh 5 | 6 | # Print the menu 7 | echo "====================================" 8 | echo "Run Docker Container (1-4): " 9 | echo "====================================" 10 | options=( 11 | "Sender" 12 | "Processor" 13 | "Receiver" 14 | "Quit" 15 | ) 16 | name="" 17 | # Select an option 18 | COLUMNS=0 19 | select option in "${options[@]}"; do 20 | case $option in 21 | "Sender") 22 | docker run -it \ 23 | --rm \ 24 | --env-file .env \ 25 | --env-file .local \ 26 | --name $senderImageName \ 27 | $senderImageName:$tag 28 | break 29 | ;; 30 | "Processor") 31 | docker run -it \ 32 | --rm \ 33 | --env-file .env \ 34 | --env-file .local \ 35 | --name $processorImageName \ 36 | $processorImageName:$tag 37 | break 38 | ;; 39 | "Receiver") 40 | docker run -it \ 41 | --rm \ 42 | --env-file .env \ 43 | --env-file .local \ 44 | --name $receiverImageName \ 45 | $receiverImageName:$tag 46 | break 47 | ;; 48 | "Quit") 49 | exit 50 | ;; 51 | *) echo "invalid option $REPLY" ;; 52 | esac 53 | done -------------------------------------------------------------------------------- /src/03-push-docker-image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Variables 4 | source ./00-variables.sh 5 | 6 | # Login to ACR 7 | az acr login --name $acrName 8 | 9 | # Retrieve ACR login server. Each container image needs to be tagged with the loginServer name of the registry. 10 | loginServer=$(az acr show --name $acrName --query loginServer --output tsv) 11 | 12 | # Use a for loop to tag and push the local docker images to the Azure Container Registry 13 | for index in ${!images[@]}; do 14 | # Tag the local sender image with the loginServer of ACR 15 | docker tag ${images[$index],,}:$tag $loginServer/${images[$index],,}:$tag 16 | 17 | # Push the container image to ACR 18 | docker push $loginServer/${images[$index],,}:$tag 19 | done -------------------------------------------------------------------------------- /src/Dockerfile: -------------------------------------------------------------------------------- 1 | # app/Dockerfile 2 | 3 | # # Stage 1 - Install build dependencies 4 | 5 | # A Dockerfile must start with a FROM instruction which sets the base image for the container. 6 | # The Python images come in many flavors, each designed for a specific use case. 7 | # The python:3.11-slim image is a good base image for most applications. 8 | # It is a minimal image built on top of Debian Linux and includes only the necessary packages to run Python. 9 | # The slim image is a good choice because it is small and contains only the packages needed to run Python. 10 | # For more information, see: 11 | # * https://hub.docker.com/_/python 12 | # * https://docs.streamlit.io/knowledge-base/tutorials/deploy/docker 13 | FROM python:3.11-slim AS builder 14 | 15 | # The WORKDIR instruction sets the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions that follow it in the Dockerfile. 16 | # If the WORKDIR doesn’t exist, it will be created even if it’s not used in any subsequent Dockerfile instruction. 17 | # For more information, see: https://docs.docker.com/engine/reference/builder/#workdir 18 | WORKDIR /app 19 | 20 | # Set environment variables. 21 | # The ENV instruction sets the environment variable to the value . 22 | # This value will be in the environment of all “descendant” Dockerfile commands and can be replaced inline in many as well. 23 | # For more information, see: https://docs.docker.com/engine/reference/builder/#env 24 | ENV PYTHONDONTWRITEBYTECODE 1 25 | ENV PYTHONUNBUFFERED 1 26 | 27 | # Install git so that we can clone the app code from a remote repo using the RUN instruction. 28 | # The RUN comand has 2 forms: 29 | # * RUN (shell form, the command is run in a shell, which by default is /bin/sh -c on Linux or cmd /S /C on Windows) 30 | # * RUN ["executable", "param1", "param2"] (exec form) 31 | # The RUN instruction will execute any commands in a new layer on top of the current image and commit the results. 32 | # The resulting committed image will be used for the next step in the Dockerfile. 33 | # For more information, see: https://docs.docker.com/engine/reference/builder/#run 34 | RUN apt-get update && apt-get install -y \ 35 | build-essential \ 36 | curl \ 37 | software-properties-common \ 38 | git \ 39 | && rm -rf /var/lib/apt/lists/* 40 | 41 | # Create a virtualenv to keep dependencies together 42 | RUN python -m venv /opt/venv 43 | ENV PATH="/opt/venv/bin:$PATH" 44 | 45 | # Clone the requirements.txt which contains dependencies to WORKDIR 46 | # COPY has two forms: 47 | # * COPY (this copies the files from the local machine to the container's own filesystem) 48 | # * COPY ["",... ""] (this form is required for paths containing whitespace) 49 | # For more information, see: https://docs.docker.com/engine/reference/builder/#copy 50 | COPY requirements.txt . 51 | 52 | # Install the Python dependencies 53 | RUN pip install --no-cache-dir --no-deps -r requirements.txt 54 | 55 | # Stage 2 - Copy only necessary files to the runner stage 56 | 57 | # The FROM instruction initializes a new build stage for the application 58 | FROM python:3.11-slim 59 | 60 | # Define the filename to copy as an argument 61 | ARG FILENAME 62 | 63 | # Set an environment variable 64 | ENV FILENAME=${FILENAME} 65 | 66 | # Sets the working directory to /app 67 | WORKDIR /app 68 | 69 | # Copy the virtual environment from the builder stage 70 | COPY --from=builder /opt/venv /opt/venv 71 | 72 | # Set environment variables 73 | ENV PATH="/opt/venv/bin:$PATH" 74 | 75 | # Clone the ${FILENAME} containing the application code 76 | COPY $FILENAME . 77 | 78 | # The ENTRYPOINT instruction has two forms: 79 | # * ENTRYPOINT ["executable", "param1", "param2"] (exec form, preferred) 80 | # * ENTRYPOINT command param1 param2 (shell form) 81 | # The ENTRYPOINT instruction allows you to configure a container that will run as an executable. 82 | # For more information, see: https://docs.docker.com/engine/reference/builder/#entrypoint 83 | ENTRYPOINT python $FILENAME -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.4 2 | aiosignal==1.3.1 3 | async-timeout==4.0.2 4 | attrs==23.1.0 5 | azure-core==1.26.4 6 | azure-identity==1.13.0 7 | azure-servicebus==7.10.0 8 | certifi==2023.5.7 9 | cffi==1.15.1 10 | charset-normalizer==3.1.0 11 | cryptography==41.0.0 12 | frozenlist==1.3.3 13 | idna==3.4 14 | isodate==0.6.1 15 | msal==1.22.0 16 | msal-extensions==1.0.0 17 | multidict==6.0.4 18 | portalocker==2.7.0 19 | pycparser==2.21 20 | PyJWT==2.7.0 21 | python-dotenv==1.0.0 22 | requests==2.31.0 23 | six==1.16.0 24 | typing_extensions==4.6.2 25 | urllib3==2.0.2 26 | yarl==1.9.2 27 | -------------------------------------------------------------------------------- /src/sbprocessor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # -------------------------------------------------------------------------------------------- 4 | # Copyright (c) 2023 Paolo Salvatori 5 | # Microsoft Corporation. All rights reserved. 6 | # Licensed under the MIT License. See License.txt in the project root for license information. 7 | # -------------------------------------------------------------------------------------------- 8 | 9 | import os 10 | import asyncio 11 | from azure.servicebus.aio import ServiceBusClient 12 | from azure.servicebus import ServiceBusMessage 13 | from azure.identity.aio import DefaultAzureCredential 14 | from dotenv import load_dotenv 15 | from dotenv import dotenv_values 16 | 17 | # Load environment variables from .env file 18 | if os.path.exists(".env"): 19 | load_dotenv(override=True) 20 | config = dotenv_values(".env") 21 | 22 | # Read environment variables 23 | fully_qualified_namespace = os.getenv("FULLY_QUALIFIED_NAMESPACE") 24 | input_queue_name = os.getenv("INPUT_QUEUE_NAME") 25 | output_queue_name = os.getenv("OUTPUT_QUEUE_NAME") 26 | max_message_count = int(os.getenv("MAX_MESSAGE_COUNT", 20)) 27 | max_wait_time = int(os.getenv("MAX_WAIT_TIME", 5)) 28 | 29 | # Print environment variables 30 | print(f"FULLY_QUALIFIED_NAMESPACE: {fully_qualified_namespace}") 31 | print(f"INPUT_QUEUE_NAME: {input_queue_name}") 32 | print(f"OUTPUT_QUEUE_NAME: {output_queue_name}") 33 | print(f"MAX_MESSAGE_COUNT: {max_message_count}") 34 | print(f"MAX_WAIT_TIME: {max_wait_time}") 35 | 36 | # Get credential object 37 | credential = DefaultAzureCredential() 38 | 39 | async def fibonacci(n): 40 | if n <= 0: 41 | raise ValueError("n must be a positive integer") 42 | elif n == 1: 43 | return 0 44 | elif n == 2: 45 | return 1 46 | else: 47 | fib1 = await fibonacci(n - 1) 48 | fib2 = await fibonacci(n - 2) 49 | return fib1 + fib2 50 | 51 | async def send_message(message_text: str, i: int): 52 | # Check that the message is not empty 53 | if message_text: 54 | try: 55 | # Create a Service Bus client using the credential 56 | async with ServiceBusClient( 57 | fully_qualified_namespace = fully_qualified_namespace, 58 | credential = credential, 59 | logging_enable = True) as servicebus_client: 60 | # Get a Queue Sender object to send messages to the output queue 61 | sender = servicebus_client.get_queue_sender(queue_name = output_queue_name) 62 | async with sender: 63 | # Create a Service Bus message and send it to the queue 64 | message = ServiceBusMessage(message_text) 65 | # Send a message to the output queue 66 | await sender.send_messages(message) 67 | print(f"[{i}] Sent result message: {message_text}") 68 | except Exception as e: 69 | print(f"An error occurred while sending [{i}] message to the {output_queue_name} queue: {e}") 70 | else: 71 | print(f"The [{i}] message is empty. Please, enter a valid message.") 72 | 73 | async def receive_messages(): 74 | # create a Service Bus client using the connection string 75 | async with ServiceBusClient( 76 | fully_qualified_namespace = fully_qualified_namespace, 77 | credential = credential, 78 | logging_enable = False) as servicebus_client: 79 | 80 | async with servicebus_client: 81 | # Get the Queue Receiver object for the input queue 82 | receiver = servicebus_client.get_queue_receiver(queue_name = input_queue_name) 83 | async with receiver: 84 | try: 85 | received_msgs = await receiver.receive_messages(max_wait_time = max_wait_time, max_message_count = max_message_count) 86 | i = 0 87 | for msg in received_msgs: 88 | # Check if message contains an integer value 89 | try: 90 | n = int(str(msg)) 91 | i += 1 92 | print(f"[{i}] Received Message: {n}") 93 | # Calculate Fibonacci number 94 | result = await fibonacci(n) 95 | print(f"[{i}] The Fibonacci number for {n} is {result}") 96 | # Send result to the output queue 97 | await send_message(str(result), i) 98 | except ValueError: 99 | print(f"[{i}] Received message {str(msg)} is not an integer number") 100 | continue 101 | finally: 102 | # Complete the message so that the message is removed from the queue 103 | await receiver.complete_message(msg) 104 | print(f"[{i}] Completed message: {str(msg)}") 105 | except Exception as e: 106 | print(f"An error occurred while receiving messages from the {input_queue_name} queue: {e}") 107 | 108 | # Receive messages from the input queue 109 | asyncio.run(receive_messages()) 110 | 111 | # Close credential object when it's no longer needed 112 | asyncio.run(credential.close()) -------------------------------------------------------------------------------- /src/sbreceiver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # -------------------------------------------------------------------------------------------- 4 | # Copyright (c) 2023 Paolo Salvatori 5 | # Microsoft Corporation. All rights reserved. 6 | # Licensed under the MIT License. See License.txt in the project root for license information. 7 | # -------------------------------------------------------------------------------------------- 8 | 9 | import os 10 | import asyncio 11 | from azure.servicebus.aio import ServiceBusClient 12 | from azure.identity.aio import DefaultAzureCredential 13 | from dotenv import load_dotenv 14 | from dotenv import dotenv_values 15 | 16 | # Load environment variables from .env file 17 | if os.path.exists(".env"): 18 | load_dotenv(override=True) 19 | config = dotenv_values(".env") 20 | 21 | # Read environment variables 22 | fully_qualified_namespace = os.getenv("FULLY_QUALIFIED_NAMESPACE") 23 | queue_name = os.getenv("OUTPUT_QUEUE_NAME") 24 | max_message_count = int(os.getenv("MAX_MESSAGE_COUNT", 20)) 25 | max_wait_time = int(os.getenv("MAX_WAIT_TIME", 5)) 26 | 27 | # Print environment variables 28 | print(f"FULLY_QUALIFIED_NAMESPACE: {fully_qualified_namespace}") 29 | print(f"OUTPUT_QUEUE_NAME: {queue_name}") 30 | print(f"MAX_MESSAGE_COUNT: {max_message_count}") 31 | print(f"MAX_WAIT_TIME: {max_wait_time}") 32 | 33 | # Get credential object 34 | credential = DefaultAzureCredential() 35 | 36 | async def receive_messages(): 37 | # create a Service Bus client using the connection string 38 | async with ServiceBusClient( 39 | fully_qualified_namespace = fully_qualified_namespace, 40 | credential = credential, 41 | logging_enable = False) as servicebus_client: 42 | 43 | async with servicebus_client: 44 | # Get the Queue Receiver object for the input queue 45 | receiver = servicebus_client.get_queue_receiver(queue_name = queue_name) 46 | async with receiver: 47 | i = 0 48 | # Receive a batch of messages until the queue is empty 49 | while True: 50 | try: 51 | received_msgs = await receiver.receive_messages(max_wait_time = max_wait_time, max_message_count = max_message_count) 52 | if len(received_msgs) == 0: 53 | break 54 | for msg in received_msgs: 55 | # Check if message contains an integer value 56 | try: 57 | n = int(str(msg)) 58 | i += 1 59 | print(f"[{i}] Received Message: {n}") 60 | except ValueError: 61 | print(f"[{i}] Received message {str(msg)} is not an integer number") 62 | continue 63 | finally: 64 | # Complete the message so that the message is removed from the queue 65 | await receiver.complete_message(msg) 66 | print(f"[{i}] Completed message: {str(msg)}") 67 | except Exception as e: 68 | print(f"An error occurred while receiving messages from the {queue_name} queue: {e}") 69 | break 70 | 71 | # Receive messages from the input queue 72 | asyncio.run(receive_messages()) 73 | 74 | # Close credential object when it's no longer needed 75 | asyncio.run(credential.close()) -------------------------------------------------------------------------------- /src/sbsender.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # -------------------------------------------------------------------------------------------- 4 | # Copyright (c) 2023 Paolo Salvatori 5 | # Microsoft Corporation. All rights reserved. 6 | # Licensed under the MIT License. See License.txt in the project root for license information. 7 | # -------------------------------------------------------------------------------------------- 8 | 9 | import os 10 | import asyncio 11 | import random 12 | from azure.servicebus.aio import ServiceBusClient 13 | from azure.servicebus import ServiceBusMessage 14 | from azure.identity.aio import DefaultAzureCredential 15 | from dotenv import load_dotenv 16 | from dotenv import dotenv_values 17 | 18 | # Load environment variables from .env file 19 | if os.path.exists(".env"): 20 | load_dotenv(override = True) 21 | config = dotenv_values(".env") 22 | 23 | # Read environment variables 24 | fully_qualified_namespace = os.getenv("FULLY_QUALIFIED_NAMESPACE") 25 | queue_name = os.getenv("INPUT_QUEUE_NAME") 26 | min_number = int(os.getenv("MIN_NUMBER", 1)) 27 | max_number = int(os.getenv("MAX_NUMBER", 10)) 28 | message_count = int(os.getenv("MESSAGE_COUNT", 100)) 29 | send_type = os.getenv("SEND_TYPE", "list") 30 | 31 | # Print environment variables 32 | print(f"FULLY_QUALIFIED_NAMESPACE: {fully_qualified_namespace}") 33 | print(f"INPUT_QUEUE_NAME: {queue_name}") 34 | print(f"MIN_NUMBER: {min_number}") 35 | print(f"MAX_NUMBER: {max_number}") 36 | print(f"MESSAGE_COUNT: {message_count}") 37 | print(f"SEND_TYPE: {send_type}") 38 | 39 | # Get credential object 40 | credential = DefaultAzureCredential() 41 | 42 | async def send_a_list_of_messages(sender): 43 | try: 44 | # Create a list of messages and send it to the queue 45 | messages = [ServiceBusMessage(f"{random.randint(min_number, max_number)}") for _ in range(message_count)] 46 | await sender.send_messages(messages) 47 | print(f"Sent a list of {message_count} messages to the {queue_name} queue") 48 | except Exception as e: 49 | print(f"An error occurred while sending a list of message to the {queue_name} queue: {e}") 50 | 51 | async def send_batch_message(sender): 52 | # Create a batch of messages 53 | async with sender: 54 | batch_message = await sender.create_message_batch() 55 | for _ in range(message_count): 56 | try: 57 | # Add a message to the batch 58 | batch_message.add_message(ServiceBusMessage(f"{random.randint(min_number, max_number)}")) 59 | except Exception as e: 60 | print(f"An error occurred while creating a batch of messages: {e}") 61 | break 62 | # Send the batch of messages to the queue 63 | try: 64 | await sender.send_messages(batch_message) 65 | print(f"Sent a batch of {message_count} messages to the {queue_name} queue") 66 | except Exception as e: 67 | print(f"An error occurred while sending a batch of message to the {queue_name} queue: {e}") 68 | 69 | async def run(): 70 | # create a Service Bus client using the credential 71 | async with ServiceBusClient( 72 | fully_qualified_namespace = fully_qualified_namespace, 73 | credential = credential, 74 | logging_enable = False) as servicebus_client: 75 | # get a Queue Sender object to send messages to the queue 76 | sender = servicebus_client.get_queue_sender(queue_name = queue_name) 77 | async with sender: 78 | if send_type == "list": 79 | await send_a_list_of_messages(sender) 80 | elif send_type == "batch": 81 | await send_batch_message(sender) 82 | else: 83 | print(f"Invalid send type: {send_type}") 84 | 85 | # Send messages to the input queue 86 | asyncio.run(run()) 87 | 88 | # Close credential object when it's no longer needed 89 | asyncio.run(credential.close()) -------------------------------------------------------------------------------- /visio/aca-jobs.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-jobs/2d9e50554a1ad635a3eb3ac17136e80bdf6db500/visio/aca-jobs.vsdx --------------------------------------------------------------------------------