Repository: Azure/apim-landing-zone-accelerator Branch: main Commit: 0180a563a013 Files: 157 Total size: 577.2 KB Directory structure: gitextract_6xk9lkjw/ ├── .devcontainer/ │ └── devcontainer.json ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── lza-validate-multiregion-apim.yml │ └── lza-validate-singleregion-apim.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── docs/ │ ├── assets/ │ │ └── genai-capabilities.pptx │ └── images/ │ └── APIM.vsdx └── scenarios/ ├── apim-baseline/ │ ├── README.md │ ├── bicep/ │ │ ├── README.md │ │ ├── apim/ │ │ │ ├── apim.bicep │ │ │ └── modules/ │ │ │ ├── dnsrecords.bicep │ │ │ └── kvaccess.bicep │ │ ├── gateway/ │ │ │ ├── appgw.bicep │ │ │ └── modules/ │ │ │ ├── certificate.bicep │ │ │ └── certificateSecret.bicep │ │ ├── main.bicep │ │ ├── networking/ │ │ │ └── networking.bicep │ │ └── shared/ │ │ ├── modules/ │ │ │ ├── azmon.bicep │ │ │ ├── dnszone.bicep │ │ │ ├── privatedeploy.bicep │ │ │ └── privateendpoint.bicep │ │ └── shared.bicep │ └── terraform/ │ ├── README.md │ ├── backend.tf.sample │ ├── main.tf │ ├── modules/ │ │ ├── apim/ │ │ │ ├── apim.tf │ │ │ ├── outputs.tf │ │ │ └── variables.tf │ │ ├── dns/ │ │ │ ├── dnszone.tf │ │ │ └── variables.tf │ │ ├── gateway/ │ │ │ ├── certificate/ │ │ │ │ ├── certificate.tf │ │ │ │ ├── providers.tf │ │ │ │ └── variables.tf │ │ │ ├── gateway.tf │ │ │ ├── outputs.tf │ │ │ └── variables.tf │ │ ├── multi_apim/ │ │ │ ├── apim.tf │ │ │ ├── outputs.tf │ │ │ └── variables.tf │ │ ├── multi_apim-dns-regional/ │ │ │ ├── dnszone.tf │ │ │ └── variables.tf │ │ ├── multi_gateway/ │ │ │ ├── certificate/ │ │ │ │ ├── certificate.tf │ │ │ │ ├── providers.tf │ │ │ │ └── variables.tf │ │ │ ├── gateway.tf │ │ │ ├── outputs.tf │ │ │ └── variables.tf │ │ ├── multi_private_dns_zone/ │ │ │ ├── outputs.tf │ │ │ ├── privatednszone.tf │ │ │ └── variables.tf │ │ ├── multi_shared/ │ │ │ ├── azmon.tf │ │ │ ├── outputs.tf │ │ │ ├── private_endpoint/ │ │ │ │ ├── outputs.tf │ │ │ │ ├── privateendpoint.tf │ │ │ │ └── variables.tf │ │ │ ├── privatedeploy.tf │ │ │ ├── shared.tf │ │ │ └── variables.tf │ │ ├── multi_traffic_manager/ │ │ │ ├── outputs.tf │ │ │ ├── trafficmanager.tf │ │ │ └── variables.tf │ │ ├── networking/ │ │ │ ├── networking.tf │ │ │ ├── outputs.tf │ │ │ └── variables.tf │ │ └── shared/ │ │ ├── azmon.tf │ │ ├── outputs.tf │ │ ├── private_dns_zone/ │ │ │ ├── outputs.tf │ │ │ ├── privatednszone.tf │ │ │ └── variables.tf │ │ ├── private_endpoint/ │ │ │ ├── outputs.tf │ │ │ ├── privateendpoint.tf │ │ │ └── variables.tf │ │ ├── privatedeploy.tf │ │ ├── shared.tf │ │ └── variables.tf │ ├── multi-region/ │ │ ├── multi-region-main.tf │ │ └── variables.tf │ ├── provider.tf │ ├── single-region/ │ │ ├── single-region-main.tf │ │ └── variables.tf │ └── variables.tf ├── certs/ │ └── place-custom-cert-here ├── scripts/ │ ├── bicep/ │ │ ├── deploy-apim-baseline.sh │ │ ├── deploy-workload-function.sh │ │ └── deploy-workload-genai.sh │ └── terraform/ │ ├── __destroy-apim-baseline.sh │ ├── azure-backend-sample.sh │ ├── deploy-apim-baseline.sh │ ├── deploy-workload-genai-new.sh │ ├── deploy-workload-genai.sh │ ├── sample-multi-region.env │ ├── sample.backend.hcl │ ├── sample.env │ └── test-apim-baseline-deployment.sh ├── workload-functions/ │ ├── README.md │ └── bicep/ │ ├── README.md │ ├── apim/ │ │ └── config.bicep │ ├── backend/ │ │ ├── backend.bicep │ │ └── modules/ │ │ └── networking.bicep │ ├── deploy/ │ │ ├── deploy.bicep │ │ └── modules/ │ │ └── networking.bicep │ └── main.bicep └── workload-genai/ ├── README.md ├── bicep/ │ ├── README.md │ ├── apim-policies/ │ │ ├── api-specs/ │ │ │ └── openapi-spec.json │ │ ├── apiManagement.bicep │ │ └── load-balancing/ │ │ ├── backends.bicep │ │ └── lb-pool.bicep │ ├── eventhub/ │ │ └── eventHub.bicep │ ├── main.bicep │ └── openai/ │ └── openai.bicep ├── policies/ │ ├── fragments/ │ │ ├── load-balancing/ │ │ │ ├── README.md │ │ │ └── simple-priority-weighted.xml │ │ ├── manage-spikes-with-payg/ │ │ │ ├── README.md │ │ │ └── retry-with-payg.xml │ │ ├── rate-limiting/ │ │ │ ├── README.md │ │ │ ├── adaptive-rate-limiting.xml │ │ │ ├── rate-limiting-by-tokens.xml │ │ │ └── rate-limiting-workaround.xml │ │ └── usage-tracking/ │ │ ├── README.md │ │ ├── usage-tracking-with-appinsights.xml │ │ └── usage-tracking-with-eventhub.xml │ ├── genai-policy.xml │ └── multi-tenancy/ │ ├── README.md │ ├── multi-tenant-product1-policy.xml │ └── multi-tenant-product2-policy.xml └── terraform/ ├── README.md ├── backend.tf.sample ├── main.tf ├── modules/ │ ├── apim_policies/ │ │ ├── api-specs/ │ │ │ └── openapi-spec.json │ │ ├── apimanagement.tf │ │ ├── backends/ │ │ │ ├── backends.tf │ │ │ └── providers.tf │ │ ├── lb_pool/ │ │ │ ├── lb-pool.tf │ │ │ └── providers.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── eventhub/ │ │ ├── eventhub.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── openai/ │ │ ├── openai.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── private_dns_zone/ │ │ ├── outputs.tf │ │ ├── privatednszone.tf │ │ └── variables.tf │ └── private_endpoint/ │ ├── outputs.tf │ ├── privateendpoint.tf │ └── variables.tf ├── provider.tf └── variables.tf ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "name": "Getting Started", "image": "mcr.microsoft.com/devcontainers/universal:2", "hostRequirements": { "cpus": 4 }, "features": { "ghcr.io/devcontainers/features/common-utils:2": { "configureZshAsDefaultShell": true, "installOhMyZsh": true, "installOhMyZshConfig": true }, "ghcr.io/stuartleeks/dev-container-features/shell-history:0": {}, "ghcr.io/devcontainers/features/azure-cli:1": { "installBicep": true, "version": "latest" }, "ghcr.io/stuartleeks/dev-container-features/azure-cli-persistence:0": {}, "ghcr.io/devcontainers/features/terraform:1": {} }, "waitFor": "onCreateCommand", "customizations": { "vscode": { "extensions": [ "eamodio.gitlens", "GitHub.copilot", "Gruntfuggly.todo-tree", "ionutvmi.path-autocomplete", "mechatroner.rainbow-csv", "ms-vsliveshare.vsliveshare", "redhat.vscode-yaml", "timonwong.shellcheck", "GitHub.vscode-pull-request-github", "humao.rest-client", "ms-azuretools.vscode-bicep", "ms-azuretools.vscode-azureterraform", "azapi-vscode.azapi" ], "settings": { "files.insertFinalNewline": true, "github.copilot.enable": { "markdown": true } } } }, "remoteEnv": { "HOST_PROJECT_PATH": "${localWorkspaceFolder}" }, "mounts": [ // map host ssh to container "source=${env:HOME}${env:USERPROFILE}/.ssh,target=/home/codespace/.ssh,type=bind,consistency=cached" ] } ================================================ FILE: .gitattributes ================================================ # Set bash scripts to use LF line endings regardless of the platform # used to clone the code *.sh text eol=lf ================================================ FILE: .github/workflows/lza-validate-multiregion-apim.yml ================================================ # This is a basic workflow to help you get started with Actions name: LZA-Validation-Multi-Region-APIM # Controls when the workflow will run on: # Triggers the workflow on push or pull request events but only for the "main" branch push: pull_request: branches: [ "main" ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v4 - uses: hashicorp/setup-terraform@v3 # Runs a set of commands using the runners shell - name: Run deploy-apim-baseline script run: | echo Performing Validation.... ls -la cd scenarios/scripts/terraform cp sample-multi-region.env .env ./deploy-apim-baseline.sh --validate-commit ================================================ FILE: .github/workflows/lza-validate-singleregion-apim.yml ================================================ # This is a basic workflow to help you get started with Actions name: LZA-Validation-Single-Region-APIM # Controls when the workflow will run on: # Triggers the workflow on push or pull request events but only for the "main" branch push: pull_request: branches: [ "main" ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v4 - uses: hashicorp/setup-terraform@v3 # Runs a set of commands using the runners shell - name: Run deploy-apim-baseline script run: | echo Performing Validation.... ls -la cd scenarios/scripts/terraform cp sample.env .env ./deploy-apim-baseline.sh --validate-commit ================================================ FILE: .gitignore ================================================ /.vs/ProjectSettings.json /.vs/slnx.sqlite deployment/bicep/parameters.local.json deployment/bicep/localparam*.json deployment/bicep/localmain.bicep deployment/bicep/localtestscript.ps1 # Terraform section # Local .terraform directories **/.terraform/* output.json parameters.json # .tfstate files *.tfstate *.tfstate.* *.tfplan # Crash log files crash.log crash.*.log # Exclude all .tfvars files, which are likely to contain sensitive data, such as # password, private keys, and other secrets. These should not be part of version # control as they are data points which are potentially sensitive and subject # to change depending on the environment. *.tfvars *.tfvars.json # Ignore override files as they are usually used to override resources locally and so # are not checked in override.tf override.tf.json *_override.tf *_override.tf.json # Include override files you do wish to add to version control using negated pattern # !example_override.tf # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan # example: *tfplan* # Ignore CLI configuration files .terraformrc terraform.rc # Don't copy local lock *.terraform.lock.hcl # Azure Functions localsettings file local.settings.json # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json project.fragment.lock.json artifacts/ *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted #*.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings node_modules/ orleans.codegen.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml # CodeRush .cr/ # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json !.vscode/*.code-snippets #Local .terraform directories **/.terraform/* .terraform/ # .tfstate files *.tfstate *.tfstate.* # tf plan files *.plan # Crash log files crash.log # Ignore override files as they are usually used to override resources locally override.tf override.tf.json *_override.tf *_override.tf.json **/azuredeploy.parameters.json scenarios/.env scenarios/apim-baseline/bicep/parameters.json # Rest client test files *.http *-backend.hcl scenarios/scripts/terraform/apim-self-signed.crt scenarios/scripts/terraform/apim-self-signed.key scenarios/scripts/terraform/tmp-self-signed-cert.conf scenarios/scripts/terraform/.env ================================================ FILE: .pre-commit-config.yaml ================================================ - repo: https://github.com/antonbabenko/pre-commit-terraform rev: v1.76.0 hooks: - id: terraform_fmt - id: terraform_docs ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Microsoft Open Source Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). Resources: - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) Microsoft Corporation. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE ================================================ FILE: README.md ================================================ # Azure API Management Landing Zone Accelerator Azure API Management Landing Zone Accelerator provides packaged guidance with reference architecture and reference implementation along with design guidance recommendations and considerations on critical design areas for provisioning APIM with a secure baseline. They are aligned with industry proven practices, such as those presented in [Azure landing zones](https://learn.microsoft.com/azure/cloud-adoption-framework/ready/landing-zone/) guidance in the Cloud Adoption Framework. ## Reference Architecture ![image](/docs/images/apim-secure-baseline.jpg) ## :mag: Design areas The enterprise architecture is broken down into six different design areas, where you can find the links to each at: | Design Area|Considerations|Recommendations| |:--------------:|:--------------:|:--------------:| | Identity and Access Management|[Design Considerations](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/app-platform/api-management/identity-and-access-management#design-considerations)|[Design Recommendations](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/app-platform/api-management/identity-and-access-management#design-recommendations)| | Network Topology and Connectivity|[Design Considerations](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/app-platform/api-management/network-topology-and-connectivity#design-considerations)|[Design Recommendations](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/app-platform/api-management/network-topology-and-connectivity#design-recommendations)| | Security|[Design Considerations](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/app-platform/api-management/security#design-considerations)|[Design Recommendations](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/app-platform/api-management/security#design-recommendations)| | Management|[Design Considerations](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/app-platform/api-management/management#design-considerations)|[Design Recommendations](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/app-platform/api-management/management#design-recommendation)| | Governance|[Design Considerations](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/app-platform/api-management/governance#design-considerations)|[Design Recommendations](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/app-platform/api-management/governance#design-recommendations)| | Platform Automation and DevOps|[Design Considerations](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/app-platform/api-management/platform-automation-and-devops#design-considerations)|[Design Recommendations](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/app-platform/api-management/platform-automation-and-devops#design-recommendations)| ## :rocket: Deployment scenarios This repo contains the Azure landing zone accelerator's reference implementations, all with supporting *Infrastructure as Code* artifacts. The scenarios covered are: ### :arrow_forward: [Scenario 1: Azure API Management - Secure Baseline](scenarios/apim-baseline/README.md) Deploys APIM with a secure baseline configuration with no backends and a sample API. ### :arrow_forward: [Scenario 2: Azure API Management - Function Backend](scenarios/workload-functions/README.md) On top of the secure baseline, deploys a private Azure function as a backend and provision APIs in APIM to access the function. ### :arrow_forward: [Scenario 3: Azure API Management - Gen AI Backend](scenarios/workload-genai/README.md) On top of the secure baseline, deploys private Azure OpenAI endpoints (3 endpoints) as backend and provision API that can handle [multiple use cases.](./scenarios/workload-genai/README.md#scenarios-handled-by-this-accelerator) *More reference implementation scenarios will be added as they become available.* ### Supported Regions Some of the new Azure OpenAI policies are not available in al the regions yet. If you see the deployment failures, try chosing a different region. The following regions are more likely to work. ```shell australiacentral, australiaeast, australiasoutheast, brazilsouth, eastasia, francecentral, germanywestcentral, koreacentral, northeurope, southeastasia, southcentralus, uksouth, ukwest, westeurope, westus2, westus3 ``` ## Got a feedback Please leverage issues if you have any feedback or request on how we can improve on this repository. --- ## Data Collection The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the repository. There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft's privacy statement. Our privacy statement is located at https://go.microsoft.com/fwlink/?LinkId=521839. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices. ### Telemetry Configuration Telemetry collection is on by default. To opt-out, set the variable ENABLE_TELEMETRY to `false` in [.env](./scenarios/.env) file. --- ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ## Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. ================================================ FILE: SECURITY.md ================================================ # Security Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://learn.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. ## Preferred Languages We prefer all communications to be in English. ## Policy Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). ================================================ FILE: SUPPORT.md ================================================ # Support ## How to file issues and get help This project uses GitHub Issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new Issue. ## Microsoft Support Policy Support for this **PROJECT or PRODUCT** is limited to the resources listed above. ================================================ FILE: scenarios/apim-baseline/README.md ================================================ # Scenario 1: Azure API Management - Secure Baseline This reference implementation demonstrates a *secure baseline infrastructure architecture* for provisioning [Azure API Management](https://learn.microsoft.com/azure/api-management/). Specifically this scenario addresses deploying Azure API Management into a [virtual network](https://learn.microsoft.com/azure/api-management/api-management-using-with-internal-vnet?tabs=stv2), in an internal mode where you can only access the API Management endpoints like API gateway, developer portal, Direct management and Git within a VNet whose access you control. By the end of this deployment guide, you would have deployed an "internal mode" Azure API Management premium instance. ![Architectural diagram showing an Azure API Management deployment in a virtual network.](../../docs/images/apim-secure-baseline.jpg) ## Core architecture components - Azure API Management (Developer SKU) - Azure Virtual Networks - Azure Application Gateway (with Web Application Firewall) - Azure Standard Public IP - Azure Key Vault - Azure Private Endpoint - Azure Private DNS Zones - Log Analytics Workspace - Azure Application Insights ## Deploy the reference implementation This reference implementation is provided with the following infrastructure as code options. Select the deployment guide you are interested in. They both deploy the same implementation. :arrow_forward: [Bicep-based deployment guide](./bicep/README.md) :arrow_forward: [Terraform-based deployment guide](./terraform/README.md) ================================================ FILE: scenarios/apim-baseline/bicep/README.md ================================================ # Azure API Management - Secure Baseline [Bicep] **Note:** This template may be out of date temporarily. Please use the [Terraform version](../terraform/README.md) of this scenario for the most up-to-date implementation. This is the Bicep-based deployment guide for [Scenario 1: Azure API Management - Secure Baseline](../README.md). ## Prerequisites This is the starting point for the instructions on deploying this reference implementation. There is required access and tooling you'll need in order to accomplish this. - An Azure subscription - The following resource providers [registered](https://learn.microsoft.com/azure/azure-resource-manager/management/resource-providers-and-types#register-resource-provider): - `Microsoft.ApiManagement` - `Microsoft.Network` - `Microsoft.KeyVault` - The user or service principal initiating the deployment process must have the [owner role](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#owner) at the subscription level to have the ability to create resource groups and to delegate access to others (Azure Managed Identities created from the IaC deployment). - Access to Bash command line to run the deployment script. - Latest [Azure CLI installed](https://learn.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest) (must be at least 2.40), or you can perform this from Azure Cloud Shell by clicking below. [![Launch Azure Cloud Shell](https://learn.microsoft.com/azure/includes/media/cloud-shell-try-it/launchcloudshell.png)](https://shell.azure.com) - JQ command line JSON processor installed ```bash sudo apt-get install jq ``` ## Steps 1. Clone/download this repo locally, or even better fork this repository. ```bash git clone https://github.com/Azure/apim-landing-zone-accelerator.git cd apim-landing-zone-accelerator/scenarios/scripts ``` 1. Log into Azure from the AZ CLI and select your subscription. ```bash az login ``` 1. Review and update deployment parameters. Copy the `sample.env` into a new file called `.env` in the same directory. ```bash The [**.env**](../../.env) parameter file is where you can customize your deployment. The defaults are a suitable starting point, but feel free to adjust any to fit your requirements. **Deployment parameters** | Name | Description | Default | Example(s) | | :---- | :---------- | :------ | :--------- | | `AZURE_LOCATION` | The Azure location to deploy to. | **eastus** | **westus** | | `RESOURCE_NAME_PREFIX` | A suffix for naming. | **apimdemo** | **appname** | | `ENVIRONMENT_TAG` | A tag that will be included in the naming. | **dev** | **stage** | | `APPGATEWAY_FQDN` | The Azure location to deploy to. | **apim.example.com** | **my.org.com** | | `CERT_TYPE` | selfsigned will create a self-signed certificate for the APPGATEWAY_FQDN. custom will use an existing certificate in pfx format that needs to be available in the [certs](../../certs) folder and named appgw.pfx | **selfsigned** | **custom** | | `CERT_PWD` | The password for the pfx certificate. Only required if CERT_TYPE is custom. | **N/A** | **password123** | | `RANDOM_IDENTIFIER` | Optional 3 character random string to ensure deployments are unique. Automatically assigned if not provided | **abc** | **pqr** | 1. Deploy the reference implementation. Run the following command to deploy the APIM baseline ```bash ./scripts/bicep/deploy-apim-baseline.sh ``` Test the echo api using the generated command from the output ## Troubleshooting If you see the message `-bash: ./deploy-apim-baseline.sh: /bin/bash^M: bad interpreter: No such file or directory` when running the script, you can fix this by running the following command: ```bash sed -i -e 's/\r$//' deploy-apim-baseline.sh ``` ================================================ FILE: scenarios/apim-baseline/bicep/apim/apim.bicep ================================================ targetScope='resourceGroup' /* * Input parameters */ @description('The name of the API Management resource to be created.') param apimName string @description('The subnet resource id to use for APIM.') @minLength(1) param apimSubnetId string @description('The email address of the publisher of the APIM resource.') @minLength(1) param publisherEmail string = 'apim@contoso.com' @description('Company name of the publisher of the APIM resource.') @minLength(1) param publisherName string = 'Contoso' @description('The pricing tier of the APIM resource.') param skuName string = 'Developer' @description('The instance size of the APIM resource.') param capacity int = 1 @description('Location for Azure resources.') param location string = resourceGroup().location param appInsightsName string param appInsightsId string param appInsightsInstrumentationKey string param keyVaultName string param keyVaultResourceGroupName string param vnetName string param networkingResourceGroupName string param apimRG string var echoSubscriptionKey = guid('echoPrimaryKey') /* * Resources */ var apimIdentityName = 'identity-${apimName}' resource apimIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = { name: apimIdentityName location: location } resource apimName_resource 'Microsoft.ApiManagement/service@2020-12-01' = { name: apimName location: location sku:{ capacity: capacity name: skuName } identity: { type:'UserAssigned' userAssignedIdentities: { '${apimIdentity.id}': {} } } properties:{ virtualNetworkType: 'Internal' publisherEmail: publisherEmail publisherName: publisherName virtualNetworkConfiguration: { subnetResourceId: apimSubnetId } apiVersionConstraint: { minApiVersion: '2019-12-01' } } } resource echoSubscription 'Microsoft.ApiManagement/service/subscriptions@2020-12-01' = { parent: apimName_resource name: 'Echo' properties: { displayName: 'Echo' scope: '/products/starter' primaryKey: echoSubscriptionKey } } resource apimName_appInsightsLogger_resource 'Microsoft.ApiManagement/service/loggers@2021-08-01' = { parent: apimName_resource name: appInsightsName properties: { loggerType: 'applicationInsights' resourceId: appInsightsId credentials: { instrumentationKey: appInsightsInstrumentationKey } } } resource apimName_applicationinsights 'Microsoft.ApiManagement/service/diagnostics@2021-08-01' = { parent: apimName_resource name: 'applicationinsights' properties: { loggerId: apimName_appInsightsLogger_resource.id alwaysLog: 'allErrors' sampling: { percentage: 100 samplingType: 'fixed' } metrics: true } } module kvaccess './modules/kvaccess.bicep' = { name: 'kvaccess' scope: resourceGroup(keyVaultResourceGroupName) params: { managedIdentity: apimIdentity keyVaultName: keyVaultName } } //Creation of private DNS zones module dnsZoneModule './modules/dnsrecords.bicep' = { name: 'apimDnsRecordsDeploy' scope: resourceGroup(networkingResourceGroupName) dependsOn: [ apimName_resource ] params: { vnetName: vnetName apimName: apimName apimRG: apimRG networkingResourceGroupName: networkingResourceGroupName } } output apimStarterSubscriptionKey string = echoSubscriptionKey output apimIdentityName string = apimIdentityName ================================================ FILE: scenarios/apim-baseline/bicep/apim/modules/dnsrecords.bicep ================================================ param vnetName string param networkingResourceGroupName string param apimName string param apimRG string resource apim 'Microsoft.ApiManagement/service@2020-12-01' existing = { name: apimName scope: resourceGroup(apimRG) } module dnsZone '../../shared/modules/dnszone.bicep' = { name: 'apimDnsZoneDeploy' params: { vnetName: vnetName networkingResourceGroupName: networkingResourceGroupName domain: '${apimName}.azure-api.net' } } resource apimDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = { name: '${apimName}.azure-api.net' } resource gatewayRecord 'Microsoft.Network/privateDnsZones/A@2020-06-01' = { parent: apimDnsZone name: '@' dependsOn: [ apim dnsZone ] properties: { aRecords: [ { ipv4Address: apim.properties.privateIPAddresses[0] } ] ttl: 36000 } } resource developerRecord 'Microsoft.Network/privateDnsZones/A@2020-06-01' = { parent: apimDnsZone name: 'developer' dependsOn: [ apim dnsZone ] properties: { aRecords: [ { ipv4Address: apim.properties.privateIPAddresses[0] } ] ttl: 36000 } } ================================================ FILE: scenarios/apim-baseline/bicep/apim/modules/kvaccess.bicep ================================================ param keyVaultName string param managedIdentity object resource accessPolicyGrant 'Microsoft.KeyVault/vaults/accessPolicies@2019-09-01' = { name: '${keyVaultName}/add' properties: { accessPolicies: [ { objectId: managedIdentity.properties.principalId tenantId: managedIdentity.properties.tenantId permissions: { secrets: [ 'get' 'list' ] certificates: [ 'get' 'list' ] } } ] } } ================================================ FILE: scenarios/apim-baseline/bicep/gateway/appgw.bicep ================================================ /* * Input parameters */ @description('The name of the Application Gateawy to be created.') param appGatewayName string @description('The FQDN of the Application Gateawy.Must match the TLS Certificate.') param appGatewayFQDN string @description('The location of the Application Gateawy to be created') param location string = resourceGroup().location @description('The subnet resource id to use for Application Gateway.') param appGatewaySubnetId string @description('Set to selfsigned if self signed certificates should be used for the Application Gateway. Set to custom and pass the CertData and CertKey if custom certificates should be used.') param appGatewayCertType string @description('The backend URL of the APIM.') param primaryBackendEndFQDN string @description('The Url for the APIM Health Probe.') param probeUrl string = '/status-0123456789abcdef' param appGatewayPublicIpName string param keyVaultName string param keyVaultResourceGroupName string param deploymentIdentityName string param deploymentSubnetId string param deploymentStorageName string param certKey string param certData string var appGatewayIdentityId = 'identity-${appGatewayName}' var appGatewayFirewallPolicy = 'waf-${appGatewayName}' resource appGatewayIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = { name: appGatewayIdentityId location: location } module certificate './modules/certificate.bicep' = { name: 'certificate' scope: resourceGroup(keyVaultResourceGroupName) params: { managedIdentity: appGatewayIdentity deploymentIdentityName: deploymentIdentityName deploymentSubnetId: deploymentSubnetId deploymentStorageName: deploymentStorageName keyVaultName: keyVaultName location: location appGatewayFQDN: appGatewayFQDN appGatewayCertType: appGatewayCertType certKey: certKey certData: certData } } resource appGatewayPublicIPAddress 'Microsoft.Network/publicIPAddresses@2019-09-01' existing = { name: appGatewayPublicIpName } resource appgw_waf_Pol 'Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies@2021-08-01' = { name: appGatewayFirewallPolicy location: location properties: { policySettings: { requestBodyCheck: true maxRequestBodySizeInKb: 128 fileUploadLimitInMb: 100 state: 'Enabled' mode: 'detection' } managedRules: { managedRuleSets: [ { ruleSetType: 'OWASP' ruleSetVersion: '3.2' } ] } } } resource appGatewayName_resource 'Microsoft.Network/applicationGateways@2019-09-01' = { name: appGatewayName location: location dependsOn: [ certificate ] identity: { type: 'UserAssigned' userAssignedIdentities: { '${appGatewayIdentity.id}': {} } } properties: { sku: { name: 'WAF_v2' tier: 'WAF_v2' } gatewayIPConfigurations: [ { name: 'appGatewayIpConfig' properties: { subnet: { id: appGatewaySubnetId } } } ] sslCertificates: [ { name: appGatewayFQDN properties: { keyVaultSecretId: certificate.outputs.secretUri } } ] sslPolicy: { minProtocolVersion: 'TLSv1_2' policyType: 'Custom' cipherSuites: [ 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256' 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384' 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256' 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384' 'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256' 'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384' 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256' 'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384' ] } trustedRootCertificates: [] frontendIPConfigurations: [ { name: 'appGwPublicFrontendIp' properties: { privateIPAllocationMethod: 'Dynamic' publicIPAddress: { id: appGatewayPublicIPAddress.id } } } ] frontendPorts: [ { name: 'port_443' properties: { port: 443 } } ] backendAddressPools: [ { name: 'apim' properties: { backendAddresses: [ { fqdn: primaryBackendEndFQDN } ] } } { name: 'sink-hole' properties: { backendAddresses: [] } } ] backendHttpSettingsCollection: [ { name: 'apim-demo-apis-https' properties: { port: 443 protocol: 'Https' cookieBasedAffinity: 'Disabled' hostName: primaryBackendEndFQDN pickHostNameFromBackendAddress: false requestTimeout: 20 probe: { id: resourceId('Microsoft.Network/applicationGateways/probes', appGatewayName, 'apim-demo-apis-https') } } } ] httpListeners: [ { name: 'apim-demo-apis-https' properties: { frontendIPConfiguration: { id: resourceId( 'Microsoft.Network/applicationGateways/frontendIPConfigurations', appGatewayName, 'appGwPublicFrontendIp' ) } frontendPort: { id: resourceId('Microsoft.Network/applicationGateways/frontendPorts', appGatewayName, 'port_443') } protocol: 'Https' sslCertificate: { id: resourceId('Microsoft.Network/applicationGateways/sslCertificates', appGatewayName, appGatewayFQDN) } hostnames: [ appGatewayFQDN ] requireServerNameIndication: false } } ] urlPathMaps: [ { name: 'urlPathMapApim' properties: { defaultBackendAddressPool: { id: resourceId( 'Microsoft.Network/applicationGateways/backendAddressPools', appGatewayName, 'apim' ) } defaultBackendHttpSettings: { id: resourceId( 'Microsoft.Network/applicationGateways/backendHttpSettingsCollection', appGatewayName, 'apim-demo-apis-https' ) } pathRules: [ { name: 'echo-api' properties: { paths: [ '/echo/*' ] backendAddressPool: { id: resourceId( 'Microsoft.Network/applicationGateways/backendAddressPools', appGatewayName, 'apim' ) } backendHttpSettings: { id: resourceId( 'Microsoft.Network/applicationGateways/backendHttpSettingsCollection', appGatewayName, 'apim-demo-apis-https' ) } } } { name: 'hello-api' properties: { paths: [ '/hello*' ] backendAddressPool: { id: resourceId( 'Microsoft.Network/applicationGateways/backendAddressPools', appGatewayName, 'apim' ) } backendHttpSettings: { id: resourceId( 'Microsoft.Network/applicationGateways/backendHttpSettingsCollection', appGatewayName, 'apim-demo-apis-https' ) } } } { name: 'openai-api' properties: { paths: [ '/openai/*' ] backendAddressPool: { id: resourceId( 'Microsoft.Network/applicationGateways/backendAddressPools', appGatewayName, 'apim' ) } backendHttpSettings: { id: resourceId( 'Microsoft.Network/applicationGateways/backendHttpSettingsCollection', appGatewayName, 'apim-demo-apis-https' ) } } } { name: 'default' properties: { paths: [ '/*' ] backendAddressPool: { id: resourceId( 'Microsoft.Network/applicationGateways/backendAddressPools', appGatewayName, 'sink-hole' ) } backendHttpSettings: { id: resourceId( 'Microsoft.Network/applicationGateways/backendHttpSettingsCollection', appGatewayName, 'apim-demo-apis-https' ) } } } ] } } ] requestRoutingRules: [ { name: 'apim-demo-apis' properties: { ruleType: 'PathBasedRouting' priority: 100 urlPathMap: { id: resourceId('Microsoft.Network/applicationGateways/urlPathMaps', appGatewayName, 'urlPathMapApim') } httpListener: { id: resourceId( 'Microsoft.Network/applicationGateways/httpListeners', appGatewayName, 'apim-demo-apis-https' ) } } } ] probes: [ { name: 'apim-demo-apis-https' properties: { protocol: 'Https' host: primaryBackendEndFQDN path: probeUrl interval: 30 timeout: 30 unhealthyThreshold: 3 pickHostNameFromBackendHttpSettings: false minServers: 0 match: { statusCodes: [ '200-399' ] } } } ] rewriteRuleSets: [] redirectConfigurations: [] firewallPolicy: { id: appgw_waf_Pol.id } enableHttp2: true autoscaleConfiguration: { minCapacity: 2 maxCapacity: 3 } } } output appGatewayPublicIpAddress string = appGatewayPublicIPAddress.properties.ipAddress ================================================ FILE: scenarios/apim-baseline/bicep/gateway/modules/certificate.bicep ================================================ param keyVaultName string param managedIdentity object param location string param appGatewayFQDN string param certKey string param certData string param appGatewayCertType string param deploymentIdentityName string param deploymentSubnetId string param deploymentStorageName string var secretName = replace(appGatewayFQDN,'.', '-') var subjectName='CN=${appGatewayFQDN}' var certPwd = appGatewayCertType == 'selfsigned' ? 'null' : certKey var certDataString = appGatewayCertType == 'selfsigned' ? 'null' : certData resource deploymentIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' existing = { name: deploymentIdentityName } resource accessPolicyGrantForCertificate 'Microsoft.KeyVault/vaults/accessPolicies@2019-09-01' = { name: '${keyVaultName}/add' properties: { accessPolicies: [ { objectId: managedIdentity.properties.principalId tenantId: managedIdentity.properties.tenantId permissions: { secrets: [ 'get' 'list' ] certificates: [ 'import' 'get' 'list' 'update' 'create' ] } } { objectId: deploymentIdentity.properties.principalId tenantId: deploymentIdentity.properties.tenantId permissions: { secrets: [ 'get' 'list' ] certificates: [ 'import' 'get' 'list' 'update' 'create' ] } } ] } } resource appGatewayCertificate 'Microsoft.Resources/deploymentScripts@2023-08-01' = { name: '${secretName}-certificate' dependsOn: [ accessPolicyGrantForCertificate ] location: location identity: { type: 'userAssigned' userAssignedIdentities: { '${deploymentIdentity.id}': {} } } kind: 'AzurePowerShell' properties: { azPowerShellVersion: '6.6' storageAccountSettings: { storageAccountName: deploymentStorageName } containerSettings: { subnetIds: [ { id: deploymentSubnetId } ] } arguments: ' -vaultName ${keyVaultName} -certificateName ${secretName} -subjectName ${subjectName} -certPwd ${certPwd} -certDataString ${certDataString} -certType ${appGatewayCertType}' scriptContent: ''' param( [string] [Parameter(Mandatory=$true)] $vaultName, [string] [Parameter(Mandatory=$true)] $certificateName, [string] [Parameter(Mandatory=$true)] $subjectName, [string] [Parameter(Mandatory=$true)] $certPwd, [string] [Parameter(Mandatory=$true)] $certDataString, [string] [Parameter(Mandatory=$true)] $certType ) $ErrorActionPreference = 'Stop' $DeploymentScriptOutputs = @{} if ($certType -eq 'selfsigned') { $policy = New-AzKeyVaultCertificatePolicy -SubjectName $subjectName -IssuerName Self -ValidityInMonths 12 -Verbose # private key is added as a secret that can be retrieved in the ARM template Add-AzKeyVaultCertificate -VaultName $vaultName -Name $certificateName -CertificatePolicy $policy -Verbose $newCert = Get-AzKeyVaultCertificate -VaultName $vaultName -Name $certificateName # it takes a few seconds for KeyVault to finish $tries = 0 do { Write-Host 'Waiting for certificate creation completion...' Start-Sleep -Seconds 10 $operation = Get-AzKeyVaultCertificateOperation -VaultName $vaultName -Name $certificateName $tries++ if ($operation.Status -eq 'failed') { throw 'Creating certificate $certificateName in vault $vaultName failed with error $($operation.ErrorMessage)' } if ($tries -gt 120) { throw 'Timed out waiting for creation of certificate $certificateName in vault $vaultName' } } while ($operation.Status -ne 'completed') } else { $ss = Convertto-SecureString -String $certPwd -AsPlainText -Force; Import-AzKeyVaultCertificate -Name $certificateName -VaultName $vaultName -CertificateString $certDataString -Password $ss } ''' retentionInterval: 'P1D' } } module appGatewaySecretsUri 'certificateSecret.bicep' = { name: '${secretName}-certificate' dependsOn: [ appGatewayCertificate ] params: { keyVaultName: keyVaultName secretName: secretName } } output secretUri string = appGatewaySecretsUri.outputs.secretUri ================================================ FILE: scenarios/apim-baseline/bicep/gateway/modules/certificateSecret.bicep ================================================ param keyVaultName string param secretName string resource keyVaultCertificate 'Microsoft.KeyVault/vaults/secrets@2021-06-01-preview' existing = { name: '${keyVaultName}/${secretName}' } output secretUri string = keyVaultCertificate.properties.secretUriWithVersion ================================================ FILE: scenarios/apim-baseline/bicep/main.bicep ================================================ targetScope = 'subscription' // Parameters @description('A short name for the workload being deployed alphanumberic only') @maxLength(8) param workloadName string @description('The environment for which the deployment is being executed') @allowed([ 'dev' 'uat' 'prod' 'dr' ]) param environment string param identifier string @description('The FQDN for the Application Gateway. Example - api.contoso.com.') param appGatewayFqdn string @description('The password for the TLS certificate for the Application Gateway. The pfx file needs to be copied to scenarios/apim-baseline/bicep/gateway/certs/appgw.pfx') param certKey string = 'placeholder' param certData string = 'placeholder' @description('Set to selfsigned if self signed certificates should be used for the Application Gateway. Set to custom and copy the pfx file to scenarios/apim-baseline/bicep/gateway/certs/appgw.pfx if custom certificates are to be used') param appGatewayCertType string param location string = deployment().location @description('Enable sending usage and telemetry feedback to Microsoft.') param enableTelemetry bool = true var telemetryId = 'ab1e5729-7452-41b2-9fbb-945cc51d9cd0-${location}-apimsb-main' // Variables var resourceSuffix = '${workloadName}-${environment}-${location}-${identifier}' var networkingResourceGroupName = 'rg-networking-${resourceSuffix}' var sharedResourceGroupName = 'rg-shared-${resourceSuffix}' var apimResourceGroupName = 'rg-apim-${resourceSuffix}' // Resource Names var apimName = 'apim-${resourceSuffix}' var appGatewayName = 'appgw-${resourceSuffix}' resource networkingRG 'Microsoft.Resources/resourceGroups@2021-04-01' = { name: networkingResourceGroupName location: location } resource sharedRG 'Microsoft.Resources/resourceGroups@2021-04-01' = { name: sharedResourceGroupName location: location } resource apimRG 'Microsoft.Resources/resourceGroups@2021-04-01' = { name: apimResourceGroupName location: location } module networking './networking/networking.bicep' = { name: 'networkingresources' scope: resourceGroup(networkingRG.name) params: { location: location resourceSuffix: resourceSuffix } } module shared './shared/shared.bicep' = { dependsOn: [ networking ] name: 'sharedresources' scope: resourceGroup(sharedRG.name) params: { workloadName: workloadName environment: environment identifier: identifier location: location resourceGroupName: sharedRG.name resourceSuffix: resourceSuffix vnetName: networking.outputs.apimCSVNetName privateEndpointSubnetid: networking.outputs.privateEndpointSubnetid networkingResourceGroupName: networkingRG.name deploymentSubnetId: networking.outputs.deploymentSubnetId } } module apimModule 'apim/apim.bicep' = { name: 'apimDeploy' scope: resourceGroup(apimRG.name) params: { apimName: apimName apimSubnetId: networking.outputs.apimSubnetid location: location appInsightsName: shared.outputs.appInsightsName appInsightsId: shared.outputs.appInsightsId appInsightsInstrumentationKey: shared.outputs.appInsightsInstrumentationKey keyVaultName: shared.outputs.keyVaultName keyVaultResourceGroupName: sharedRG.name networkingResourceGroupName: networkingRG.name apimRG: apimRG.name vnetName: networking.outputs.apimCSVNetName } } module appgwModule 'gateway/appgw.bicep' = { name: 'appgwDeploy' scope: resourceGroup(networkingRG.name) dependsOn: [ apimModule ] params: { appGatewayName: appGatewayName appGatewayFQDN: appGatewayFqdn location: location appGatewaySubnetId: networking.outputs.appGatewaySubnetid primaryBackendEndFQDN: '${apimName}.azure-api.net' keyVaultName: shared.outputs.keyVaultName keyVaultResourceGroupName: sharedRG.name appGatewayCertType: appGatewayCertType certKey: certKey certData: certData appGatewayPublicIpName: networking.outputs.appGatewayPublicIpName deploymentIdentityName: shared.outputs.deploymentIdentityName deploymentSubnetId: networking.outputs.deploymentSubnetId deploymentStorageName: shared.outputs.deploymentStorageName } } @description('Microsoft telemetry deployment.') #disable-next-line no-deployments-resources resource telemetrydeployment 'Microsoft.Resources/deployments@2021-04-01' = if (enableTelemetry) { location: location name: telemetryId properties: { mode: 'Incremental' template: { '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' contentVersion: '1.0.0.0' resources: {} } } } output resourceSuffix string = resourceSuffix output networkingResourceGroupName string = networkingResourceGroupName output sharedResourceGroupName string = sharedResourceGroupName output apimResourceGroupName string = apimResourceGroupName output apimName string = apimName output apimIdentityName string = apimModule.outputs.apimIdentityName output vnetId string = networking.outputs.apimCSVNetId output vnetName string = networking.outputs.apimCSVNetName output privateEndpointSubnetid string = networking.outputs.privateEndpointSubnetid output deploymentIdentityName string = shared.outputs.deploymentIdentityName output deploymentSubnetId string = networking.outputs.deploymentSubnetId output deploymentStorageName string = shared.outputs.deploymentStorageName output keyVaultName string = shared.outputs.keyVaultName output appGatewayName string = appGatewayName output appGatewayPublicIpAddress string = appgwModule.outputs.appGatewayPublicIpAddress output apimStarterSubscriptionKey string = apimModule.outputs.apimStarterSubscriptionKey ================================================ FILE: scenarios/apim-baseline/bicep/networking/networking.bicep ================================================ param apimCSVNetNameAddressPrefix string = '10.2.0.0/16' param appGatewayAddressPrefix string = '10.2.4.0/24' param apimAddressPrefix string = '10.2.7.0/24' param privateEndpointAddressPrefix string = '10.2.5.0/24' param deploymentAddressPrefix string = '10.2.8.0/24' param location string @description('Standardized suffix text to be added to resource names') param resourceSuffix string // Variables var owner = 'APIM Const Set' var apimCSVNetName = 'vnet-apim-cs-${resourceSuffix}' var appGatewaySubnetName = 'snet-apgw-${resourceSuffix}' var apimSubnetName = 'snet-apim-${resourceSuffix}' var appGatewaySNNSG = 'nsg-apgw-${resourceSuffix}' var apimSNNSG = 'nsg-apim-${resourceSuffix}' var privateEndpointSubnetName = 'snet-prep-${resourceSuffix}' var privateEndpointSNNSG = 'nsg-prep-${resourceSuffix}' var deploymentSubnetName = 'snet-deploy-${resourceSuffix}' var appGatewayPublicIpName = 'pip-appgw-${resourceSuffix}' // Resources - VNet - SubNets resource vnetApimCs 'Microsoft.Network/virtualNetworks@2021-02-01' = { name: apimCSVNetName location: location tags: { Owner: owner } properties: { addressSpace: { addressPrefixes: [ apimCSVNetNameAddressPrefix ] } enableVmProtection: false enableDdosProtection: false subnets: [ { name: appGatewaySubnetName properties: { addressPrefix: appGatewayAddressPrefix networkSecurityGroup: { id: appGatewayNSG.id } } } { name: apimSubnetName properties: { addressPrefix: apimAddressPrefix networkSecurityGroup: { id: apimNSG.id } } } { name: privateEndpointSubnetName properties: { addressPrefix: privateEndpointAddressPrefix networkSecurityGroup: { id: privateEndpointNSG.id } privateEndpointNetworkPolicies: 'Disabled' } } { name: deploymentSubnetName properties: { addressPrefix: deploymentAddressPrefix serviceEndpoints: [ { service: 'Microsoft.Storage' } ] delegations: [ { name: 'Microsoft.ContainerInstance.containerGroups' properties: { serviceName: 'Microsoft.ContainerInstance/containerGroups' } } ] } } ] } } // Network Security Groups (NSG) resource appGatewayNSG 'Microsoft.Network/networkSecurityGroups@2020-06-01' = { name: appGatewaySNNSG location: location properties: { securityRules: [ { name: 'AllowHealthProbes' properties: { protocol: '*' sourcePortRange: '*' destinationPortRange: '65200-65535' sourceAddressPrefix: 'GatewayManager' destinationAddressPrefix: '*' access: 'Allow' priority: 100 direction: 'Inbound' } } { name: 'AllowClientTrafficToSubnet' properties: { protocol: 'Tcp' sourcePortRange: '*' destinationPortRanges: ['80', '443'] sourceAddressPrefix: '*' destinationAddressPrefix: appGatewayAddressPrefix access: 'Allow' priority: 110 direction: 'Inbound' } } { name: 'AllowClientTrafficToFrontendIP' properties: { protocol: 'Tcp' sourcePortRange: '*' destinationPortRanges: ['80', '443'] sourceAddressPrefix: '*' destinationAddressPrefix: '${pipAppGw.properties.ipAddress}/32' access: 'Allow' priority: 111 direction: 'Inbound' } } { name: 'AllowAzureLoadBalancer' properties: { protocol: '*' sourcePortRange: '*' destinationPortRange: '*' sourceAddressPrefix: 'AzureLoadBalancer' destinationAddressPrefix: '*' access: 'Allow' priority: 120 direction: 'Inbound' } } ] } } resource apimNSG 'Microsoft.Network/networkSecurityGroups@2020-06-01' = { name: apimSNNSG location: location properties: { securityRules: [ { name: 'AllowApimManagement' properties: { priority: 2000 sourceAddressPrefix: 'ApiManagement' protocol: 'Tcp' destinationPortRange: '3443' access: 'Allow' direction: 'Inbound' sourcePortRange: '*' destinationAddressPrefix: 'VirtualNetwork' } } { name: 'AllowAzureLoadBalancer' properties: { priority: 2010 sourceAddressPrefix: 'AzureLoadBalancer' protocol: 'Tcp' destinationPortRange: '6390' access: 'Allow' direction: 'Inbound' sourcePortRange: '*' destinationAddressPrefix: 'VirtualNetwork' } } { name: 'AllowAzureTrafficManager' properties: { priority: 2020 sourceAddressPrefix: 'AzureTrafficManager' protocol: 'Tcp' destinationPortRange: '443' access: 'Allow' direction: 'Inbound' sourcePortRange: '*' destinationAddressPrefix: 'VirtualNetwork' } } { name: 'AllowStorage' properties: { priority: 2000 sourceAddressPrefix: 'VirtualNetwork' protocol: 'Tcp' destinationPortRange: '443' access: 'Allow' direction: 'Outbound' sourcePortRange: '*' destinationAddressPrefix: 'Storage' } } { name: 'AllowSql' properties: { priority: 2010 sourceAddressPrefix: 'VirtualNetwork' protocol: 'Tcp' destinationPortRange: '1433' access: 'Allow' direction: 'Outbound' sourcePortRange: '*' destinationAddressPrefix: 'SQL' } } { name: 'AllowKeyVault' properties: { priority: 2020 sourceAddressPrefix: 'VirtualNetwork' protocol: 'Tcp' destinationPortRange: '443' access: 'Allow' direction: 'Outbound' sourcePortRange: '*' destinationAddressPrefix: 'AzureKeyVault' } } { name: 'AllowMonitor' properties: { priority: 2030 sourceAddressPrefix: 'VirtualNetwork' protocol: 'Tcp' destinationPortRanges: ['1886', '443'] access: 'Allow' direction: 'Outbound' sourcePortRange: '*' destinationAddressPrefix: 'AzureMonitor' } } ] } } resource privateEndpointNSG 'Microsoft.Network/networkSecurityGroups@2020-06-01' = { name: privateEndpointSNNSG location: location properties: { securityRules: [] } } // Public IP resource pipAppGw 'Microsoft.Network/publicIPAddresses@2023-04-01' = { name: appGatewayPublicIpName location: location sku: { name: 'Standard' } zones: ['1', '2', '3'] properties: { publicIPAddressVersion: 'IPv4' publicIPAllocationMethod: 'Static' } } // Output section output apimCSVNetName string = apimCSVNetName output apimCSVNetId string = vnetApimCs.id output appGatewaySubnetName string = appGatewaySubnetName output apimSubnetName string = apimSubnetName output privateEndpointSubnetName string = privateEndpointSubnetName output appGatewaySubnetid string = '${vnetApimCs.id}/subnets/${appGatewaySubnetName}' output apimSubnetid string = '${vnetApimCs.id}/subnets/${apimSubnetName}' output privateEndpointSubnetid string = '${vnetApimCs.id}/subnets/${privateEndpointSubnetName}' output deploymentSubnetId string = '${vnetApimCs.id}/subnets/${deploymentSubnetName}' output deploymentSubnetName string = deploymentSubnetName output publicIpAppGw string = pipAppGw.id output appGatewayPublicIpName string = appGatewayPublicIpName ================================================ FILE: scenarios/apim-baseline/bicep/shared/modules/azmon.bicep ================================================ targetScope='resourceGroup' // Parameters @description('Azure location to which the resources are to be deployed') param location string @description('Standardized suffix text to be added to resource names') param resourceSuffix string // Variables var appInsightsName = 'appi-${resourceSuffix}' var logAnalyticsWorkspaceName = 'log-${resourceSuffix}' // Resources resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-06-01' = { name: logAnalyticsWorkspaceName location: location properties: any({ retentionInDays: 30 features: { searchVersion: 1 } sku: { name: 'PerGB2018' } }) } resource appInsights 'Microsoft.Insights/components@2020-02-02' = { name: appInsightsName location: location kind: 'web' properties: { Application_Type: 'web' WorkspaceResourceId: logAnalyticsWorkspace.id } } output appInsightsConnectionString string = appInsights.properties.ConnectionString output appInsightsName string = appInsights.name output appInsightsId string = appInsights.id output appInsightsInstrumentationKey string = appInsights.properties.InstrumentationKey ================================================ FILE: scenarios/apim-baseline/bicep/shared/modules/dnszone.bicep ================================================ param vnetName string param networkingResourceGroupName string param domain string resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' existing = { name: vnetName scope: resourceGroup(networkingResourceGroupName) } resource dnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { name: domain location: 'global' } resource vnetLinks 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { name: vnetName parent: dnsZone location: 'global' properties: { virtualNetwork: { id: vnet.id } registrationEnabled: false } } output dnsZoneName string = dnsZone.name output dnsZoneId string = dnsZone.id output vnetLinksLink string = vnetLinks.id ================================================ FILE: scenarios/apim-baseline/bicep/shared/modules/privatedeploy.bicep ================================================ param resourceSuffix string param location string param deploymentSubnetId string var userAssignedIdentityName = 'mi-deploy-${resourceSuffix}' param storageAccountName string = toLower(take(replace('stdep${resourceSuffix}', '-',''), 24)) resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { name: storageAccountName location: location sku: { name: 'Standard_LRS' } kind: 'StorageV2' properties: { networkAcls: { bypass: 'AzureServices' virtualNetworkRules: [ { id: deploymentSubnetId action: 'Allow' state: 'Succeeded' } ] defaultAction: 'Deny' } } } resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { name: userAssignedIdentityName location: location } resource storageFileDataPrivilegedContributor 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { name: '69566ab7-960f-475b-8e7c-b3118f30c6bd' // Storage File Data Privileged Contributor scope: tenant() } resource roleAssignmentStorage 'Microsoft.Authorization/roleAssignments@2022-04-01' = { scope: storageAccount name: guid(storageFileDataPrivilegedContributor.id, userAssignedIdentity.id, storageAccount.id) properties: { principalId: userAssignedIdentity.properties.principalId roleDefinitionId: storageFileDataPrivilegedContributor.id principalType: 'ServicePrincipal' } } output deploymentIdentityName string = userAssignedIdentityName output deploymentStorageName string = storageAccountName ================================================ FILE: scenarios/apim-baseline/bicep/shared/modules/privateendpoint.bicep ================================================ param privateEndpointName string param groupId string param location string param vnetName string param networkingResourceGroupName string param subnetId string param serviceResourceId string param createDnsZone bool = true param domain string resource privateEndpoint 'Microsoft.Network/privateEndpoints@2021-03-01' = { name: privateEndpointName location: location properties: { subnet: { id: subnetId } privateLinkServiceConnections: [ { name: privateEndpointName properties: { privateLinkServiceId: serviceResourceId groupIds: [ groupId ] } } ] } } module dnsZoneNew './dnszone.bicep' = if (createDnsZone == true) { name: take('${replace(domain, '.', '-')}-deploy', 64) params: { vnetName: vnetName networkingResourceGroupName: networkingResourceGroupName domain: domain } dependsOn: [ privateEndpoint ] } resource dnsZone 'Microsoft.Network/privateDnsZones@2018-09-01' existing = if (createDnsZone == false) { name: domain } var dnsZoneName = (createDnsZone == true) ? dnsZoneNew.outputs.dnsZoneName : dnsZone.name var dnsZoneId = (createDnsZone == true) ? dnsZoneNew.outputs.dnsZoneId : dnsZone.id resource dnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2020-03-01' = { name: 'default' parent: privateEndpoint properties: { privateDnsZoneConfigs: [ { name: dnsZoneName properties: { privateDnsZoneId: dnsZoneId } } ] } } output privateEndpointId string = privateEndpoint.id output dnsZoneGroupId string = dnsZoneGroup.id ================================================ FILE: scenarios/apim-baseline/bicep/shared/shared.bicep ================================================ targetScope='resourceGroup' // Parameters @description('A short name for the workload being deployed') param workloadName string @description('The environment for which the deployment is being executed') @allowed([ 'dev' 'uat' 'prod' 'dr' ]) param environment string param identifier string @description('Azure location to which the resources are to be deployed') param location string param vnetName string param privateEndpointSubnetid string param deploymentSubnetId string param networkingResourceGroupName string @description('The name of the shared resource group') param resourceGroupName string @description('Standardized suffix text to be added to resource names') param resourceSuffix string // Variables - ensure key vault name does not end with '-' var tempKeyVaultName = take('kv-${workloadName}-${environment}-${location}', 20) // Must be between 3-24 alphanumeric characters var uniqueKeyVaultName = take('${tempKeyVaultName}-${identifier}', 24) var keyVaultName = endsWith(uniqueKeyVaultName, '-') ? substring(uniqueKeyVaultName, 0, length(uniqueKeyVaultName) - 1) : uniqueKeyVaultName var privateEndpoint_keyvault_Name = 'pep-kv-${resourceSuffix}' // Resources module appInsights './modules/azmon.bicep' = { name: 'azmon' scope: resourceGroup(resourceGroupName) params: { location: location resourceSuffix: resourceSuffix } } resource key_vault 'Microsoft.KeyVault/vaults@2023-07-01' = { name: keyVaultName location: location properties: { tenantId: subscription().tenantId sku: { family: 'A' name: 'standard' } publicNetworkAccess: 'Disabled' networkAcls: { bypass: 'AzureServices' defaultAction: 'Deny' ipRules: [] virtualNetworkRules: [] } accessPolicies: [ ] } } module keyvaultPrivateEndpoint './modules/privateendpoint.bicep' = { name: privateEndpoint_keyvault_Name scope: resourceGroup(networkingResourceGroupName) params: { location: location privateEndpointName: privateEndpoint_keyvault_Name groupId: 'vault' serviceResourceId: key_vault.id vnetName: vnetName networkingResourceGroupName: networkingResourceGroupName subnetId: privateEndpointSubnetid domain:'privatelink.vaultcore.azure.net' } } module deploy './modules/privatedeploy.bicep' = { name: 'deploymenEssentials' params: { location: location resourceSuffix: resourceSuffix deploymentSubnetId: deploymentSubnetId } } // Outputs output appInsightsConnectionString string = appInsights.outputs.appInsightsConnectionString output appInsightsName string=appInsights.outputs.appInsightsName output appInsightsId string=appInsights.outputs.appInsightsId output appInsightsInstrumentationKey string=appInsights.outputs.appInsightsInstrumentationKey output keyVaultName string = key_vault.name output deploymentIdentityName string = deploy.outputs.deploymentIdentityName output deploymentStorageName string = deploy.outputs.deploymentStorageName ================================================ FILE: scenarios/apim-baseline/terraform/README.md ================================================ # Azure API Management - Secure Baseline [Terraform] - Single Region Deployment - Optional Multi-Region High Availability - Optional Zone Redundancy This is the Terraform-based deployment guide for [Scenario 1: Azure API Management - Secure Baseline](../README.md). ## Sections - [Prerequisites](#prerequisites) - [Quick Start](#quick-start) - [Deployment Customization](#deployment-customization) - [Deployment Steps](#deployment-steps) - [Using a Terraform AzureRM backend](#using-a-terraform-azurerm-backend) - [Troubleshooting](#troubleshooting) ## Prerequisites This is the starting point for the instructions on deploying this reference implementation. There is the required access and tooling you'll need in order to accomplish this. - An Azure subscription - The following resource providers [registered](https://learn.microsoft.com/azure/azure-resource-manager/management/resource-providers-and-types#register-resource-provider): - `Microsoft.ApiManagement` - `Microsoft.Network` - `Microsoft.KeyVault` - The user or service principal initiating the deployment process must either the [owner role](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#owner) or the permissions below for a least privilege setup: | Role | Level | Why | | :---- | :---------- | :------ | | Contributor | Subscription | The plan needs the ability to create resource groups | | User Access Administrator | Subscription | The plan delegate access to Azure Managed Identities created by the deployment. The UAA role can be scoped to just "Storage File Data Privileged Contributor" for security hardening. | - Access to Bash command line to run the deployment script. - Latest [Azure CLI installed](https://learn.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest) (must be at least 2.40), or you can perform this from Azure Cloud Shell by clicking below. [![Launch Azure Cloud Shell](https://learn.microsoft.com/azure/includes/media/cloud-shell-try-it/launchcloudshell.png)](https://shell.azure.com) - JQ command line JSON processor installed ```bash sudo apt-get install jq ``` - Terraform installed. You can download the latest version from the [Terraform website](https://www.terraform.io/downloads.html). However, if using the dev container, this will not need to be downloaded and installed separately. ## Quick Start This will yield a working deployment for testing/poc, with no customizations. ```bash # Login to Azure az login # Clone the repo git clone https://github.com/Azure/apim-landing-zone-accelerator.git # Change into the scripts/terraform directory cd apim-landing-zone-accelerator/scenarios/scripts/terraform # Use the provided sample environment file cp sample.env .env to create one and edit as needed # Deploy the APIM baseline ./deploy-apim-baseline.sh # Follow on screen prompts ``` ## Deployment Customization Review and update deployment parameters. The **.env** parameter file is where you can customize your deployment. The defaults are a suitable starting point, but feel free to adjust any to fit your requirements. Copy the [`sample.env`](../../scripts/terraform/sample.env) into a new file called `.env` in the same directory. ```bash cp sample.env .env ``` **Deployment parameters** | Name | Description | Default | Example(s) | | :---- | :---------- | :------ | :--------- | | `AZURE_LOCATION` | The Azure location to deploy to. | **eastus2** | **eastus2** | | `MULT_REGION`| Should this deployment extend to a secondary location? | **false** | **true** | | `AZURE_LOCATION2`| The Azure secondary location to deploy to? | **centralus** | **centralus** | | `ZONE_REDUNDANT` | Should the deployment be zone redundant. | **false** | **true** | | `RESOURCE_NAME_PREFIX` | A suffix for naming. | **apimdemo** | **appname** | | `ENVIRONMENT_TAG` | A tag that will be included in the naming. | **dev** | **stage** | | `APPGATEWAY_FQDN` | The Azure location to deploy to. | **apim.example.com** | **my.org.com** | | `CERT_TYPE` | selfsigned will create a self-signed certificate for the APPGATEWAY_FQDN. custom will use an existing certificate in pfx format that needs to be available in the [certs](../../certs) folder and named appgw.pfx | **selfsigned** | **custom** | | `CERT_PWD` | The password for the pfx certificate. Only required if CERT_TYPE is custom. | **N/A** | **password123** | | `RANDOM_IDENTIFIER` | Optional 3 character random string to ensure deployments are unique. Automatically assigned if not provided | **abc** | **pqr** | ### examples `.env` file - Single region, Single Zone deployment with Developer SKU ```bash AZURE_LOCATION='eastus2' RESOURCE_NAME_PREFIX='lzv01' ENVIRONMENT_TAG='dev' APPGATEWAY_FQDN='apim.example.com' CERT_TYPE='selfsigned' ZONE_REDUNDANT='false' MULTI_REGION='false' AZURE_LOCATION2='' ``` - Single region and Zone redundant deployment with Premium SKU ```bash AZURE_LOCATION='eastus2' RESOURCE_NAME_PREFIX='lzv01' ENVIRONMENT_TAG='dev' APPGATEWAY_FQDN='apim.example.com' CERT_TYPE='selfsigned' ZONE_REDUNDANT='true' MULTI_REGION='false' ``` - Multi-region and Zone Redundant deployment with Premium SKU ```bash AZURE_LOCATION='eastus2' RESOURCE_NAME_PREFIX='lzv01' ENVIRONMENT_TAG='dev' APPGATEWAY_FQDN='apim.example.com' CERT_TYPE='selfsigned' ZONE_REDUNDANT='true' MULTI_REGION='true' AZURE_LOCATION2='centralus' ``` ## Deployment Steps 1. Clone/download this repo locally, or even better fork this repository. ```bash git clone https://github.com/Azure/apim-landing-zone-accelerator.git --branch wip/apim-lza cd apim-landing-zone-accelerator/scenarios/scripts/terraform ``` 2. Log into Azure from the AZ CLI and select your subscription. ```bash az login ``` 3. Deploy the reference implementation. Run the following command to deploy the APIM baseline ```bash # Note, you haven't created an .env file as explained above # use cp sample.env .env to create one and edit as needed ./deploy-apim-baseline.sh ``` Notes: - During script execution, you will encounter prompts and will need to respond with a 'y' to continue. - If error: "Error: .env file not found in the current directory.", please refer to the [Deployment Configuration](#deployment-configuration) section to create the .env file. Test the echo api using the generated command from the output. ## Using a Terraform AzureRM backend For terraform, you have the option to setup a backend [tf backend](https://developer.hashicorp.com/terraform/language/settings/backends/configuration). As part of the repository we provide a `azure-backend-sample.sh` script. This script will create a storage account and a container to store the terraform state. You can run the script with the following command: ```bash ./azure-backend-sample.sh \ --resource-group my-resource-group \ --storage-account mystorageaccount \ --container my-container ``` An `${ENVIRONMENT_TAG}-backend.hcl` file will be created automatically in the same directory as your `.env`. The file looks like this: ```hcl resource_group_name = "my-resource-group" storage_account_name = "mystorageaccount" container_name = "my-container" ``` Note: When using an AZURERM Backend and if your deployment is using a service principal vs a user account to login, make sure to also follow the terraform guidance here: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/service_principal_client_secret#configuring-the-service-principal-in-terraform ## Troubleshooting If you see the message `-bash: ./deploy-apim-baseline.sh: /bin/bash^M: bad interpreter: No such file or directory` when running the script, you can fix this by running the following command: ```bash sed -i -e 's/\r$//' deploy-apim-baseline.sh ``` ================================================ FILE: scenarios/apim-baseline/terraform/backend.tf.sample ================================================ # terraform { # backend "azurerm" { # # ---------------------- # # Will be passing in these arguments via CLI as the state file \ # # is now being overwritten via local testing environments # # > https://developer.hashicorp.com/terraform/language/settings/backends/configuration#command-line-key-value-pairs # # ---------------------- # # e.g: terraform init \ # # -backend-config="resource_group_name=rg-tfstate-auseast" \ # # -backend-config="storage_account_name=tfstateauseaststorage" \ # # -backend-config="container_name=apimlza" \ # # -backend-config="key=terraform-apimlza-dev-v2.tfstate" # # ---------------------- # # resource_group_name = "rg-tfstate-auseast" # # storage_account_name = "tfstateauseaststorage" # # container_name = "apimlza" # # key = "terraform-apimlza-dev-v6.tfstate" # } # } ================================================ FILE: scenarios/apim-baseline/terraform/main.tf ================================================ # This module deploys an Azure API Management (APIM) service in a single region. module "apim_baseline_single_region" { count = var.multiRegionEnabled ? 0 : 1 source = "./single-region" location = var.location workloadName = var.workloadName appGatewayFqdn = var.appGatewayFqdn appGatewayCertType = var.appGatewayCertType environment = var.environment apimCSVNetNameAddressPrefix = var.apimCSVNetNameAddressPrefix appGatewayAddressPrefix = var.appGatewayAddressPrefix apimAddressPrefix = var.apimAddressPrefix privateEndpointAddressPrefix = var.privateEndpointAddressPrefix deploymentAddressPrefix = var.deploymentAddressPrefix additionalClientIds = var.additionalClientIds certificatePassword = var.certificatePassword certificatePath = var.certificatePath identifier = var.identifier zoneRedundantEnabled = var.zoneRedundantEnabled } module "apim_baseline_multi_region" { count = var.multiRegionEnabled ? 1 : 0 source = "./multi-region" location = var.location workloadName = var.workloadName appGatewayFqdn = var.appGatewayFqdn appGatewayCertType = var.appGatewayCertType environment = var.environment apimCSVNetNameAddressPrefix = var.apimCSVNetNameAddressPrefix appGatewayAddressPrefix = var.appGatewayAddressPrefix apimAddressPrefix = var.apimAddressPrefix privateEndpointAddressPrefix = var.privateEndpointAddressPrefix deploymentAddressPrefix = var.deploymentAddressPrefix additionalClientIds = var.additionalClientIds certificatePassword = var.certificatePassword certificatePath = var.certificatePath identifier = var.identifier apimCSVNetNameSecondAddressPrefix = var.apimCSVNetNameSecondAddressPrefix appGatewaySecondAddressPrefix = var.appGatewaySecondAddressPrefix apimSecondAddressPrefix = var.apimSecondAddressPrefix privateEndpointSecondAddressPrefix = var.privateEndpointSecondAddressPrefix deploymentSecondAddressPrefix = var.deploymentSecondAddressPrefix locationSecond = var.locationSecond zoneRedundantEnabled = var.zoneRedundantEnabled } ================================================ FILE: scenarios/apim-baseline/terraform/modules/apim/apim.tf ================================================ locals { apimName = "apim-${var.resourceSuffix}" apimPipPrimaryPip = "pip-apim-${var.resourceSuffix}" apimIdentityName = "identity-${local.apimName}" skuCount = var.zoneRedundantEnabled ? 3 : 1 skuNameAuto = var.zoneRedundantEnabled ? "Premium_${local.skuCount}" : var.skuName zones = var.zoneRedundantEnabled ? ["1", "2", "3"] : null } resource "azurerm_user_assigned_identity" "apimIdentity" { name = local.apimIdentityName location = var.location resource_group_name = var.resourceGroupName } data "azurerm_key_vault" "keyVault" { name = var.keyVaultName resource_group_name = var.sharedResourceGroupName } #------------------------------- # Creation of an internal APIM instance #------------------------------- resource "azurerm_api_management" "apim_internal" { name = local.apimName location = var.location resource_group_name = var.resourceGroupName publisher_name = var.publisherName publisher_email = var.publisherEmail virtual_network_type = "Internal" sku_name = local.skuNameAuto zones = local.zones min_api_version = "2019-12-01" virtual_network_configuration { subnet_id = var.apimSubnetId } identity { type = "UserAssigned" identity_ids = ["${azurerm_user_assigned_identity.apimIdentity.id}"] } lifecycle { #prevent_destroy = true } } #------------------------------- # Creation of the apim logger entity #------------------------------- resource "azurerm_api_management_logger" "apim_logger" { name = "apim-logger" api_management_name = azurerm_api_management.apim_internal.name resource_group_name = var.resourceGroupName resource_id = var.workspaceId application_insights { instrumentation_key = var.instrumentationKey } lifecycle { #prevent_destroy = true } } #------------------------------- # API management service diagnostic #------------------------------- resource "azurerm_api_management_diagnostic" "apim_diagnostic" { identifier = "applicationinsights" resource_group_name = var.resourceGroupName api_management_name = azurerm_api_management.apim_internal.name api_management_logger_id = azurerm_api_management_logger.apim_logger.id sampling_percentage = 100.0 always_log_errors = true verbosity = "verbose" #possible value are verbose, error, information frontend_request { body_bytes = 32 headers_to_log = [ "content-type", "accept", "origin", ] } frontend_response { body_bytes = 32 headers_to_log = [ "content-type", "content-length", "origin", ] } backend_request { body_bytes = 32 headers_to_log = [ "content-type", "accept", "origin", ] } backend_response { body_bytes = 32 headers_to_log = [ "content-type", "content-length", "origin", ] } lifecycle { #prevent_destroy = true } } resource "azurerm_api_management_product" "starter" { display_name = "Starter" product_id = "starter" api_management_name = azurerm_api_management.apim_internal.name resource_group_name = azurerm_api_management.apim_internal.resource_group_name published = true lifecycle { #prevent_destroy = true } } resource "random_uuid" "starter_key" { lifecycle { #prevent_destroy = true } } resource "azurerm_api_management_subscription" "echo" { api_management_name = azurerm_api_management.apim_internal.name resource_group_name = azurerm_api_management.apim_internal.resource_group_name product_id = azurerm_api_management_product.starter.id display_name = "Echo API" primary_key = random_uuid.starter_key.result allow_tracing = false state = "active" lifecycle { #prevent_destroy = true } } #------------------------------- # Importing the Echo API into API Management #------------------------------- resource "azurerm_api_management_api" "echo_api" { name = "echo-api" api_management_name = azurerm_api_management.apim_internal.name resource_group_name = azurerm_api_management.apim_internal.resource_group_name revision = "1" display_name = "Echo API" path = "echo" protocols = ["https"] service_url = "https://httpbin.io/anything" lifecycle { #prevent_destroy = true } } resource "azurerm_api_management_api_operation" "echo_api_operation" { api_name = azurerm_api_management_api.echo_api.name api_management_name = azurerm_api_management.apim_internal.name resource_group_name = azurerm_api_management.apim_internal.resource_group_name display_name = "Retrieve resource" method = "GET" url_template = "/resource" request { query_parameter { type = "string" name = "param1" default_value = "sample" required = true } query_parameter { type = "number" name = "param2" required = false } } response { status_code = 200 description = "A demonstration of a GET call on a sample resource. It is handled by an \"echo\" backend which returns a response equal to the request (the supplied headers and body are being returned as received)." } operation_id = "retrieve-resource" lifecycle { #prevent_destroy = true } } resource "azurerm_api_management_product_api" "echo" { api_name = azurerm_api_management_api.echo_api.name product_id = azurerm_api_management_product.starter.product_id api_management_name = azurerm_api_management.apim_internal.name resource_group_name = azurerm_api_management.apim_internal.resource_group_name lifecycle { #prevent_destroy = true } } resource "azurerm_key_vault_access_policy" "apim_access_policy" { key_vault_id = data.azurerm_key_vault.keyVault.id tenant_id = azurerm_user_assigned_identity.apimIdentity.tenant_id object_id = azurerm_user_assigned_identity.apimIdentity.principal_id secret_permissions = [ "Get", "List" ] certificate_permissions = [ "Get", "List" ] } ================================================ FILE: scenarios/apim-baseline/terraform/modules/apim/outputs.tf ================================================ output "primaryBackendendFqdn" { value = azurerm_api_management.apim_internal.gateway_url } output "bakendUrl" { value = "${azurerm_api_management.apim_internal.name}.azure-api.net" } output "subscriptionKey" { value = random_uuid.starter_key.result } output "apimPrivateIp" { value = azurerm_api_management.apim_internal.private_ip_addresses[0] } output "apimName" { value = azurerm_api_management.apim_internal.name } output "apimIdentityName" { value = azurerm_user_assigned_identity.apimIdentity.name } ================================================ FILE: scenarios/apim-baseline/terraform/modules/apim/variables.tf ================================================ variable "location" { type = string description = "The Azure location in which the deployment is happening" default = "eastus" } variable "resourceSuffix" { type = string description = "A suffix for naming" } variable "environment" { type = string description = "Environment" default = "dev" } variable "resourceGroupName" { type = string description = "The name of the resource group" } #------------------------------- # APIM specific variables #------------------------------- variable "keyVaultName" { description = "The name of the Key Vault" type = string } variable "publisherName" { description = "The name of the publisher/company" type = string default = "Contoso" } variable "publisherEmail" { description = "The email of the publisher/company; shows as administrator email in APIM" type = string default = "apim@contoso.com" } variable "skuName" { description = "String consisting of two parts separated by an underscore(_). The first part is the name, valid values include: Consumption, Developer, Basic, Standard and Premium. The second part is the capacity (e.g. the number of deployed units of the sku), which must be a positive integer (e.g. Developer_1)" type = string default = "Developer_1" } variable "apimSubnetId" { description = "The subnet id of the apim instance" type = string } variable "workspaceId" { type = string description = "The workspace id of the log analytics workspace" } variable "instrumentationKey" { type = string description = "App insights instrumentation key" } variable "sharedResourceGroupName" { type = string description = "The name of the shared resource group" } variable "zoneRedundantEnabled" { description = "Boolean to indicate if the deployment is zone redundant" type = bool default = false } ================================================ FILE: scenarios/apim-baseline/terraform/modules/dns/dnszone.tf ================================================ /* Creates a Private DNS ZOne, A Records and Vnet Link for each of the below endpoints API Gateway contosointernalvnet.azure-api.net Developer portal contosointernalvnet.portal.azure-api.net The new developer portal contosointernalvnet.developer.azure-api.net Direct management endpoint contosointernalvnet.management.azure-api.net Git contosointernalvnet.scm.azure-api.net */ #------------------------------- # DNS zones #------------------------------- resource "azurerm_private_dns_zone" "gateway" { name = "azure-api.net" resource_group_name = var.resourceGroupName lifecycle { #prevent_destroy = true } } resource "azurerm_private_dns_zone" "dev_portal" { name = "portal.azure-api.net" resource_group_name = var.resourceGroupName lifecycle { #prevent_destroy = true } } resource "azurerm_private_dns_zone" "new_dev_portal" { name = "developer.azure-api.net" resource_group_name = var.resourceGroupName lifecycle { #prevent_destroy = true } } resource "azurerm_private_dns_zone" "mgmt_portal" { name = "management.azure-api.net" resource_group_name = var.resourceGroupName lifecycle { #prevent_destroy = true } } resource "azurerm_private_dns_zone" "scm" { name = "scm.azure-api.net" resource_group_name = var.resourceGroupName lifecycle { #prevent_destroy = true } } #------------------------------- # A records for the DNS zones #------------------------------- resource "azurerm_private_dns_a_record" "gateway_record" { name = lower(var.apimName) zone_name = azurerm_private_dns_zone.gateway.name resource_group_name = var.resourceGroupName ttl = 36000 records = [var.apimPrivateIp] lifecycle { #prevent_destroy = true } } resource "azurerm_private_dns_a_record" "dev_portal_record" { name = "portal" zone_name = azurerm_private_dns_zone.dev_portal.name resource_group_name = var.resourceGroupName ttl = 300 records = [var.apimPrivateIp] lifecycle { #prevent_destroy = true } } resource "azurerm_private_dns_a_record" "new_dev_portal_record" { name = "developer" zone_name = azurerm_private_dns_zone.new_dev_portal.name resource_group_name = var.resourceGroupName ttl = 300 records = [var.apimPrivateIp] lifecycle { #prevent_destroy = true } } resource "azurerm_private_dns_a_record" "mgmt_portal_record" { name = "management" zone_name = azurerm_private_dns_zone.mgmt_portal.name resource_group_name = var.resourceGroupName ttl = 300 records = [var.apimPrivateIp] lifecycle { #prevent_destroy = true } } resource "azurerm_private_dns_a_record" "scm_record" { name = "scm" zone_name = azurerm_private_dns_zone.scm.name resource_group_name = var.resourceGroupName ttl = 300 records = [var.apimPrivateIp] lifecycle { #prevent_destroy = true } } #------------------------------- # Vnet links #------------------------------- resource "azurerm_private_dns_zone_virtual_network_link" "gateway_vnetlink" { name = "gateway-vnet-link" resource_group_name = var.resourceGroupName private_dns_zone_name = azurerm_private_dns_zone.gateway.name virtual_network_id = var.apimVnetId lifecycle { #prevent_destroy = true } } resource "azurerm_private_dns_zone_virtual_network_link" "dev_portal_vnetlink" { name = "portal-vnet-link" resource_group_name = var.resourceGroupName private_dns_zone_name = azurerm_private_dns_zone.dev_portal.name virtual_network_id = var.apimVnetId lifecycle { #prevent_destroy = true } } resource "azurerm_private_dns_zone_virtual_network_link" "new_dev_portal_vnetlink" { name = "dev-portal-vnet-link" resource_group_name = var.resourceGroupName private_dns_zone_name = azurerm_private_dns_zone.new_dev_portal.name virtual_network_id = var.apimVnetId lifecycle { #prevent_destroy = true } } resource "azurerm_private_dns_zone_virtual_network_link" "mgmt_vnetlink" { name = "mgmt-vnet-link" resource_group_name = var.resourceGroupName private_dns_zone_name = azurerm_private_dns_zone.mgmt_portal.name virtual_network_id = var.apimVnetId lifecycle { #prevent_destroy = true } } resource "azurerm_private_dns_zone_virtual_network_link" "scm_vnetlink" { name = "scm-vnet-link" resource_group_name = var.resourceGroupName private_dns_zone_name = azurerm_private_dns_zone.scm.name virtual_network_id = var.apimVnetId lifecycle { #prevent_destroy = true } } ================================================ FILE: scenarios/apim-baseline/terraform/modules/dns/variables.tf ================================================ variable "location" { type = string description = "The Azure location in which the deployment is happening" default = "eastus" } variable "resourceSuffix" { type = string description = "A suffix for naming" } variable "environment" { type = string description = "Environment" default = "dev" } variable "resourceGroupName" { type = string description = "The name of the resource group" } variable "apimName" { type = string description = "The name of the API Management instance" } variable "apimPrivateIp" { type = string description = "The private IP address of the API Management instance" } variable "apimVnetId" { type = string } ================================================ FILE: scenarios/apim-baseline/terraform/modules/gateway/certificate/certificate.tf ================================================ ################################################## # Tried creating the certificate using the # # azurerm_key_vault_certificate resource, but it # # doesn't work due to keyvault being private # ################################################## # resource "azurerm_key_vault_certificate" "kv_domain_certs" { # count = local.isLocalCertificate ? 1 : 0 # name = local.secretName # key_vault_id = var.keyvaultId # certificate { # contents = filebase64(var.certificate_path) # password = var.certificate_password # } # certificate_policy { # issuer_parameters { # name = "Self" # } # key_properties { # exportable = true # key_size = 256 # key_type = "EC" # reuse_key = false # curve = "P-256" # } # secret_properties { # content_type = "application/x-pkcs12" # } # } # lifecycle { # #prevent_destroy = true # } # } # resource "azurerm_key_vault_certificate" "local_domain_certs" { # count = !local.isLocalCertificate ? 1 : 0 # name = "generated-cert" # key_vault_id = var.keyvaultId # certificate_policy { # issuer_parameters { # name = "Self" # } # key_properties { # exportable = true # key_size = 2048 # key_type = "RSA" # reuse_key = true # } # lifetime_action { # action { # action_type = "AutoRenew" # } # trigger { # days_before_expiry = 30 # } # } # secret_properties { # content_type = "application/x-pkcs12" # } # x509_certificate_properties { # extended_key_usage = ["1.3.6.1.5.5.7.3.1"] # key_usage = [ # "digitalSignature", # "keyEncipherment" # ] # subject = "CN=${var.appGatewayFqdn}" # validity_in_months = 12 # } # } # lifecycle { # #prevent_destroy = true # } # } ######################################################### # Tried creating the certificate using the # # azurerm_resource_deployment_script_azure_power_shell # # resource, but it doesn't work due to keyvault being # # private. Main issue compared to bicep is the resource # # doesn't have the option to run from a subnet # ######################################################### # resource "azurerm_resource_deployment_script_azure_power_shell" "appGatewayCertificate" { # name = "${local.secretName}-certificate" # resource_group_name = var.sharedResourceGroupName # location = var.location # version = "6.6" # retention_interval = "P1D" # command_line = " -vaultName ${var.keyVaultName} -certificateName ${local.secretName} -subjectName ${local.subjectName} -certPwd ${local.certPwd} -certDataString ${local.certDataString} -certType ${var.appGatewayCertType}" # cleanup_preference = "OnSuccess" # force_update_tag = "1" # timeout = "PT30M" # # container -> doesn't have the property to tell it from which subnet to run # script_content = < doesn't have the property to tell it from which subnet to run # script_content = < Bicep has keyvault as private, should we change this? # -> This will need the certificate to be created through a azurerm_template_deployment resource public_network_access_enabled = false network_acls { bypass = "AzureServices" default_action = "Deny" } } locals { # deployment_client_ids = toset( # concat( # [data.azurerm_client_config.current.object_id], # var.additionalClientIds # ) # ) privateEndpoint_keyvault_Name = "pep-kv-${var.resourceSuffix}" apim_cs_vnet_name = "vnet-apim-cs-${var.resourceSuffix}" networkingResourceGroupName = "rg-networking-${var.resourceSuffix}" private_endpoint_subnet_name = "snet-prep-${var.resourceSuffix}" } # created as a seperate resource, as managed identity uses the azurerm_key_vault_access_policy as well. See note at https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_access_policy resource "azurerm_key_vault_access_policy" "deployment_spn_access_policy" { key_vault_id = azurerm_key_vault.key_vault.id tenant_id = data.azurerm_client_config.current.tenant_id object_id = data.azurerm_client_config.current.object_id key_permissions = [ "Get", ] secret_permissions = [ "Get", ] storage_permissions = [ "Get", ] certificate_permissions = [ "Import", "Get", "List", "Update", "Create" ] } data "azurerm_virtual_network" "apim_cs_vnet" { name = local.apim_cs_vnet_name resource_group_name = local.networkingResourceGroupName } data "azurerm_subnet" "private_endpoint_subnet" { name = local.private_endpoint_subnet_name resource_group_name = local.networkingResourceGroupName virtual_network_name = local.apim_cs_vnet_name } module "keyvault_private_endpoint" { source = "./private_endpoint" name = local.privateEndpoint_keyvault_Name location = var.location resource_group_name = local.networkingResourceGroupName subnet_id = data.azurerm_subnet.private_endpoint_subnet.id private_connection_resource_id = azurerm_key_vault.key_vault.id is_manual_connection = false subresource_name = "vault" private_dns_zone_group_name = "KeyVaultPrivateDnsZoneGroup" private_dns_zone_group_ids = [var.keyVaultPrivateDnsZoneId] } ================================================ FILE: scenarios/apim-baseline/terraform/modules/multi_shared/variables.tf ================================================ variable "location" { type = string description = "The Azure location in which the deployment is happening" default = "eastus" } variable "resourceSuffix" { type = string description = "A suffix for naming" } variable "environment" { type = string description = "Environment" default = "dev" } variable "resourceGroupName" { type = string description = "The name of the resource group" } variable "keyVaultName" { type = string description = "The name of the Key Vault" } variable "keyVaultSku" { type = string description = "The Name of the SKU used for this Key Vault. Possible values are standard and premium" default = "standard" } variable "keyVaultPrivateDnsZoneId" { type = string description = "The ID of the private DNS zone for the Key Vault" } variable "additionalClientIds" { description = "List of additional clients to add to the Key Vault access policy." type = list(string) default = [] } variable "deploymentSubnetId" { type = string } variable "storage_account_name" { type = string } ================================================ FILE: scenarios/apim-baseline/terraform/modules/multi_traffic_manager/outputs.tf ================================================ output "fqdn" { value = azurerm_traffic_manager_profile.tm1.fqdn } ================================================ FILE: scenarios/apim-baseline/terraform/modules/multi_traffic_manager/trafficmanager.tf ================================================ resource "azurerm_traffic_manager_profile" "tm1" { name = var.name resource_group_name = var.resourceGroupName traffic_routing_method = "Performance" dns_config { relative_name = var.name ttl = 60 } monitor_config { expected_status_code_ranges = ["200-299"] path = var.probe_url port = 443 protocol = "HTTPS" } } resource "azurerm_traffic_manager_azure_endpoint" "primaryEndpoint" { name = var.primaryName profile_id = azurerm_traffic_manager_profile.tm1.id always_serve_enabled = false weight = 100 target_resource_id = var.primaryPublicIpId } resource "azurerm_traffic_manager_azure_endpoint" "secondaryEndpoint" { name = var.secondaryName profile_id = azurerm_traffic_manager_profile.tm1.id always_serve_enabled = false weight = 100 target_resource_id = var.secondaryPublicIpId } ================================================ FILE: scenarios/apim-baseline/terraform/modules/multi_traffic_manager/variables.tf ================================================ variable "name" { type = string description = "The traffic manager profile name" } variable "environment" { type = string description = "Environment" default = "dev" } variable "resourceGroupName" { type = string description = "The name of the resource group" } variable "primaryName" { type = string description = "" } variable "primaryPublicIpId" { type = string description = "" } variable "secondaryName" { type = string description = "" } variable "secondaryPublicIpId" { type = string description = "" } variable "probe_url" { type = string description = "" default = "/status-0123456789abcdef" } ================================================ FILE: scenarios/apim-baseline/terraform/modules/networking/networking.tf ================================================ locals { apim_cs_vnet_name = "vnet-apim-cs-${var.resourceSuffix}" appgateway_subnet_name = "snet-apgw-${var.resourceSuffix}" deploy_subnet_name = "snet-deploy-${var.resourceSuffix}" appgateway_snnsg = "nsg-apgw-${var.resourceSuffix}" private_endpoint_subnet_name = "snet-prep-${var.resourceSuffix}" private_endpoint_snnsg = "nsg-prep-${var.resourceSuffix}" apim_subnet_name = "snet-apim-${var.resourceSuffix}" owner = "APIM Const Set" appgateway_public_ipname = "pip-appgw-${var.resourceSuffix}" apim_snnsg = "nsg-apim-${var.resourceSuffix}" } resource "azurerm_network_security_group" "appgateway_nsg" { name = local.appgateway_snnsg location = var.location resource_group_name = var.resourceGroupName security_rule { name = "AllowHealthProbesInbound" priority = 100 protocol = "*" destination_port_range = "65200-65535" access = "Allow" direction = "Inbound" source_port_range = "*" source_address_prefix = "GatewayManager" destination_address_prefix = "*" } security_rule { name = "AllowTLSInbound" priority = 110 protocol = "Tcp" destination_port_range = "443" access = "Allow" direction = "Inbound" source_port_range = "*" source_address_prefix = "*" destination_address_prefix = "*" } security_rule { name = "AllowHTTPInbound" priority = 111 protocol = "Tcp" destination_port_range = "80" access = "Allow" direction = "Inbound" source_port_range = "*" source_address_prefix = "*" destination_address_prefix = "*" } security_rule { name = "AllowAzureLoadBalancerInbound" priority = 121 protocol = "Tcp" destination_port_range = "*" access = "Allow" direction = "Inbound" source_port_range = "*" source_address_prefix = "AzureLoadBalancer" destination_address_prefix = "*" } lifecycle { #prevent_destroy = true } } resource "azurerm_network_security_group" "apim_snnsg_nsg" { name = local.apim_snnsg location = var.location resource_group_name = var.resourceGroupName security_rule { name = "AllowApimVnetInbound" priority = 2000 protocol = "Tcp" destination_port_range = "3443" access = "Allow" direction = "Inbound" source_port_range = "*" source_address_prefix = "ApiManagement" destination_address_prefix = "VirtualNetwork" } security_rule { name = "apim-azure-infra-lb" priority = 2010 protocol = "Tcp" destination_port_range = "6390" access = "Allow" direction = "Inbound" source_port_range = "*" source_address_prefix = "AzureLoadBalancer" destination_address_prefix = "VirtualNetwork" } security_rule { name = "apim-azure-storage" priority = 2000 protocol = "Tcp" destination_port_range = "443" access = "Allow" direction = "Outbound" source_port_range = "*" source_address_prefix = "VirtualNetwork" destination_address_prefix = "Storage" } security_rule { name = "apim-azure-sql" priority = 2010 protocol = "Tcp" destination_port_range = "1443" access = "Allow" direction = "Outbound" source_port_range = "*" source_address_prefix = "VirtualNetwork" destination_address_prefix = "SQL" } security_rule { name = "apim-azure-kv" priority = 2020 protocol = "Tcp" destination_port_range = "443" access = "Allow" direction = "Outbound" source_port_range = "*" source_address_prefix = "VirtualNetwork" destination_address_prefix = "AzureKeyVault" } security_rule { name = "apim-azure-monitor" priority = 2030 protocol = "Tcp" destination_port_range = "443" access = "Allow" direction = "Outbound" source_port_range = "*" source_address_prefix = "VirtualNetwork" destination_address_prefix = "AzureMonitor" } lifecycle { #prevent_destroy = true } } resource "azurerm_network_security_group" "private_endpoint_snnsg_nsg" { name = local.private_endpoint_snnsg location = var.location resource_group_name = var.resourceGroupName lifecycle { #prevent_destroy = true } } resource "azurerm_virtual_network" "apim_cs_vnet" { name = local.apim_cs_vnet_name location = var.location resource_group_name = var.resourceGroupName address_space = [var.apimCSVNetNameAddressPrefix] tags = { Owner = local.owner } lifecycle { #prevent_destroy = true } } resource "azurerm_subnet" "appgateway_subnet" { name = local.appgateway_subnet_name resource_group_name = var.resourceGroupName virtual_network_name = azurerm_virtual_network.apim_cs_vnet.name address_prefixes = [var.appGatewayAddressPrefix] lifecycle { #prevent_destroy = true } } resource "azurerm_subnet_network_security_group_association" "appgateway_subnet" { subnet_id = azurerm_subnet.appgateway_subnet.id network_security_group_id = azurerm_network_security_group.appgateway_nsg.id lifecycle { #prevent_destroy = true } } resource "azurerm_subnet" "private_endpoint_subnet" { name = local.private_endpoint_subnet_name resource_group_name = var.resourceGroupName virtual_network_name = azurerm_virtual_network.apim_cs_vnet.name address_prefixes = [var.privateEndpointAddressPrefix] lifecycle { #prevent_destroy = true } } resource "azurerm_subnet_network_security_group_association" "private_endpoint_subnet" { subnet_id = azurerm_subnet.private_endpoint_subnet.id network_security_group_id = azurerm_network_security_group.private_endpoint_snnsg_nsg.id lifecycle { #prevent_destroy = true } } resource "azurerm_subnet" "deploy_subnet" { name = local.deploy_subnet_name resource_group_name = var.resourceGroupName virtual_network_name = azurerm_virtual_network.apim_cs_vnet.name address_prefixes = [var.deploymentAddressPrefix] service_endpoints = ["Microsoft.Storage"] delegation { name = "Microsoft.ContainerInstance.containerGroups" service_delegation { name = "Microsoft.ContainerInstance/containerGroups" actions = ["Microsoft.Network/virtualNetworks/subnets/action"] } } lifecycle { #prevent_destroy = true } } resource "azurerm_subnet" "apim_subnet" { name = local.apim_subnet_name resource_group_name = var.resourceGroupName virtual_network_name = azurerm_virtual_network.apim_cs_vnet.name address_prefixes = [var.apimAddressPrefix] lifecycle { #prevent_destroy = true } } resource "azurerm_subnet_network_security_group_association" "apim_subnet" { subnet_id = azurerm_subnet.apim_subnet.id network_security_group_id = azurerm_network_security_group.apim_snnsg_nsg.id lifecycle { #prevent_destroy = true } } ================================================ FILE: scenarios/apim-baseline/terraform/modules/networking/outputs.tf ================================================ output "apimSubnetId" { value = azurerm_subnet.apim_subnet.id } output "appGatewaySubnetId" { value = azurerm_subnet.appgateway_subnet.id } output "apimVnetId" { value = azurerm_virtual_network.apim_cs_vnet.id } output "deploymentSubnetId" { value = azurerm_subnet.deploy_subnet.id } ================================================ FILE: scenarios/apim-baseline/terraform/modules/networking/variables.tf ================================================ variable "location" { type = string description = "The Azure location in which the deployment is happening" default = "eastus" } variable "resourceSuffix" { type = string description = "A suffix for naming" } variable "environment" { type = string description = "Environment" default = "dev" } variable "resourceGroupName" { type = string description = "The name of the resource group" } variable "apimCSVNetNameAddressPrefix" { description = "APIM CSV Net Name Address Prefix" type = string } variable "appGatewayAddressPrefix" { description = "App Gateway Address Prefix" type = string } variable "apimAddressPrefix" { description = "APIM Address Prefix" type = string } variable "privateEndpointAddressPrefix" { description = "Private Endpoint Address Prefix" type = string } variable "deploymentAddressPrefix" { description = "Deployment Address Prefix" type = string } ================================================ FILE: scenarios/apim-baseline/terraform/modules/shared/azmon.tf ================================================ #------------------------------- # Creation of log analytics workspace instance #------------------------------- resource "azurerm_log_analytics_workspace" "log_analytics_workspace" { name = "log-${var.resourceSuffix}" location = var.location resource_group_name = var.resourceGroupName sku = "PerGB2018" retention_in_days = 30 lifecycle { #prevent_destroy = true } } #------------------------------- # Creation of an application insight instance #------------------------------- resource "azurerm_application_insights" "shared_apim_insight" { name = "appi-${var.resourceSuffix}" location = var.location resource_group_name = var.resourceGroupName application_type = "web" workspace_id = azurerm_log_analytics_workspace.log_analytics_workspace.id lifecycle { #prevent_destroy = true } } ================================================ FILE: scenarios/apim-baseline/terraform/modules/shared/outputs.tf ================================================ output "workspaceId" { description = "The id of the workspace" value = azurerm_log_analytics_workspace.log_analytics_workspace.id } output "instrumentationKey" { description = "The instrumentation key of the workspace" value = azurerm_application_insights.shared_apim_insight.instrumentation_key } output "keyVaultId" { value = azurerm_key_vault.key_vault.id } output "keyVaultName" { value = azurerm_key_vault.key_vault.name } output "deploymentIdentityName" { value = azurerm_user_assigned_identity.privatedeploymanagedidentity.name } output "deploymentStorageName" { value = azurerm_storage_account.privatedeploystorage.name } ================================================ FILE: scenarios/apim-baseline/terraform/modules/shared/private_dns_zone/outputs.tf ================================================ output "id" { description = "Specifies the resource id of the private dns zone" value = azurerm_private_dns_zone.private_dns_zone.id } ================================================ FILE: scenarios/apim-baseline/terraform/modules/shared/private_dns_zone/privatednszone.tf ================================================ resource "azurerm_private_dns_zone" "private_dns_zone" { name = var.name resource_group_name = var.resource_group_name tags = var.tags lifecycle { ignore_changes = [ tags ] } } resource "azurerm_private_dns_zone_virtual_network_link" "link" { name = "link_to_${lower(basename(var.virtual_networks_to_link_id))}" resource_group_name = var.resource_group_name private_dns_zone_name = azurerm_private_dns_zone.private_dns_zone.name virtual_network_id = var.virtual_networks_to_link_id lifecycle { ignore_changes = [ tags ] } } ================================================ FILE: scenarios/apim-baseline/terraform/modules/shared/private_dns_zone/variables.tf ================================================ variable "name" { description = "(Required) Specifies the name of the private dns zone" type = string } variable "resource_group_name" { description = "(Required) Specifies the resource group name of the private dns zone" type = string } variable "tags" { description = "(Optional) Specifies the tags of the private dns zone" default = {} } variable "virtual_networks_to_link_id" { description = "(Optional) Specifies the virtual networks id to which create a virtual network link" type = string } ================================================ FILE: scenarios/apim-baseline/terraform/modules/shared/private_endpoint/outputs.tf ================================================ output "id" { description = "Specifies the resource id of the private endpoint." value = azurerm_private_endpoint.private_endpoint.id } output "private_dns_zone_group" { description = "Specifies the private dns zone group of the private endpoint." value = azurerm_private_endpoint.private_endpoint.private_dns_zone_group } output "private_dns_zone_configs" { description = "Specifies the private dns zone(s) configuration" value = azurerm_private_endpoint.private_endpoint.private_dns_zone_configs } ================================================ FILE: scenarios/apim-baseline/terraform/modules/shared/private_endpoint/privateendpoint.tf ================================================ resource "azurerm_private_endpoint" "private_endpoint" { name = var.name location = var.location resource_group_name = var.resource_group_name subnet_id = var.subnet_id tags = var.tags private_service_connection { name = "${var.name}Connection" private_connection_resource_id = var.private_connection_resource_id is_manual_connection = var.is_manual_connection subresource_names = try([var.subresource_name], null) request_message = try(var.request_message, null) } private_dns_zone_group { name = var.private_dns_zone_group_name private_dns_zone_ids = var.private_dns_zone_group_ids } lifecycle { ignore_changes = [ tags ] } } ================================================ FILE: scenarios/apim-baseline/terraform/modules/shared/private_endpoint/variables.tf ================================================ variable "name" { description = "(Required) Specifies the name of the private endpoint. Changing this forces a new resource to be created." type = string } variable "resource_group_name" { description = "(Required) The name of the resource group. Changing this forces a new resource to be created." type = string } variable "private_connection_resource_id" { description = "(Required) Specifies the resource id of the private link service" type = string } variable "location" { description = "(Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created." type = string } variable "subnet_id" { description = "(Required) Specifies the resource id of the subnet" type = string } variable "is_manual_connection" { description = "(Optional) Specifies whether the private endpoint connection requires manual approval from the remote resource owner." type = string default = false } variable "subresource_name" { description = "(Optional) Specifies a subresource name which the Private Endpoint is able to connect to." type = string default = null } variable "request_message" { description = "(Optional) Specifies a message passed to the owner of the remote resource when the private endpoint attempts to establish the connection to the remote resource." type = string default = null } variable "private_dns_zone_group_name" { description = "(Required) Specifies the Name of the Private DNS Zone Group. Changing this forces a new private_dns_zone_group resource to be created." type = string } variable "private_dns_zone_group_ids" { description = "(Required) Specifies the list of Private DNS Zones to include within the private_dns_zone_group." type = list(string) } variable "tags" { description = "(Optional) Specifies the tags of the network security group" default = {} } variable "private_dns" { default = {} } ================================================ FILE: scenarios/apim-baseline/terraform/modules/shared/privatedeploy.tf ================================================ resource "azurerm_storage_account" "privatedeploystorage" { name = var.storage_account_name location = var.location resource_group_name = var.resourceGroupName account_tier = "Standard" account_replication_type = "LRS" allow_nested_items_to_be_public = false shared_access_key_enabled = false network_rules { bypass = ["AzureServices"] default_action = "Deny" virtual_network_subnet_ids = [ var.deploymentSubnetId ] } } # Resource: User Assigned Identity resource "azurerm_user_assigned_identity" "privatedeploymanagedidentity" { name = "mi-deploy-${var.resourceSuffix}" location = var.location resource_group_name = var.resourceGroupName } # Resource: Role Assignment resource "azurerm_role_assignment" "privatedeploystorageroleassignment" { scope = azurerm_storage_account.privatedeploystorage.id role_definition_name = "Storage File Data Privileged Contributor" principal_id = azurerm_user_assigned_identity.privatedeploymanagedidentity.principal_id } ================================================ FILE: scenarios/apim-baseline/terraform/modules/shared/shared.tf ================================================ data "azurerm_client_config" "current" {} #------------------------------- # Creation of a key vault instance #------------------------------- resource "azurerm_key_vault" "key_vault" { name = var.keyVaultName location = var.location resource_group_name = var.resourceGroupName tenant_id = data.azurerm_client_config.current.tenant_id sku_name = var.keyVaultSku # -> Bicep has keyvault as private, should we change this? # -> This will need the certificate to be created through a azurerm_template_deployment resource public_network_access_enabled = false network_acls { bypass = "AzureServices" default_action = "Deny" } } locals { # deployment_client_ids = toset( # concat( # [data.azurerm_client_config.current.object_id], # var.additionalClientIds # ) # ) privateEndpoint_keyvault_Name = "pep-kv-${var.resourceSuffix}" apim_cs_vnet_name = "vnet-apim-cs-${var.resourceSuffix}" networkingResourceGroupName = "rg-networking-${var.resourceSuffix}" private_endpoint_subnet_name = "snet-prep-${var.resourceSuffix}" } # created as a seperate resource, as managed identity uses the azurerm_key_vault_access_policy as well. See note at https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_access_policy resource "azurerm_key_vault_access_policy" "deployment_spn_access_policy" { key_vault_id = azurerm_key_vault.key_vault.id tenant_id = data.azurerm_client_config.current.tenant_id object_id = data.azurerm_client_config.current.object_id key_permissions = [ "Get", ] secret_permissions = [ "Get", ] storage_permissions = [ "Get", ] certificate_permissions = [ "Import", "Get", "List", "Update", "Create" ] } data "azurerm_virtual_network" "apim_cs_vnet" { name = local.apim_cs_vnet_name resource_group_name = local.networkingResourceGroupName } data "azurerm_subnet" "private_endpoint_subnet" { name = local.private_endpoint_subnet_name resource_group_name = local.networkingResourceGroupName virtual_network_name = local.apim_cs_vnet_name } module "keyvault_dns_zone" { source = "./private_dns_zone" name = "privatelink.vaultcore.azure.net" resource_group_name = local.networkingResourceGroupName virtual_networks_to_link_id = data.azurerm_virtual_network.apim_cs_vnet.id } module "keyvault_private_endpoint" { source = "./private_endpoint" name = local.privateEndpoint_keyvault_Name location = var.location resource_group_name = local.networkingResourceGroupName subnet_id = data.azurerm_subnet.private_endpoint_subnet.id private_connection_resource_id = azurerm_key_vault.key_vault.id is_manual_connection = false subresource_name = "vault" private_dns_zone_group_name = "KeyVaultPrivateDnsZoneGroup" private_dns_zone_group_ids = [module.keyvault_dns_zone.id] } ================================================ FILE: scenarios/apim-baseline/terraform/modules/shared/variables.tf ================================================ variable "location" { type = string description = "The Azure location in which the deployment is happening" default = "eastus" } variable "resourceSuffix" { type = string description = "A suffix for naming" } variable "environment" { type = string description = "Environment" default = "dev" } variable "resourceGroupName" { type = string description = "The name of the resource group" } variable "keyVaultName" { type = string description = "The name of the Key Vault" } variable "keyVaultSku" { type = string description = "The Name of the SKU used for this Key Vault. Possible values are standard and premium" default = "standard" } variable "additionalClientIds" { description = "List of additional clients to add to the Key Vault access policy." type = list(string) default = [] } variable "deploymentSubnetId" { type = string } variable "storage_account_name" { type = string } ================================================ FILE: scenarios/apim-baseline/terraform/multi-region/multi-region-main.tf ================================================ resource "random_string" "suffix" { length = 5 upper = false special = false } locals { resourceSuffix = "${var.workloadName}-${var.environment}-${var.location}-${var.identifier}" networkingResourceGroupName = "rg-networking-${local.resourceSuffix}" sharedResourceGroupName = "rg-shared-${local.resourceSuffix}" apimResourceGroupName = "rg-apim-${local.resourceSuffix}" keyVaultName = substr(lower(replace("kv-${var.workloadName}${random_string.suffix.result}", "-", "")), 0, 23) storageAccountName = substr(lower(replace("sadep${var.workloadName}${random_string.suffix.result}", "-", "")), 0, 21) # to support multi-region resourceSuffix2nd = "${var.workloadName}-${var.environment}-${var.locationSecond}-${var.identifier}" networkingResourceGroupName2nd = "rg-networking-${local.resourceSuffix2nd}" sharedResourceGroupName2nd = "rg-shared-${local.resourceSuffix2nd}" apimResourceGroupName2nd = "rg-apim-${local.resourceSuffix2nd}" keyVaultName2nd = substr(lower(replace("kv-${var.workloadName}${random_string.suffix.result}-2nd", "-", "")), 0, 23) storageAccountName2nd = substr(lower(replace("sadep2${var.workloadName}${random_string.suffix.result}", "-", "")), 0, 21) tags = {} } # Global Infra module "keyvault_dns_zone_multi" { depends_on = [module.networking] source = "../modules/multi_private_dns_zone" name = "privatelink.vaultcore.azure.net" resource_group_name = azurerm_resource_group.networking.name virtual_networks_to_link_id = module.networking.apimVnetId second_virtual_networks_to_link_id = module.networkingSecond.apimVnetId multiRegionEnabled = var.multiRegionEnabled } # Primary Region resource "azurerm_resource_group" "networking" { name = local.networkingResourceGroupName location = var.location tags = local.tags } resource "azurerm_resource_group" "shared" { name = local.sharedResourceGroupName location = var.location tags = local.tags } resource "azurerm_resource_group" "apim" { name = local.apimResourceGroupName location = var.location tags = local.tags } module "networking" { depends_on = [azurerm_resource_group.networking] source = "../modules/networking" location = var.location resourceGroupName = azurerm_resource_group.networking.name resourceSuffix = local.resourceSuffix environment = var.environment apimAddressPrefix = var.apimAddressPrefix appGatewayAddressPrefix = var.appGatewayAddressPrefix apimCSVNetNameAddressPrefix = var.apimCSVNetNameAddressPrefix privateEndpointAddressPrefix = var.privateEndpointAddressPrefix deploymentAddressPrefix = var.deploymentAddressPrefix } module "shared" { depends_on = [module.networking] source = "../modules/multi_shared" location = var.location resourceGroupName = azurerm_resource_group.shared.name resourceSuffix = local.resourceSuffix additionalClientIds = var.additionalClientIds keyVaultName = local.keyVaultName keyVaultSku = var.keyVaultSku keyVaultPrivateDnsZoneId = module.keyvault_dns_zone_multi.id deploymentSubnetId = module.networking.deploymentSubnetId storage_account_name = local.storageAccountName } module "apim" { depends_on = [module.shared, module.networking] source = "../modules/multi_apim" location = var.location resourceGroupName = azurerm_resource_group.apim.name resourceSuffix = local.resourceSuffix environment = var.environment apimSubnetId = module.networking.apimSubnetId instrumentationKey = module.shared.instrumentationKey workspaceId = module.shared.workspaceId sharedResourceGroupName = azurerm_resource_group.shared.name keyVaultName = local.keyVaultName apimSecondSubnetId = module.networkingSecond.apimSubnetId zoneRedundantEnabled = var.zoneRedundantEnabled locationSecond = var.locationSecond } module "gateway" { depends_on = [module.networking, module.apim, module.shared] source = "../modules/multi_gateway" location = var.location resourceGroupName = azurerm_resource_group.networking.name resourceSuffix = local.resourceSuffix environment = var.environment appGatewayFqdn = var.appGatewayFqdn appGatewayCertType = var.appGatewayCertType certificate_password = var.certificatePassword certificate_path = var.certificatePath subnetId = module.networking.appGatewaySubnetId primaryBackendendFqdn = module.apim.apim_regional_url_1 keyvaultId = module.shared.keyVaultId keyVaultName = module.shared.keyVaultName sharedResourceGroupName = azurerm_resource_group.shared.name deploymentIdentityName = module.shared.deploymentIdentityName deploymentSubnetId = module.networking.deploymentSubnetId deploymentStorageName = module.shared.deploymentStorageName } module "dns" { depends_on = [module.apim, module.gateway] source = "../modules/dns" location = var.location resourceGroupName = azurerm_resource_group.networking.name resourceSuffix = local.resourceSuffix environment = var.environment apimName = module.apim.apimName apimPrivateIp = module.apim.apimPrivateIp apimVnetId = module.networking.apimVnetId } module "dnsRegional" { depends_on = [module.apim, module.gatewaySecond] source = "../modules/multi_apim-dns-regional" location = var.location resourceGroupName = azurerm_resource_group.networking.name resourceSuffix = local.resourceSuffix environment = var.environment apimRegionalName = module.apim.apim_regional_name_1 apimPrivateIp = module.apim.apim_regional_IP_1 apimVnetId = module.networking.apimVnetId apimSecondRegionalName = module.apim.apim_regional_name_2 apimSecondPrivateIp = module.apim.apim_regional_IP_2 apimSecondVnetId = module.networkingSecond.apimVnetId } # Secondary Region resource "azurerm_resource_group" "networkingSecond" { name = "${local.networkingResourceGroupName2nd}" location = var.locationSecond tags = local.tags } resource "azurerm_resource_group" "sharedSecond" { name = "${local.sharedResourceGroupName2nd}" location = var.locationSecond tags = local.tags } resource "azurerm_resource_group" "apimSecond" { name = "${local.apimResourceGroupName2nd}" location = var.locationSecond tags = local.tags } module "networkingSecond" { depends_on = [azurerm_resource_group.networkingSecond] source = "../modules/networking" location = var.locationSecond resourceGroupName = azurerm_resource_group.networkingSecond.name resourceSuffix = local.resourceSuffix2nd environment = var.environment apimAddressPrefix = var.apimSecondAddressPrefix appGatewayAddressPrefix = var.appGatewaySecondAddressPrefix apimCSVNetNameAddressPrefix = var.apimCSVNetNameSecondAddressPrefix privateEndpointAddressPrefix = var.privateEndpointSecondAddressPrefix deploymentAddressPrefix = var.deploymentSecondAddressPrefix } module "sharedSecond" { depends_on = [module.networkingSecond] source = "../modules/multi_shared" location = var.locationSecond resourceGroupName = azurerm_resource_group.sharedSecond.name resourceSuffix = local.resourceSuffix2nd additionalClientIds = var.additionalClientIds keyVaultName = "${local.keyVaultName}-2nd" keyVaultSku = var.keyVaultSku keyVaultPrivateDnsZoneId = module.keyvault_dns_zone_multi.id deploymentSubnetId = module.networkingSecond.deploymentSubnetId storage_account_name = local.storageAccountName2nd } module "gatewaySecond" { depends_on = [module.networkingSecond, module.apim, module.sharedSecond] source = "../modules/multi_gateway" location = var.locationSecond resourceGroupName = azurerm_resource_group.networkingSecond.name resourceSuffix = local.resourceSuffix2nd environment = var.environment appGatewayFqdn = var.appGatewayFqdn appGatewayCertType = var.appGatewayCertType certificate_password = var.certificatePassword certificate_path = var.certificatePath subnetId = module.networkingSecond.appGatewaySubnetId primaryBackendendFqdn = module.apim.apim_regional_url_2 keyvaultId = module.sharedSecond.keyVaultId keyVaultName = module.sharedSecond.keyVaultName sharedResourceGroupName = azurerm_resource_group.sharedSecond.name deploymentIdentityName = module.sharedSecond.deploymentIdentityName deploymentSubnetId = module.networkingSecond.deploymentSubnetId deploymentStorageName = module.sharedSecond.deploymentStorageName } module "trafficmanager" { depends_on = [module.apim, module.gateway, module.gatewaySecond] source = "../modules/multi_traffic_manager" name = replace(var.appGatewayFqdn,".","-") resourceGroupName = azurerm_resource_group.networking.name environment = var.environment primaryName = module.gateway.app_gateway_name primaryPublicIpId = module.gateway.gw_pip_id secondaryName = module.gatewaySecond.app_gateway_name secondaryPublicIpId = module.gatewaySecond.gw_pip_id } ================================================ FILE: scenarios/apim-baseline/terraform/multi-region/variables.tf ================================================ variable "location" { type = string description = "The Azure location in which the deployment is happening" default = "eastus2" } variable "locationSecond" { type = string description = "The Azure location in which the secondary deployment is happening" default = "centralus" } variable "workloadName" { type = string description = "A suffix for naming" default = "apimdemo" } variable "appGatewayFqdn" { type = string description = "The Azure location to deploy to" default = "apim.example.com" } variable "appGatewayCertType" { type = string description = "selfsigned will create a self-signed certificate for the APPGATEWAY_FQDN. custom will use an existing certificate in pfx format that needs to be available in the [certs](../../certs) folder and named appgw.pfx " default = "selfsigned" } variable "environment" { type = string description = "Environment" default = "dev" } variable "keyVaultSku" { type = string description = "The Name of the SKU used for this Key Vault. Possible values are standard and premium" default = "standard" } variable "additionalClientIds" { description = "List of additional clients to add to the Key Vault access policy." type = list(string) default = [] } variable "certificatePassword" { description = "Password for the certificate" type = string sensitive = true default = "" } variable "certificatePath" { description = "Path to the certificate" type = string default = "../../certs/appgw.pfx" } variable "identifier" { description = "The identifier for the resource deployments" type = string } # Primary Region Network variable "apimCSVNetNameAddressPrefix" { description = "APIM CSV Net Name Address Prefix" type = string default = "10.2.0.0/16" } variable "appGatewayAddressPrefix" { description = "App Gateway Address Prefix" type = string default = "10.2.4.0/24" } variable "apimAddressPrefix" { description = "APIM Address Prefix" type = string default = "10.2.7.0/24" } variable "privateEndpointAddressPrefix" { description = "Private Endpoint Address Prefix" type = string default = "10.2.5.0/24" } variable "deploymentAddressPrefix" { description = "Deployment Address Prefix" type = string default = "10.2.8.0/24" } # HA Scenarios Variables # Secondary Region Network variable "apimCSVNetNameSecondAddressPrefix" { description = "APIM CSV Net Name Address Prefix" type = string default = "10.3.0.0/16" } variable "appGatewaySecondAddressPrefix" { description = "App Gateway Address Prefix" type = string default = "10.3.4.0/24" } variable "apimSecondAddressPrefix" { description = "APIM Address Prefix" type = string default = "10.3.7.0/24" } variable "privateEndpointSecondAddressPrefix" { description = "Private Endpoint Address Prefix" type = string default = "10.3.5.0/24" } variable "deploymentSecondAddressPrefix" { description = "Deployment Address Prefix" type = string default = "10.3.8.0/24" } # This will deploy APIM to primary region and extend a location to a secondary region. # This uses the Premium V1 SKU of APIM. variable "multiRegionEnabled" { description = "Boolean to indicate if the deployment is multi-region" type = bool default = true } variable "zoneRedundantEnabled" { description = "Boolean to indicate if the deployment is zone redundant" type = bool default = false } ================================================ FILE: scenarios/apim-baseline/terraform/provider.tf ================================================ terraform { # for storage backends, see backend.tf.sample required_providers { azurerm = { source = "hashicorp/azurerm" version = ">= 3.1" } random = { source = "hashicorp/random" version = "~> 3.6.0" } azapi = { source = "azure/azapi" version = "~> 1.0" } } } # Configure the Microsft Azure provider provider "azurerm" { features { resource_group { prevent_deletion_if_contains_resources = false } } use_oidc = true storage_use_azuread = true subscription_id = var.subscription_id # client_id = var.client_id # client_secret = var.client_secret # tenant_id = var.tenant_id } provider "azapi" { # Configuration options } ================================================ FILE: scenarios/apim-baseline/terraform/single-region/single-region-main.tf ================================================ locals { resourceSuffix = "${var.workloadName}-${var.environment}-${var.location}-${var.identifier}" networkingResourceGroupName = "rg-networking-${local.resourceSuffix}" sharedResourceGroupName = "rg-shared-${local.resourceSuffix}" apimResourceGroupName = "rg-apim-${local.resourceSuffix}" keyVaultName = "kv-${var.workloadName}-${var.environment}-${var.identifier}" tags = {} } resource "azurerm_resource_group" "networking" { name = local.networkingResourceGroupName location = var.location tags = local.tags } resource "azurerm_resource_group" "shared" { name = local.sharedResourceGroupName location = var.location tags = local.tags } resource "azurerm_resource_group" "apim" { name = local.apimResourceGroupName location = var.location tags = local.tags } module "networking" { depends_on = [azurerm_resource_group.networking] source = "../modules/networking" location = var.location resourceGroupName = azurerm_resource_group.networking.name resourceSuffix = local.resourceSuffix environment = var.environment apimAddressPrefix = var.apimAddressPrefix appGatewayAddressPrefix = var.appGatewayAddressPrefix apimCSVNetNameAddressPrefix = var.apimCSVNetNameAddressPrefix privateEndpointAddressPrefix = var.privateEndpointAddressPrefix deploymentAddressPrefix = var.deploymentAddressPrefix } module "shared" { depends_on = [module.networking] source = "../modules/shared" location = var.location resourceGroupName = azurerm_resource_group.shared.name resourceSuffix = local.resourceSuffix additionalClientIds = var.additionalClientIds keyVaultName = local.keyVaultName keyVaultSku = var.keyVaultSku deploymentSubnetId = module.networking.deploymentSubnetId storage_account_name = substr(lower(replace("stdep${local.resourceSuffix}", "-", "")), 0, 21) } module "apim" { depends_on = [module.shared, module.networking] source = "../modules/apim" location = var.location resourceGroupName = azurerm_resource_group.apim.name resourceSuffix = local.resourceSuffix environment = var.environment apimSubnetId = module.networking.apimSubnetId instrumentationKey = module.shared.instrumentationKey workspaceId = module.shared.workspaceId sharedResourceGroupName = azurerm_resource_group.shared.name keyVaultName = local.keyVaultName zoneRedundantEnabled = var.zoneRedundantEnabled } module "gateway" { depends_on = [module.networking, module.apim, module.shared] source = "../modules/gateway" location = var.location resourceGroupName = azurerm_resource_group.networking.name resourceSuffix = local.resourceSuffix environment = var.environment appGatewayFqdn = var.appGatewayFqdn appGatewayCertType = var.appGatewayCertType certificate_password = var.certificatePassword certificate_path = var.certificatePath subnetId = module.networking.appGatewaySubnetId primaryBackendendFqdn = module.apim.bakendUrl keyvaultId = module.shared.keyVaultId keyVaultName = module.shared.keyVaultName sharedResourceGroupName = azurerm_resource_group.shared.name deploymentIdentityName = module.shared.deploymentIdentityName deploymentSubnetId = module.networking.deploymentSubnetId deploymentStorageName = module.shared.deploymentStorageName } module "dns" { depends_on = [module.apim, module.gateway] source = "../modules/dns" location = var.location resourceGroupName = azurerm_resource_group.networking.name resourceSuffix = local.resourceSuffix environment = var.environment apimName = module.apim.apimName apimPrivateIp = module.apim.apimPrivateIp apimVnetId = module.networking.apimVnetId } ================================================ FILE: scenarios/apim-baseline/terraform/single-region/variables.tf ================================================ variable "location" { type = string description = "The Azure location in which the deployment is happening" default = "eastus2" } variable "workloadName" { type = string description = "A suffix for naming" default = "apimdemo" } variable "appGatewayFqdn" { type = string description = "The Azure location to deploy to" default = "apim.example.com" } variable "appGatewayCertType" { type = string description = "selfsigned will create a self-signed certificate for the APPGATEWAY_FQDN. custom will use an existing certificate in pfx format that needs to be available in the [certs](../../certs) folder and named appgw.pfx " default = "selfsigned" } variable "environment" { type = string description = "Environment" default = "dev" } variable "apimCSVNetNameAddressPrefix" { description = "APIM CSV Net Name Address Prefix" type = string default = "10.2.0.0/16" } variable "appGatewayAddressPrefix" { description = "App Gateway Address Prefix" type = string default = "10.2.4.0/24" } variable "apimAddressPrefix" { description = "APIM Address Prefix" type = string default = "10.2.7.0/24" } variable "privateEndpointAddressPrefix" { description = "Private Endpoint Address Prefix" type = string default = "10.2.5.0/24" } variable "deploymentAddressPrefix" { description = "Deployment Address Prefix" type = string default = "10.2.8.0/24" } variable "keyVaultSku" { type = string description = "The Name of the SKU used for this Key Vault. Possible values are standard and premium" default = "standard" } variable "additionalClientIds" { description = "List of additional clients to add to the Key Vault access policy." type = list(string) default = [] } variable "certificatePassword" { description = "Password for the certificate" type = string sensitive = true default = "" } variable "certificatePath" { description = "Path to the certificate" type = string default = "../../certs/appgw.pfx" } variable "identifier" { description = "The identifier for the resource deployments" type = string } variable "zoneRedundantEnabled" { description = "Boolean to indicate if the deployment is zone redundant" type = bool default = false } ================================================ FILE: scenarios/apim-baseline/terraform/variables.tf ================================================ variable "location" { type = string description = "The Azure location in which the deployment is happening" default = "eastus2" } variable "workloadName" { type = string description = "A suffix for naming" default = "apimdemo" } variable "appGatewayFqdn" { type = string description = "The Azure location to deploy to" default = "apim.example.com" } variable "appGatewayCertType" { type = string description = "selfsigned will create a self-signed certificate for the APPGATEWAY_FQDN. custom will use an existing certificate in pfx format that needs to be available in the [certs](../../certs) folder and named appgw.pfx " default = "selfsigned" } variable "environment" { type = string description = "Environment" default = "dev" } variable "keyVaultSku" { type = string description = "The Name of the SKU used for this Key Vault. Possible values are standard and premium" default = "standard" } variable "additionalClientIds" { description = "List of additional clients to add to the Key Vault access policy." type = list(string) default = [] } variable "certificatePassword" { description = "Password for the certificate" type = string sensitive = true default = "" } variable "certificatePath" { description = "Path to the certificate" type = string default = "../../certs/appgw.pfx" } variable "identifier" { description = "The identifier for the resource deployments" type = string } # Primary Region Network variable "apimCSVNetNameAddressPrefix" { description = "APIM CSV Net Name Address Prefix" type = string default = "10.2.0.0/16" } variable "appGatewayAddressPrefix" { description = "App Gateway Address Prefix" type = string default = "10.2.4.0/24" } variable "apimAddressPrefix" { description = "APIM Address Prefix" type = string default = "10.2.7.0/24" } variable "privateEndpointAddressPrefix" { description = "Private Endpoint Address Prefix" type = string default = "10.2.5.0/24" } variable "deploymentAddressPrefix" { description = "Deployment Address Prefix" type = string default = "10.2.8.0/24" } # HA Scenarios Variables # This will deploy APIM to primary region and extend a location to a secondary region. # This uses the Premium V1 SKU of APIM. variable "multiRegionEnabled" { description = "Boolean to indicate if the deployment is multi-region" type = bool default = false } variable "zoneRedundantEnabled" { description = "Boolean to indicate if the deployment is zone redundant" type = bool default = false } variable "locationSecond" { type = string description = "The Azure location in which the secondary deployment is happening" default = "centralus" } # Secondary Region Network variable "apimCSVNetNameSecondAddressPrefix" { description = "APIM CSV Net Name Address Prefix" type = string default = "10.3.0.0/16" } variable "appGatewaySecondAddressPrefix" { description = "App Gateway Address Prefix" type = string default = "10.3.4.0/24" } variable "apimSecondAddressPrefix" { description = "APIM Address Prefix" type = string default = "10.3.7.0/24" } variable "privateEndpointSecondAddressPrefix" { description = "Private Endpoint Address Prefix" type = string default = "10.3.5.0/24" } variable "deploymentSecondAddressPrefix" { description = "Deployment Address Prefix" type = string default = "10.3.8.0/24" } variable "subscription_id" { type = string description = "The Azure subscription ID to deploy to" } # To avoid Terraform missing variable warnings variable "certData" { default="" } variable "certKey" { default="" } variable "enableTelemetry" { default="" } ================================================ FILE: scenarios/certs/place-custom-cert-here ================================================ ================================================ FILE: scenarios/scripts/bicep/deploy-apim-baseline.sh ================================================ #!/bin/bash set -e script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" if [[ -f "$script_dir/../../.env" ]]; then echo "Loading .env" source "$script_dir/../../.env" fi if [[ ${#AZURE_LOCATION} -eq 0 ]]; then echo 'ERROR: Missing environment variable AZURE_LOCATION' 1>&2 exit 6 else AZURE_LOCATION="${AZURE_LOCATION%$'\r'}" fi if [[ ${#RESOURCE_NAME_PREFIX} -eq 0 ]]; then echo 'ERROR: Missing environment variable RESOURCE_NAME_PREFIX' 1>&2 exit 6 else RESOURCE_NAME_PREFIX="${RESOURCE_NAME_PREFIX%$'\r'}" fi if [[ ${#ENVIRONMENT_TAG} -eq 0 ]]; then echo 'ERROR: Missing environment variable ENVIRONMENT_TAG' 1>&2 exit 6 else ENVIRONMENT_TAG="${ENVIRONMENT_TAG%$'\r'}" fi if [[ ${#APPGATEWAY_FQDN} -eq 0 ]]; then echo 'ERROR: Missing environment variable APPGATEWAY_FQDN' 1>&2 exit 6 else APPGATEWAY_FQDN="${APPGATEWAY_FQDN%$'\r'}" fi if [[ ${#CERT_TYPE} -eq 0 ]]; then echo 'ERROR: Missing environment variable CERT_TYPE' 1>&2 exit 6 else CERT_TYPE="${CERT_TYPE%$'\r'}" fi if [[ ${#ENABLE_TELEMETRY} -eq 0 ]]; then telemetry=true fi if [[ "$CERT_TYPE" == "selfsigned" ]]; then cert_data='' cert_Pwd='' else cert_data=$(base64 -w 0 "$script_dir/../../certs/appgw.pfx") cert_pwd=$(CERT_PWD) fi if [[ ${#RANDOM_IDENTIFIER} -eq 0 ]]; then chars="abcdefghijklmnopqrstuvwxyz" random_string="" for i in {1..3}; do random_char="${chars:RANDOM%${#chars}:1}" random_string+="$random_char" done echo "RANDOM_IDENTIFIER='$random_string'" >> "$script_dir/../../.env" else random_string="${RANDOM_IDENTIFIER}" fi cat << EOF > "$script_dir/../../apim-baseline/bicep/parameters.json" { "\$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { "workloadName" :{ "value": "${RESOURCE_NAME_PREFIX}" }, "environment" :{ "value": "${ENVIRONMENT_TAG}" }, "identifier" :{ "value": "${random_string}" }, "appGatewayFqdn" :{ "value": "${APPGATEWAY_FQDN}" }, "appGatewayCertType" :{ "value": "${CERT_TYPE}" }, "certData" :{ "value": "${cert_data}" }, "certKey" :{ "value": "${cert_pwd}" }, "enableTelemetry" :{ "value": ${telemetry} } } } EOF deployment_name="apim-baseline-${RESOURCE_NAME_PREFIX}" cd "$script_dir/../../apim-baseline/bicep/" echo "==" echo "== Starting bicep deployment ${deployment_name}" echo "==" output=$(az deployment sub create \ --template-file main.bicep \ --name "$deployment_name" \ --parameters parameters.json \ --location "$AZURE_LOCATION" \ --output json) echo "== Completed bicep deployment ${deployment_name}" echo "$output" | jq "[.properties.outputs | to_entries | .[] | {key:.key, value: .value.value}] | from_entries" > "$script_dir/../../apim-baseline/bicep/output.json" appGatewayPublicIpAddress=$(cat "$script_dir/../../apim-baseline/bicep/output.json" | jq -r '.appGatewayPublicIpAddress') apimStarterSubscriptionKey=$(cat "$script_dir/../../apim-baseline/bicep/output.json" | jq -r '.apimStarterSubscriptionKey') testUri="curl -k -v -H 'Host: ${APPGATEWAY_FQDN}' -H 'Ocp-Apim-Subscription-Key: ${apimStarterSubscriptionKey}' https://${appGatewayPublicIpAddress}/echo/resource?param1=sample" echo "Test the deployment by running the following command: ${testUri}" echo -e "\n" ================================================ FILE: scenarios/scripts/bicep/deploy-workload-function.sh ================================================ #!/bin/bash set -e script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" if [[ -f "$script_dir/../../.env" ]]; then echo "Loading .env" source "$script_dir/../../.env" fi if [[ -f "$script_dir/../../apim-baseline/bicep/output.json" ]]; then echo "Loading baseline configuration" while IFS='=' read -r key value; do export "$key=${value//\"/}" done < <(jq -r 'to_entries|map("\(.key)=\(.value|tostring)")|.[]' "$script_dir/../../apim-baseline/bicep/output.json") else echo "ERROR: Missing baseline configuration. Run deploy-apim-baseline.sh" 1>&2 exit 6 fi if [[ ${#AZURE_LOCATION} -eq 0 ]]; then echo 'ERROR: Missing environment variable AZURE_LOCATION' 1>&2 exit 6 else AZURE_LOCATION="${AZURE_LOCATION%$'\r'}" fi if [[ ${#RESOURCE_NAME_PREFIX} -eq 0 ]]; then echo 'ERROR: Missing environment variable RESOURCE_NAME_PREFIX' 1>&2 exit 6 else RESOURCE_NAME_PREFIX="${RESOURCE_NAME_PREFIX%$'\r'}" fi if [[ ${#ENVIRONMENT_TAG} -eq 0 ]]; then echo 'ERROR: Missing environment variable ENVIRONMENT_TAG' 1>&2 exit 6 else ENVIRONMENT_TAG="${ENVIRONMENT_TAG%$'\r'}" fi if [[ ${#ENABLE_TELEMETRY} -eq 0 ]]; then telemetry=true fi cat << EOF > "$script_dir/../../workload-functions/bicep/parameters.json" { "\$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { "resourceSuffix" :{ "value": "${resourceSuffix}" }, "networkingResourceGroupName" :{ "value": "${networkingResourceGroupName}" }, "apimResourceGroupName" :{ "value": "${apimResourceGroupName}" }, "apimName" :{ "value": "${apimName}" }, "vnetName" :{ "value": "${vnetName}" }, "deploymentIdentityName" :{ "value": "${deploymentIdentityName}" }, "deploymentSubnetId" :{ "value": "${deploymentSubnetId}" }, "deploymentStorageName" :{ "value": "${deploymentStorageName}" }, "privateEndpointSubnetid" :{ "value": "${privateEndpointSubnetid}" }, "sharedResourceGroupName" :{ "value": "${sharedResourceGroupName}" }, "enableTelemetry" :{ "value": ${telemetry} } } } EOF deployment_name="workload-functions-${RESOURCE_NAME_PREFIX}" echo "$deployment_name" cd "$script_dir/../../workload-functions/bicep/" echo "==" echo "== Starting bicep deployment ${deployment_name}" echo "==" output=$(az deployment sub create \ --template-file main.bicep \ --name "$deployment_name" \ --parameters parameters.json \ --location "$AZURE_LOCATION" \ --output json) echo "== Completed bicep deployment ${deployment_name}" echo "$output" | jq "[.properties.outputs | to_entries | .[] | {key:.key, value: .value.value}] | from_entries" > "$script_dir/../../workload-functions/bicep/output.json" APPGATEWAY_FQDN="${APPGATEWAY_FQDN%$'\r'}" testUri="curl -k -H 'Host: ${APPGATEWAY_FQDN}' -H 'Ocp-Apim-Subscription-Key: ${apimStarterSubscriptionKey}' https://${appGatewayPublicIpAddress}/hello?name=world" echo "Test the deployment by running the following command: ${testUri}" echo -e "\n" ================================================ FILE: scenarios/scripts/bicep/deploy-workload-genai.sh ================================================ #!/bin/bash set -e script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" if [[ -f "$script_dir/../../.env" ]]; then echo "Loading .env" source "$script_dir/../../.env" fi if [[ ${#ENABLE_TELEMETRY} -eq 0 ]]; then telemetry=true fi if [[ -f "$script_dir/../../apim-baseline/bicep/output.json" ]]; then echo "Loading baseline configuration" while IFS='=' read -r key value; do export "$key=${value//\"/}" done < <(jq -r 'to_entries|map("\(.key)=\(.value|tostring)")|.[]' "$script_dir/../../apim-baseline/bicep/output.json") else echo "ERROR: Missing baseline configuration. Run deploy-apim-baseline.sh" 1>&2 exit 6 fi cat << EOF > "$script_dir/../../workload-genai/bicep/parameters.json" { "\$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { "apiManagementServiceName" :{ "value": "${apimName}" }, "resourceSuffix" :{ "value": "${resourceSuffix}" }, "apimResourceGroupName" :{ "value": "${apimResourceGroupName}" }, "apimIdentityName" :{ "value": "${apimIdentityName}" }, "vnetName" :{ "value": "${vnetName}" }, "privateEndpointSubnetid" :{ "value": "${privateEndpointSubnetid}" }, "networkingResourceGroupName" :{ "value": "${networkingResourceGroupName}" }, "enableTelemetry" :{ "value": ${telemetry} } } } EOF deployment_name="workload-genai-${RESOURCE_NAME_PREFIX}" echo "$deployment_name" cd "$script_dir/../../workload-genai/bicep/" echo "==" echo "== Starting bicep deployment ${deployment_name}" echo "==" output=$(az deployment sub create \ --template-file main.bicep \ --name "$deployment_name" \ --parameters parameters.json \ --location "$AZURE_LOCATION" \ --output json) echo "== Completed bicep deployment ${deployment_name}" echo "$output" | jq "[.properties.outputs | to_entries | .[] | {key:.key, value: .value.value}] | from_entries" > "$script_dir/../../workload-genai/bicep/output.json" apimSubscriptionKey=$(cat "$script_dir/../../workload-genai/bicep/output.json" | jq -r '.apiManagementAzureOpenAIProductSubscriptionKey') multiTenantProduct1SubscriptionKey=$(cat "$script_dir/../../workload-genai/bicep/output.json" | jq -r '.apiManagementMultitenantProduct1SubscriptionKey') multiTenantProduct2SubscriptionKey=$(cat "$script_dir/../../workload-genai/bicep/output.json" | jq -r '.apiManagementMultitenantProduct2SubscriptionKey') testUri="curl -k -H 'Host: ${APPGATEWAY_FQDN}' -H 'Ocp-Apim-Subscription-Key: ${apimSubscriptionKey}' -H 'Content-Type: application/json' https://${appGatewayPublicIpAddress}/openai/deployments/aoai/chat/completions?api-version=2024-02-15-preview -d '{\"messages\": [{\"role\":\"system\",\"content\":\"You are an AI assistant that helps people find information.\"}]}'" echo "Test the deployment by running the following command: ${testUri}" echo -e "\n" multiTenantProduct1TestUri="curl -k -H 'Host: ${APPGATEWAY_FQDN}' -H 'Ocp-Apim-Subscription-Key: ${multiTenantProduct1SubscriptionKey}' -H 'Content-Type: application/json' https://${appGatewayPublicIpAddress}/openai/deployments/aoai/chat/completions?api-version=2024-02-15-preview -d '{\"messages\": [{\"role\":\"system\",\"content\":\"You are an AI assistant that helps people find information.\"}]}'" echo "Test the deployment for multi-tenant Product1 by running the following command: ${multiTenantProduct1TestUri}" echo -e "\n" multiTenantProduct2TestUri="curl -k -H 'Host: ${APPGATEWAY_FQDN}' -H 'Ocp-Apim-Subscription-Key: ${multiTenantProduct2SubscriptionKey}' -H 'Content-Type: application/json' https://${appGatewayPublicIpAddress}/openai/deployments/aoai/chat/completions?api-version=2024-02-15-preview -d '{\"messages\": [{\"role\":\"system\",\"content\":\"You are an AI assistant that helps people find information.\"}]}'" echo "Test the deployment for multi-tenant Product2 by running the following command: ${multiTenantProduct2TestUri}" echo -e "\n" ================================================ FILE: scenarios/scripts/terraform/__destroy-apim-baseline.sh ================================================ # validate if wants to proceed source "./.env" echo "Using TFVARS: ../../apim-baseline/terraform/${ENVIRONMENT_TAG}.tfvars" echo "Do you want to destroy the deployment? (y/n)" read -r response if [[ $response =~ ^[Yy]$ ]]; then cd ../../apim-baseline/terraform terraform destroy --auto-approve -var-file="${ENVIRONMENT_TAG}.tfvars" else echo "Exiting..." fi ================================================ FILE: scenarios/scripts/terraform/azure-backend-sample.sh ================================================ #!/bin/bash set -e # This script sets up the backend configuration for Terraform in Azure. # It requires the user to be logged into Azure CLI and verifies the current subscription. # The script accepts the following parameters: # --resource-group or -g: The Azure resource group name for the Terraform backend. # --storage-account or -s: The Azure storage account name for the Terraform backend. # --container or -c: The Azure storage container name for the Terraform backend. # --auto-confirm or -y: Automatically confirm prompts without user interaction. # Example execution: # ./azure-backend-sample.sh --resource-group my-resource-group --storage-account my-storage-account --container my-container --auto-confirm # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in --resource-group|-g) TF_BACKEND_RESOURCE_GROUP_NAME=$2; shift 2 ;; --storage-account|-s) TF_BACKEND_STORAGE_ACCOUNT_NAME=$2; shift 2 ;; --container|-c) TF_BACKEND_CONTAINER_NAME=$2; shift 2 ;; --auto-confirm|-y) auto_confirm=true; shift ;; *) echo "Invalid argument: $1"; exit 1 ;; esac done # Validate required arguments if [[ -z "$TF_BACKEND_RESOURCE_GROUP_NAME" || -z "$TF_BACKEND_STORAGE_ACCOUNT_NAME" || -z "$TF_BACKEND_CONTAINER_NAME" ]]; then echo "Error: --resource-group, --storage-account, and --container are required arguments." exit 1 fi # Source .env to get Azure location script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" env_file="./.env" if [[ -f "$env_file" ]]; then echo "Found .env, sourcing it..." cat "$env_file" source "$env_file" else echo "###########################" echo "Error: .env file not found in the current directory." echo "###########################" fi # Validate if needed environment variables are set if [[ -z "$AZURE_LOCATION" ]]; then echo "Error: AZURE_LOCATION is not set in the .env file." exit 1 fi if [[ -z "$ENVIRONMENT_TAG" ]]; then echo "Error: ENVIRONMENT_TAG is not set in the .env file." exit 1 fi # Ensure user is logged into Azure CLI if ! az account show > /dev/null 2>&1; then echo "Please log in to Azure CLI using 'az login'."; exit 1 fi # Display the current subscription az account show --query "{subscriptionId:id, subscriptionName:name}" --output table # Confirm to proceed if not auto-confirmed if [[ $auto_confirm != true ]]; then read -p "Do you want to continue? (y/n): " response [[ $response =~ ^[Yy]$ ]] || { echo "Exiting..."; exit 1; } fi # Function to create resource group if it doesn't exist create_resource_group() { echo "Creating resource group $TF_BACKEND_RESOURCE_GROUP_NAME..." az group create --name "$TF_BACKEND_RESOURCE_GROUP_NAME" --location "$AZURE_LOCATION" > /dev/null } # Function to create storage account if it doesn't exist create_storage_account() { echo "Creating storage account and container $TF_BACKEND_STORAGE_ACCOUNT_NAME..." az storage account create --name "$TF_BACKEND_STORAGE_ACCOUNT_NAME" --resource-group "$TF_BACKEND_RESOURCE_GROUP_NAME" --location "$AZURE_LOCATION" --sku Standard_LRS > /dev/null az storage container create --name "$TF_BACKEND_CONTAINER_NAME" --account-name "$TF_BACKEND_STORAGE_ACCOUNT_NAME" > /dev/null } # Validate or create resource group if [[ $(az group exists --name "$TF_BACKEND_RESOURCE_GROUP_NAME" --output tsv) == "false" ]]; then if [[ $auto_confirm == true ]]; then create_resource_group else read -p "Resource group not found. Create it? (y/n): " response [[ $response =~ ^[Yy]$ ]] && create_resource_group || { echo "Exiting..."; exit 1; } fi fi # Validate or create storage account if ! az storage account show --name "$TF_BACKEND_STORAGE_ACCOUNT_NAME" --resource-group "$TF_BACKEND_RESOURCE_GROUP_NAME" > /dev/null 2>&1; then if [[ $auto_confirm == true ]]; then create_storage_account else read -p "Storage account not found. Create it? (y/n): " response [[ $response =~ ^[Yy]$ ]] && create_storage_account || { echo "Exiting..."; exit 1; } fi fi # Validate or create container if ! az storage container show --name "$TF_BACKEND_CONTAINER_NAME" --account-name "$TF_BACKEND_STORAGE_ACCOUNT_NAME" > /dev/null 2>&1; then if [[ $auto_confirm == true ]]; then az storage container create --name "$TF_BACKEND_CONTAINER_NAME" --account-name "$TF_BACKEND_STORAGE_ACCOUNT_NAME" > /dev/null else read -p "Container not found. Create it? (y/n): " response [[ $response =~ ^[Yy]$ ]] && az storage container create --name "$TF_BACKEND_CONTAINER_NAME" --account-name "$TF_BACKEND_STORAGE_ACCOUNT_NAME" > /dev/null || { echo "Exiting..."; exit 1; } fi fi echo "Backend resources are ready." # Create needed backend.hcl file backend_hcl_file="./${ENVIRONMENT_TAG}-backend.hcl" cat < "$backend_hcl_file" resource_group_name = "$TF_BACKEND_RESOURCE_GROUP_NAME" storage_account_name = "$TF_BACKEND_STORAGE_ACCOUNT_NAME" container_name = "$TF_BACKEND_CONTAINER_NAME" EOF echo "Backend configuration written to $backend_hcl_file." backend_tf_file="../../apim-baseline/terraform/${ENVIRONMENT_TAG}-backend.tf" cat < "$backend_tf_file" terraform { backend "azurerm" { resource_group_name = "$TF_BACKEND_RESOURCE_GROUP_NAME" storage_account_name = "$TF_BACKEND_STORAGE_ACCOUNT_NAME" container_name = "$TF_BACKEND_CONTAINER_NAME" } } EOF echo "Terraform backend configuration written to $backend_tf_file." ================================================ FILE: scenarios/scripts/terraform/deploy-apim-baseline.sh ================================================ #!/bin/bash set -e script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" env_file="./.env" while [[ $# -gt 0 ]]; do case "$1" in --auto-confirm|-y) auto_confirm=true; shift 2 ;; --delete-local-state|-d) delete_local_state=true; shift ;; --validate-commit|-t) validate_commit=true; shift ;; *) echo "Invalid argument: $1"; exit 1 ;; esac done # show a message if auto confirm or delete local state is set if [[ $auto_confirm == true ]]; then echo "Auto-confirmation enabled, proceeding without prompts." fi if [[ $delete_local_state == true ]]; then echo "Delete local state enabled, local Terraform state files will be removed." fi if [[ -f "$env_file" ]]; then echo "Found .env, sourcing it..." cat "$env_file" source "$env_file" else echo "###########################" echo "Error: .env file not found in the current directory." echo " a sample is available at ./sample.env" echo "###########################" exit 1 fi if [[ ${#RANDOM_IDENTIFIER} -eq 0 ]]; then chars="abcdefghijklmnopqrstuvwxyz" random_string="" for _ in {1..3}; do random_char="${chars:RANDOM%${#chars}:1}" random_string+="$random_char" done echo -e "\nRANDOM_IDENTIFIER='$random_string'" >> "$env_file" else random_string="${RANDOM_IDENTIFIER}" fi #### VALIDATE VARIABLES: # params if [[ ${#AZURE_LOCATION} -eq 0 ]]; then echo 'ERROR: Missing environment variable AZURE_LOCATION' 1>&2 exit 6 else AZURE_LOCATION="${AZURE_LOCATION%$'\r'}" fi if [[ ${#RESOURCE_NAME_PREFIX} -eq 0 ]]; then echo 'ERROR: Missing environment variable RESOURCE_NAME_PREFIX' 1>&2 exit 6 else RESOURCE_NAME_PREFIX="${RESOURCE_NAME_PREFIX%$'\r'}" fi if [[ ${#ENVIRONMENT_TAG} -eq 0 ]]; then echo 'ERROR: Missing environment variable ENVIRONMENT_TAG' 1>&2 exit 6 else ENVIRONMENT_TAG="${ENVIRONMENT_TAG%$'\r'}" fi if [[ ${#APPGATEWAY_FQDN} -eq 0 ]]; then echo 'ERROR: Missing environment variable APPGATEWAY_FQDN' 1>&2 exit 6 else APPGATEWAY_FQDN="${APPGATEWAY_FQDN%$'\r'}" fi if [[ ${#CERT_TYPE} -eq 0 ]]; then echo 'ERROR: Missing environment variable CERT_TYPE' 1>&2 exit 6 else CERT_TYPE="${CERT_TYPE%$'\r'}" fi if [[ ${#ENABLE_TELEMETRY} -eq 0 ]]; then telemetry=true fi if [[ "$CERT_TYPE" == "selfsigned" ]]; then #cert_data='' #cert_Pwd='' cat << EOF > "$script_dir/tmp-self-signed-cert.conf" [ req ] default_bits = 4096 distinguished_name = req_distinguished_name req_extensions = req_ext x509_extensions = v3_req prompt = no [ req_distinguished_name ] CN = ${APPGATEWAY_FQDN} [ req_ext ] subjectAltName = @alt_names [ v3_req ] subjectAltName = @alt_names [ alt_names ] DNS.1 = ${APPGATEWAY_FQDN} EOF openssl req -x509 -nodes -days 365 -newkey rsa:4096 \ -keyout "$script_dir/apim-self-signed.key" \ -out "$script_dir/apim-self-signed.crt" \ -config "$script_dir/tmp-self-signed-cert.conf" cert_pwd=$(tr -dc 'A-Za-z0-9!@#$%^&*()_+{}|:<>?' < /dev/urandom | head -c 16) cert_file="$script_dir/../../certs/self-signed-cert.pfx" openssl pkcs12 -export \ -out "$cert_file" \ -inkey "$script_dir/apim-self-signed.key" \ -in "$script_dir/apim-self-signed.crt" \ -passout pass:"$cert_pwd" else cert_file="$script_dir/../../certs/appgw.pfx" cert_pwd=$(CERT_PWD) fi ### MULTI REGION AND ZONE REDUNDANT UPDATES if [[ "$MULTI_REGION" == "true" ]]; then echo "Multi Region is enabled, checking for AZURE_LOCATION2..." if [[ ${#AZURE_LOCATION2} -eq 0 ]]; then echo 'ERROR: Multi Region was set to true, however environment variable AZURE_LOCATION2 is missing' 1>&2 exit 6 else AZURE_LOCATION2="${AZURE_LOCATION2%$'\r'}" MULTI_REGION="${MULTI_REGION%$'\r'}" echo "Multi Region is enabled, using AZURE_LOCATION2: ${AZURE_LOCATION2}" fi else echo "Multi Region is not enabled, AZURE_LOCATION2 will not be used." MULTI_REGION="${MULTI_REGION%$'\r'}" AZURE_LOCATION2="" fi if [[ ${#ZONE_REDUNDANT} -eq 0 ]]; then # Assume false if not set ZONE_REDUNDANT="false" else ZONE_REDUNDANT="${ZONE_REDUNDANT%$'\r'}" fi if [[ $validate_commit == true ]]; then echo "Performing commit validation checks ..." echo "Setting up ficticious subscription_id" SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000" else ### VALIDATE IF AZ LOGIN IS REQUIRED, SHOW THE SUBSCRIPTION AND CONFIRM IF WANT TO CONTINUE az account show > /dev/null if [ $? -ne 0 ]; then echo "You need to login to Azure CLI. Run 'az login' and try again." exit 6 fi echo -e "\n" echo "Currently selected subscription:" az account show --query "{subscriptionId:id, subscriptionName:name}" --output table echo -e "\n" echo "If you want to change the subscription, run 'az account set --subscription '" echo -e "\n" if [[ $auto_confirm == true ]]; then echo "auto-confirmation enabled ... continuing" else echo "Do you want to continue? (y/n)" read -r response if [[ ! $response =~ ^[Yy]$ ]]; then echo "Exiting..." exit 6 fi fi # Get the current subscription ID SUBSCRIPTION_ID=$(az account show --query id -o tsv) fi # creating tfvars # create tfvars echo "Creating terraform variables file..." cat << EOF > "$script_dir/../../apim-baseline/terraform/${ENVIRONMENT_TAG}.tfvars" location = "${AZURE_LOCATION}" workloadName = "${RESOURCE_NAME_PREFIX}" environment = "${ENVIRONMENT_TAG}" identifier = "${random_string}" appGatewayFqdn = "${APPGATEWAY_FQDN}" appGatewayCertType = "${CERT_TYPE}" certificatePath = "${cert_file}" certificatePassword = "${cert_pwd}" enableTelemetry = "${telemetry}" multiRegionEnabled = "${MULTI_REGION}" zoneRedundantEnabled = "${ZONE_REDUNDANT}" locationSecond = "${AZURE_LOCATION2}" subscription_id = "${SUBSCRIPTION_ID}" EOF cat "$script_dir/../../apim-baseline/terraform/${ENVIRONMENT_TAG}.tfvars" #### Init Terraform with Backend or local storage based on presence of backend.hcl file backend_hcl_file="./${ENVIRONMENT_TAG}-backend.hcl" if [[ -f "$backend_hcl_file" ]]; then echo "Found existing backend file, using it..." echo "Copying backend file to terraform directory..." cp "$backend_hcl_file" "../../apim-baseline/terraform/${ENVIRONMENT_TAG}-backend.hcl" cat "../../apim-baseline/terraform/${ENVIRONMENT_TAG}-backend.hcl" echo "Initializing Terraform backend..." cd "../../apim-baseline/terraform" || exit echo "==" echo "== Starting terraform deployment baseline" echo "==" # Delete local state files echo "== deleting local state files" rm -rf .terraform rm -f terraform.lock.hcl rm -f terraform.tfstate rm -f terraform.tfstate.backup terraform init \ -backend-config="${ENVIRONMENT_TAG}-backend.hcl" \ -backend-config="key=${ENVIRONMENT_TAG}-baseline-lza.tfstate" else echo "Initializing Terraform with local backend..." cd "../../apim-baseline/terraform" || exit terraform init -backend=false fi # Check if there is an existing local state file if [[ -f "${ENVIRONMENT_TAG}.tfstate" ]]; then echo -n "Found existing local state files..." if [[ "$delete_local_state" == "true" ]]; then echo "Deleting local Terraform state files..." rm -f "${ENVIRONMENT_TAG}.tfplan" rm -f "${ENVIRONMENT_TAG}.tfvars" rm -f "${ENVIRONMENT_TAG}-backend.hcl" else echo "and reusing it. Use --delete-local-state to remove it." fi fi # If the validate commit flag is set, run terraform validate and exit if [[ $validate_commit == true ]]; then echo "Running Terraform validate and quitting ..." terraform validate if [[ $? -ne 0 ]]; then echo "Terraform validation failed." exit 7 fi echo "Terraform validation succeeded." exit 0 fi ### Create the Terraform plan echo "Creating Terraform plan..." terraform plan -var-file="${ENVIRONMENT_TAG}.tfvars" -out="${ENVIRONMENT_TAG}.tfplan" echo "Terraform plan created" # validate if wants to proceed if [[ $auto_confirm == true ]]; then echo "auto-confirmation enabled ... continuing" response="y" else echo "Do you want to create it? (y/n)" read -r response fi if [[ $response =~ ^[Yy]$ ]]; then echo "Applying Terraform plan..." terraform apply "${ENVIRONMENT_TAG}.tfplan" else echo "Exiting..." exit 6 fi echo "== Completed terraform deployment" # Testing the deployment echo "Validating deployment..." APIM_SERVICE_NAME="apim-${RESOURCE_NAME_PREFIX}-${ENVIRONMENT_TAG}-${AZURE_LOCATION}-${RANDOM_IDENTIFIER}" APIM_RESOURCE_GROUP="rg-apim-${RESOURCE_NAME_PREFIX}-${ENVIRONMENT_TAG}-${AZURE_LOCATION}-${RANDOM_IDENTIFIER}" NETWORK_RESOURCE_GROUP="rg-networking-${RESOURCE_NAME_PREFIX}-${ENVIRONMENT_TAG}-${AZURE_LOCATION}-${RANDOM_IDENTIFIER}" APPGATEWAY_PIP="pip-appgw-${RESOURCE_NAME_PREFIX}-${ENVIRONMENT_TAG}-${AZURE_LOCATION}-${RANDOM_IDENTIFIER}" SUBSCRIPTION_ID=$(az account show --query id -o tsv) API_SUBSCRIPTION_NAME="Echo API" # Get the access token echo "Obtaining Access Token..." TOKEN=$(az account get-access-token --query accessToken --output tsv) # get the subscription id based on the subscription display name echo "Getting API Management Subscription info ... [1/3]" API_MANAGEMENT_INFO=$(curl -s -S -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$APIM_RESOURCE_GROUP/providers/Microsoft.ApiManagement/service/$APIM_SERVICE_NAME/subscriptions?api-version=2022-08-01") echo "Getting API Management Subscription info ... [2/3]" API_SUBSCRIPTION_ID=$(echo $API_MANAGEMENT_INFO | jq -r --arg API_SUBSCRIPTION_NAME "$API_SUBSCRIPTION_NAME" '.value[] | select(.properties.displayName == $API_SUBSCRIPTION_NAME) | .name' ) echo "Getting API Management Subscription info ... [3/3]" # Call the Azure REST API to get subscription keys output=$(curl -s -S -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Content-Length: 0" \ "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$APIM_RESOURCE_GROUP/providers/Microsoft.ApiManagement/service/$APIM_SERVICE_NAME/subscriptions/$API_SUBSCRIPTION_ID/listSecrets?api-version=2022-08-01") # Extract the subscription keys PRIMARY_KEY=$(echo "$output" | jq -r '.primaryKey') if [[ "$MULTI_REGION" == "true" ]]; then APPGWNAME_DASHES="${APPGATEWAY_FQDN//./-}" TRAFFIC_MANAGER_FQDN="${APPGWNAME_DASHES}.trafficmanager.net" #testUri="curl -k -v https://${TRAFFIC_MANAGER_FQDN}/status-0123456789abcdef" testUri="curl -k -v -H 'Ocp-Apim-Subscription-Key: ${PRIMARY_KEY}' -H 'Content-Type: application/json' https://${TRAFFIC_MANAGER_FQDN}/echo/resource?param1=sample" echo "Testing against ${TRAFFIC_MANAGER_FQDN}" eval ${testUri} echo "Test the deployment by running the following command: ${testUri}" echo -e "\n" else APPGATEWAYPUBLICIPADDRESS=$(az network public-ip show --resource-group "$NETWORK_RESOURCE_GROUP" --name "$APPGATEWAY_PIP" --query ipAddress -o tsv) testUri="curl -k -v -H 'Host: ${APPGATEWAY_FQDN}' -H 'Ocp-Apim-Subscription-Key: ${PRIMARY_KEY}' -H 'Content-Type: application/json' https://${APPGATEWAYPUBLICIPADDRESS}/echo/resource?param1=sample" echo "Testing against ${APPGATEWAY_FQDN}" eval ${testUri} echo "Test the deployment by running the following command: ${testUri}" echo -e "\n" fi ================================================ FILE: scenarios/scripts/terraform/deploy-workload-genai-new.sh ================================================ #!/bin/bash set -e script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" env_file="./.env" while [[ $# -gt 0 ]]; do case "$1" in --auto-confirm|-y) auto_confirm=true; shift 2 ;; --delete-local-state|-d) delete_local_state=true; shift ;; *) echo "Invalid argument: $1"; exit 1 ;; esac done # show a message if auto confirm or delete local state is set if [[ $auto_confirm == true ]]; then echo "Auto-confirmation enabled, proceeding without prompts." fi if [[ $delete_local_state == true ]]; then echo "Delete local state enabled, local Terraform state files will be removed." fi if [[ -f "$env_file" ]]; then echo "Found .env, sourcing it..." cat "$env_file" source "$env_file" else echo "###########################" echo "Error: .env file not found in the current directory." echo " a sample is available at ./sample.env" echo "###########################" exit 1 fi if [[ ${#RANDOM_IDENTIFIER} -eq 0 ]]; then chars="abcdefghijklmnopqrstuvwxyz" random_string="" for _ in {1..3}; do random_char="${chars:RANDOM%${#chars}:1}" random_string+="$random_char" done echo -e "\nRANDOM_IDENTIFIER='$random_string'" >> "$env_file" else random_string="${RANDOM_IDENTIFIER}" fi #### VALIDATE VARIABLES: # params if [[ ${#AZURE_LOCATION} -eq 0 ]]; then echo 'ERROR: Missing environment variable AZURE_LOCATION' 1>&2 exit 6 else AZURE_LOCATION="${AZURE_LOCATION%$'\r'}" fi if [[ ${#RESOURCE_NAME_PREFIX} -eq 0 ]]; then echo 'ERROR: Missing environment variable RESOURCE_NAME_PREFIX' 1>&2 exit 6 else RESOURCE_NAME_PREFIX="${RESOURCE_NAME_PREFIX%$'\r'}" fi if [[ ${#ENVIRONMENT_TAG} -eq 0 ]]; then echo 'ERROR: Missing environment variable ENVIRONMENT_TAG' 1>&2 exit 6 else ENVIRONMENT_TAG="${ENVIRONMENT_TAG%$'\r'}" fi if [[ ${#APPGATEWAY_FQDN} -eq 0 ]]; then echo 'ERROR: Missing environment variable APPGATEWAY_FQDN' 1>&2 exit 6 else APPGATEWAY_FQDN="${APPGATEWAY_FQDN%$'\r'}" fi if [[ ${#CERT_TYPE} -eq 0 ]]; then echo 'ERROR: Missing environment variable CERT_TYPE' 1>&2 exit 6 else CERT_TYPE="${CERT_TYPE%$'\r'}" fi if [[ ${#ENABLE_TELEMETRY} -eq 0 ]]; then telemetry=true fi ### VALIDATE IF AZ LOGIN IS REQUIRED, SHOW THE SUBSCRIPTION AND CONFIRM IF WANT TO CONTINUE az account show > /dev/null if [ $? -ne 0 ]; then echo "You need to login to Azure CLI. Run 'az login' and try again." exit 6 fi echo -e "\n" echo "Currently selected subscription:" az account show --query "{subscriptionId:id, subscriptionName:name}" --output table echo -e "\n" echo "If you want to change the subscription, run 'az account set --subscription '" echo -e "\n" if [[ $auto_confirm == true ]]; then echo "auto-confirmation enabled ... continuing" else echo "Do you want to continue? (y/n)" read -r response if [[ ! $response =~ ^[Yy]$ ]]; then echo "Exiting..." exit 6 fi fi # creating tfvars # create tfvars echo "Creating terraform variables file..." cat << EOF > "$script_dir/../../workload-genai/terraform/${ENVIRONMENT_TAG}.tfvars" location = "${AZURE_LOCATION}" workloadName = "${RESOURCE_NAME_PREFIX}" environment = "${ENVIRONMENT_TAG}" identifier = "${random_string}" enableTelemetry = "${telemetry}" EOF cat "$script_dir/../../workload-genai/terraform/${ENVIRONMENT_TAG}.tfvars" #### Init Terraform with Backend or local storage based on presence of backend.hcl file backend_hcl_file="./${ENVIRONMENT_TAG}-backend.hcl" if [[ -f "$backend_hcl_file" ]]; then echo "Found existing backend file, using it..." echo "Copying backend file to terraform directory..." cp "$backend_hcl_file" "../../workload-genai/terraform/${ENVIRONMENT_TAG}-backend.hcl" cat "../../workload-genai/terraform/${ENVIRONMENT_TAG}-backend.hcl" echo "Initializing Terraform backend..." cd "../../workload-genai/terraform" || exit echo "==" echo "== Starting terraform deployment baseline" echo "==" # Delete local state files echo "== deleting local state files" rm -rf .terraform rm -f terraform.lock.hcl rm -f terraform.tfstate rm -f terraform.tfstate.backup terraform init \ -backend-config="${ENVIRONMENT_TAG}-backend.hcl" \ -backend-config="key=${ENVIRONMENT_TAG}-baseline-lza.tfstate" else echo "Initializing Terraform with local backend..." cd "../../workload-genai/terraform" || exit terraform init -backend=false fi # Check if there is an existing local state file if [[ -f "${ENVIRONMENT_TAG}.tfstate" ]]; then echo -n "Found existing local state files..." if [[ "$delete_local_state" == "true" ]]; then echo "Deleting local Terraform state files..." rm -f "${ENVIRONMENT_TAG}.tfplan" rm -f "${ENVIRONMENT_TAG}.tfvars" rm -f "${ENVIRONMENT_TAG}-backend.hcl" else echo "and reusing it. Use --delete-local-state to remove it." fi fi echo "Creating Terraform plan..." terraform plan -var-file="${ENVIRONMENT_TAG}.tfvars" -out="${ENVIRONMENT_TAG}.tfplan" echo "Terraform plan created" # validate if wants to proceed if [[ $auto_confirm == true ]]; then echo "auto-confirmation enabled ... continuing" response="y" else echo "Do you want to create it? (y/n)" read -r response fi if [[ $response =~ ^[Yy]$ ]]; then echo "Applying Terraform plan..." terraform apply "${ENVIRONMENT_TAG}.tfplan" else echo "Exiting..." exit 6 fi echo "== Completed terraform deployment" # remove the plan file, tfvars and terraform.tfstate rm -f "${ENVIRONMENT_TAG}.tfplan" rm -f terraform.tfstate rm -f "${ENVIRONMENT_TAG}.tfvars" rm -f "${ENVIRONMENT_TAG}-backend.hcl" # Setting variables APIM_SERVICE_NAME="apim-${RESOURCE_NAME_PREFIX}-${ENVIRONMENT_TAG}-${AZURE_LOCATION}-${RANDOM_IDENTIFIER}" APIM_RESOURCE_GROUP="rg-apim-${RESOURCE_NAME_PREFIX}-${ENVIRONMENT_TAG}-${AZURE_LOCATION}-${RANDOM_IDENTIFIER}" NETWORK_RESOURCE_GROUP="rg-networking-${RESOURCE_NAME_PREFIX}-${ENVIRONMENT_TAG}-${AZURE_LOCATION}-${RANDOM_IDENTIFIER}" APPGATEWAY_PIP="pip-appgw-${RESOURCE_NAME_PREFIX}-${ENVIRONMENT_TAG}-${AZURE_LOCATION}-${RANDOM_IDENTIFIER}" SUBSCRIPTION_ID=$(az account show --query id -o tsv) API_SUBSCRIPTION_ID="aoai-product-subscription" MT_PRODUCT1_SUBSCRIPTION_ID="multi-tenant-product1-subscription" MT_PRODUCT2_SUBSCRIPTION_ID="multi-tenant-product2-subscription" # Get the access token TOKEN=$(az account get-access-token --query accessToken --output tsv) # Call the Azure REST API to get subscription keys output=$(curl -s -S -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Content-Length: 0" \ "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$APIM_RESOURCE_GROUP/providers/Microsoft.ApiManagement/service/$APIM_SERVICE_NAME/subscriptions/$API_SUBSCRIPTION_ID/listSecrets?api-version=2022-08-01") # Extract the subscription keys PRIMARY_KEY=$(echo "$output" | jq -r '.primaryKey') APPGATEWAYPUBLICIPADDRESS=$(az network public-ip show --resource-group "$NETWORK_RESOURCE_GROUP" --name "$APPGATEWAY_PIP" --query ipAddress -o tsv) testUri="curl -k -H 'Host: ${APPGATEWAY_FQDN}' -H 'Ocp-Apim-Subscription-Key: ${PRIMARY_KEY}' -H 'Content-Type: application/json' https://${APPGATEWAYPUBLICIPADDRESS}/openai/deployments/gpt-35-turbo-16k/chat/completions?api-version=2024-02-15-preview -d '{\"messages\": [{\"role\":\"system\",\"content\":\"You are an AI assistant that helps people find information.\"}]}'" echo "Test the deployment by running the following command: ${testUri}" echo -e "\n" # Call the Azure REST API to get subscription key of multi-tenant product1 mt_product1_sub_output=$(curl -s -S -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Content-Length: 0" \ "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$APIM_RESOURCE_GROUP/providers/Microsoft.ApiManagement/service/$APIM_SERVICE_NAME/subscriptions/$MT_PRODUCT1_SUBSCRIPTION_ID/listSecrets?api-version=2022-08-01") # Extract the subscription keys MT_PRODUCT1_SUB_PRIMARY_KEY=$(echo "$mt_product1_sub_output" | jq -r '.primaryKey') testUri="curl -k -H 'Host: ${APPGATEWAY_FQDN}' -H 'Ocp-Apim-Subscription-Key: ${MT_PRODUCT1_SUB_PRIMARY_KEY}' -H 'Content-Type: application/json' https://${APPGATEWAYPUBLICIPADDRESS}/openai/deployments/gpt-35-turbo-16k/chat/completions?api-version=2024-02-15-preview -d '{\"messages\": [{\"role\":\"system\",\"content\":\"You are an AI assistant that helps people find information.\"}]}'" echo "Test the deployment for multi-tenant Product1 by running the following command: ${testUri}" echo -e "\n" # Call the Azure REST API to get subscription key of multi-tenant product2 mt_product2_sub_output=$(curl -s -S -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Content-Length: 0" \ "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$APIM_RESOURCE_GROUP/providers/Microsoft.ApiManagement/service/$APIM_SERVICE_NAME/subscriptions/$MT_PRODUCT2_SUBSCRIPTION_ID/listSecrets?api-version=2022-08-01") # Extract the subscription keys MT_PRODUCT2_SUB_PRIMARY_KEY=$(echo "$mt_product2_sub_output" | jq -r '.primaryKey') testUri="curl -k -H 'Host: ${APPGATEWAY_FQDN}' -H 'Ocp-Apim-Subscription-Key: ${MT_PRODUCT2_SUB_PRIMARY_KEY}' -H 'Content-Type: application/json' https://${APPGATEWAYPUBLICIPADDRESS}/openai/deployments/gpt-35-turbo-16k/chat/completions?api-version=2024-02-15-preview -d '{\"messages\": [{\"role\":\"system\",\"content\":\"You are an AI assistant that helps people find information.\"}]}'" echo "Test the deployment for multi-tenant Product2 by running the following command: ${testUri}" echo -e "\n" ================================================ FILE: scenarios/scripts/terraform/deploy-workload-genai.sh ================================================ #!/bin/bash #echo "Not updated yet..." #exit 0 set -e script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" env_file="./.env" echo "Script directory: $script_dir" echo "Current directory: $(pwd)" # if the script is run with -y flag, it will not prompt for confirmation if [[ $1 == "-y" ]]; then auto_confirm=true fi if [[ -f "$env_file" ]]; then echo "Loading .env" # Convert Windows line endings to Unix if needed sed -i 's/\r$//' "$env_file" 2>/dev/null || true source "$env_file" else echo "###########################" echo "Error: .env file not found in the current directory." echo " a sample is available at ./sample.env" echo "###########################" exit 1 fi if [[ ${#RANDOM_IDENTIFIER} -eq 0 ]]; then echo "Please run first the deploy-apim-baseline.sh script" echo "Error: Missing environment variable RANDOM_IDENTIFIER. Automatically created by baseline" 1>&2 exit 6 else random_string="${RANDOM_IDENTIFIER}" fi #### VALIDATE VARIABLES: if [[ ${#AZURE_LOCATION} -eq 0 ]]; then echo 'ERROR: Missing environment variable AZURE_LOCATION' 1>&2 exit 6 else AZURE_LOCATION="${AZURE_LOCATION%$'\r'}" fi # params if [[ ${#AZURE_LOCATION} -eq 0 ]]; then echo 'ERROR: Missing environment variable AZURE_LOCATION' 1>&2 exit 6 else AZURE_LOCATION="${AZURE_LOCATION%$'\r'}" fi if [[ ${#RESOURCE_NAME_PREFIX} -eq 0 ]]; then echo 'ERROR: Missing environment variable RESOURCE_NAME_PREFIX' 1>&2 exit 6 else RESOURCE_NAME_PREFIX="${RESOURCE_NAME_PREFIX%$'\r'}" fi if [[ ${#ENVIRONMENT_TAG} -eq 0 ]]; then echo 'ERROR: Missing environment variable ENVIRONMENT_TAG' 1>&2 exit 6 else ENVIRONMENT_TAG="${ENVIRONMENT_TAG%$'\r'}" fi if [[ ${#APPGATEWAY_FQDN} -eq 0 ]]; then echo 'ERROR: Missing environment variable APPGATEWAY_FQDN' 1>&2 exit 6 else APPGATEWAY_FQDN="${APPGATEWAY_FQDN%$'\r'}" fi if [[ ${#CERT_TYPE} -eq 0 ]]; then echo 'ERROR: Missing environment variable CERT_TYPE' 1>&2 exit 6 else CERT_TYPE="${CERT_TYPE%$'\r'}" fi if [[ ${#ENABLE_TELEMETRY} -eq 0 ]]; then telemetry=true fi ### VALIDATE IF AZ LOGIN IS REQUIRED, SHOW THE SUBSCRIPTION AND CONFIRM IF WANT TO CONTINUE az account show > /dev/null if [ $? -ne 0 ]; then echo "You need to login to Azure CLI. Run 'az login' and try again." exit 6 fi echo "Using subscription:" az account show --query "{subscriptionId:id, subscriptionName:name}" --output table if [[ $auto_confirm == true ]]; then echo "auto-confirmation enabled ... continuing" else echo "Do you want to continue? (y/n)" read -r response if [[ ! $response =~ ^[Yy]$ ]]; then echo "Exiting..." exit 6 fi fi # creating tfvars # create tfvars echo "Creating terraform variables file..." cat << EOF > "$script_dir/../../workload-genai/terraform/${ENVIRONMENT_TAG}.tfvars" location = "${AZURE_LOCATION}" workloadName = "${RESOURCE_NAME_PREFIX}" environment = "${ENVIRONMENT_TAG}" identifier = "${random_string}" enableTelemetry = "${telemetry}" EOF echo "Copying backend file to terraform directory..." backend_hcl_file="./${ENVIRONMENT_TAG}-backend.hcl" if [[ -f "$backend_hcl_file" ]]; then echo "Found existing backend file, using it..." cp "$backend_hcl_file" "../../workload-genai/terraform/${ENVIRONMENT_TAG}-backend.hcl" else echo "No backend file found at $backend_hcl_file, skipping backend configuration..." fi echo "Initializing Terraform backend..." cd "$script_dir/../../workload-genai/terraform" || exit if [[ -f "${ENVIRONMENT_TAG}-backend.hcl" ]]; then echo "Using backend configuration..." # Delete local state files only when using remote backend rm -rf .terraform rm -f terraform.lock.hcl #rm -f terraform.tfstate rm -f terraform.tfstate.backup terraform init \ -backend-config="${ENVIRONMENT_TAG}-backend.hcl" \ -backend-config="key=${ENVIRONMENT_TAG}-genai-lza.tfstate" else echo "Using local state..." # Only delete .terraform directory to force re-initialization rm -rf .terraform rm -f terraform.lock.hcl terraform init fi echo "Creating Terraform plan..." terraform plan -var-file="${ENVIRONMENT_TAG}.tfvars" -out="${ENVIRONMENT_TAG}.tfplan" echo "Terraform plan created" # validate if wants to proceed if [[ $auto_confirm == true ]]; then echo "auto-confirmation enabled ... continuing" response="y" else echo "Do you want to create it? (y/n)" read -r response fi if [[ $response =~ ^[Yy]$ ]]; then echo "Applying Terraform plan..." terraform apply "${ENVIRONMENT_TAG}.tfplan" else echo "Exiting..." exit 6 fi echo "== Completed terraform deployment" # remove the plan file, tfvars and terraform.tfstate rm -f "${ENVIRONMENT_TAG}.tfplan" rm -f terraform.tfstate rm -f "${ENVIRONMENT_TAG}.tfvars" rm -f "${ENVIRONMENT_TAG}-backend.hcl" # Setting variables APIM_SERVICE_NAME="apim-${RESOURCE_NAME_PREFIX}-${ENVIRONMENT_TAG}-${AZURE_LOCATION}-${RANDOM_IDENTIFIER}" APIM_RESOURCE_GROUP="rg-apim-${RESOURCE_NAME_PREFIX}-${ENVIRONMENT_TAG}-${AZURE_LOCATION}-${RANDOM_IDENTIFIER}" NETWORK_RESOURCE_GROUP="rg-networking-${RESOURCE_NAME_PREFIX}-${ENVIRONMENT_TAG}-${AZURE_LOCATION}-${RANDOM_IDENTIFIER}" APPGATEWAY_PIP="pip-appgw-${RESOURCE_NAME_PREFIX}-${ENVIRONMENT_TAG}-${AZURE_LOCATION}-${RANDOM_IDENTIFIER}" SUBSCRIPTION_ID=$(az account show --query id -o tsv) API_SUBSCRIPTION_ID="aoai-product-subscription" MT_PRODUCT1_SUBSCRIPTION_ID="multi-tenant-product1-subscription" MT_PRODUCT2_SUBSCRIPTION_ID="multi-tenant-product2-subscription" # Get the access token TOKEN=$(az account get-access-token --query accessToken --output tsv) # Call the Azure REST API to get subscription keys output=$(curl -s -S -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Content-Length: 0" \ "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$APIM_RESOURCE_GROUP/providers/Microsoft.ApiManagement/service/$APIM_SERVICE_NAME/subscriptions/$API_SUBSCRIPTION_ID/listSecrets?api-version=2022-08-01") # Extract the subscription keys PRIMARY_KEY=$(echo "$output" | jq -r '.primaryKey') APPGATEWAYPUBLICIPADDRESS=$(az network public-ip show --resource-group "$NETWORK_RESOURCE_GROUP" --name "$APPGATEWAY_PIP" --query ipAddress -o tsv) testUri="curl -k -H 'Host: ${APPGATEWAY_FQDN}' -H 'Ocp-Apim-Subscription-Key: ${PRIMARY_KEY}' -H 'Content-Type: application/json' https://${APPGATEWAYPUBLICIPADDRESS}/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-02-15-preview -d '{\"messages\": [{\"role\":\"system\",\"content\":\"You are an AI assistant that helps people find information.\"}]}'" echo "Test the deployment by running the following command: ${testUri}" echo -e "\n" # Call the Azure REST API to get subscription key of multi-tenant product1 mt_product1_sub_output=$(curl -s -S -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Content-Length: 0" \ "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$APIM_RESOURCE_GROUP/providers/Microsoft.ApiManagement/service/$APIM_SERVICE_NAME/subscriptions/$MT_PRODUCT1_SUBSCRIPTION_ID/listSecrets?api-version=2022-08-01") # Extract the subscription keys MT_PRODUCT1_SUB_PRIMARY_KEY=$(echo "$mt_product1_sub_output" | jq -r '.primaryKey') testUri="curl -k -H 'Host: ${APPGATEWAY_FQDN}' -H 'Ocp-Apim-Subscription-Key: ${MT_PRODUCT1_SUB_PRIMARY_KEY}' -H 'Content-Type: application/json' https://${APPGATEWAYPUBLICIPADDRESS}/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-02-15-preview -d '{\"messages\": [{\"role\":\"system\",\"content\":\"You are an AI assistant that helps people find information.\"}]}'" echo "Test the deployment for multi-tenant Product1 by running the following command: ${testUri}" echo -e "\n" # Call the Azure REST API to get subscription key of multi-tenant product2 mt_product2_sub_output=$(curl -s -S -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Content-Length: 0" \ "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$APIM_RESOURCE_GROUP/providers/Microsoft.ApiManagement/service/$APIM_SERVICE_NAME/subscriptions/$MT_PRODUCT2_SUBSCRIPTION_ID/listSecrets?api-version=2022-08-01") # Extract the subscription keys MT_PRODUCT2_SUB_PRIMARY_KEY=$(echo "$mt_product2_sub_output" | jq -r '.primaryKey') testUri="curl -k -H 'Host: ${APPGATEWAY_FQDN}' -H 'Ocp-Apim-Subscription-Key: ${MT_PRODUCT2_SUB_PRIMARY_KEY}' -H 'Content-Type: application/json' https://${APPGATEWAYPUBLICIPADDRESS}/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-02-15-preview -d '{\"messages\": [{\"role\":\"system\",\"content\":\"You are an AI assistant that helps people find information.\"}]}'" echo "Test the deployment for multi-tenant Product2 by running the following command: ${testUri}" echo -e "\n" ================================================ FILE: scenarios/scripts/terraform/sample-multi-region.env ================================================ AZURE_LOCATION='eastus2' RESOURCE_NAME_PREFIX='lzv01' ENVIRONMENT_TAG='dev' APPGATEWAY_FQDN='apim.example.com' CERT_TYPE='selfsigned' ZONE_REDUNDANT='false' MULTI_REGION='true' AZURE_LOCATION2='centralus' ================================================ FILE: scenarios/scripts/terraform/sample.backend.hcl ================================================ resource_group_name = "my-resource-group" storage_account_name = "mystorageaccount" container_name = "my-container" ================================================ FILE: scenarios/scripts/terraform/sample.env ================================================ AZURE_LOCATION='eastus2' RESOURCE_NAME_PREFIX='lzv01' ENVIRONMENT_TAG='dev' APPGATEWAY_FQDN='apim.example.com' CERT_TYPE='selfsigned' ZONE_REDUNDANT='false' MULTI_REGION='false' AZURE_LOCATION2='centralus' ================================================ FILE: scenarios/scripts/terraform/test-apim-baseline-deployment.sh ================================================ #!/bin/bash set -e script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" env_file="./.env" if [[ -f "$env_file" ]]; then echo "Found .env, sourcing it..." cat "$env_file" source "$env_file" else echo "###########################" echo "Error: .env file not found in the current directory." echo " a sample is available at ./sample.env" echo "###########################" exit 1 fi if [[ ${#RANDOM_IDENTIFIER} -eq 0 ]]; then chars="abcdefghijklmnopqrstuvwxyz" random_string="" for _ in {1..3}; do random_char="${chars:RANDOM%${#chars}:1}" random_string+="$random_char" done echo -e "\nRANDOM_IDENTIFIER='$random_string'" >> "$env_file" else random_string="${RANDOM_IDENTIFIER}" fi #### VALIDATE VARIABLES: # params if [[ ${#AZURE_LOCATION} -eq 0 ]]; then echo 'ERROR: Missing environment variable AZURE_LOCATION' 1>&2 exit 6 else AZURE_LOCATION="${AZURE_LOCATION%$'\r'}" fi if [[ ${#RESOURCE_NAME_PREFIX} -eq 0 ]]; then echo 'ERROR: Missing environment variable RESOURCE_NAME_PREFIX' 1>&2 exit 6 else RESOURCE_NAME_PREFIX="${RESOURCE_NAME_PREFIX%$'\r'}" fi if [[ ${#ENVIRONMENT_TAG} -eq 0 ]]; then echo 'ERROR: Missing environment variable ENVIRONMENT_TAG' 1>&2 exit 6 else ENVIRONMENT_TAG="${ENVIRONMENT_TAG%$'\r'}" fi if [[ ${#APPGATEWAY_FQDN} -eq 0 ]]; then echo 'ERROR: Missing environment variable APPGATEWAY_FQDN' 1>&2 exit 6 else APPGATEWAY_FQDN="${APPGATEWAY_FQDN%$'\r'}" fi if [[ ${#CERT_TYPE} -eq 0 ]]; then echo 'ERROR: Missing environment variable CERT_TYPE' 1>&2 exit 6 else CERT_TYPE="${CERT_TYPE%$'\r'}" fi if [[ ${#ENABLE_TELEMETRY} -eq 0 ]]; then telemetry=true fi ### MULTI REGION AND ZONE REDUNDANT UPDATES if [[ "$MULTI_REGION" == "true" ]]; then echo "Multi Region is enabled, checking for AZURE_LOCATION2..." if [[ ${#AZURE_LOCATION2} -eq 0 ]]; then echo 'ERROR: Multi Region was set to true, however environment variable AZURE_LOCATION2 is missing' 1>&2 exit 6 else AZURE_LOCATION2="${AZURE_LOCATION2%$'\r'}" MULTI_REGION="${MULTI_REGION%$'\r'}" echo "Multi Region is enabled, using AZURE_LOCATION2: ${AZURE_LOCATION2}" fi else echo "Multi Region is not enabled, AZURE_LOCATION2 will not be used." MULTI_REGION="${MULTI_REGION%$'\r'}" AZURE_LOCATION2="" fi if [[ ${#ZONE_REDUNDANT} -eq 0 ]]; then # Assume false if not set ZONE_REDUNDANT="false" else ZONE_REDUNDANT="${ZONE_REDUNDANT%$'\r'}" fi ### VALIDATE IF AZ LOGIN IS REQUIRED, SHOW THE SUBSCRIPTION AND CONFIRM IF WANT TO CONTINUE az account show > /dev/null if [ $? -ne 0 ]; then echo "You need to login to Azure CLI. Run 'az login' and try again." exit 6 fi echo -e "\n" echo "Currently selected subscription:" az account show --query "{subscriptionId:id, subscriptionName:name}" --output table echo -e "\n" echo "If you want to change the subscription, run 'az account set --subscription '" echo -e "\n" # Get the current subscription ID SUBSCRIPTION_ID=$(az account show --query id -o tsv) # Testing the deployment echo "Validating deployment..." APIM_SERVICE_NAME="apim-${RESOURCE_NAME_PREFIX}-${ENVIRONMENT_TAG}-${AZURE_LOCATION}-${RANDOM_IDENTIFIER}" APIM_RESOURCE_GROUP="rg-apim-${RESOURCE_NAME_PREFIX}-${ENVIRONMENT_TAG}-${AZURE_LOCATION}-${RANDOM_IDENTIFIER}" NETWORK_RESOURCE_GROUP="rg-networking-${RESOURCE_NAME_PREFIX}-${ENVIRONMENT_TAG}-${AZURE_LOCATION}-${RANDOM_IDENTIFIER}" APPGATEWAY_PIP="pip-appgw-${RESOURCE_NAME_PREFIX}-${ENVIRONMENT_TAG}-${AZURE_LOCATION}-${RANDOM_IDENTIFIER}" SUBSCRIPTION_ID=$(az account show --query id -o tsv) API_SUBSCRIPTION_NAME="Echo API" # Get the access token echo "Obtaining Access Token..." TOKEN=$(az account get-access-token --query accessToken --output tsv) # get the subscription id based on the subscription display name echo "Getting API Management Subscription info ... [1/3]" API_MANAGEMENT_INFO=$(curl -s -S -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$APIM_RESOURCE_GROUP/providers/Microsoft.ApiManagement/service/$APIM_SERVICE_NAME/subscriptions?api-version=2022-08-01") echo "Getting API Management Subscription info ... [2/3]" API_SUBSCRIPTION_ID=$(echo $API_MANAGEMENT_INFO | jq -r --arg API_SUBSCRIPTION_NAME "$API_SUBSCRIPTION_NAME" '.value[] | select(.properties.displayName == $API_SUBSCRIPTION_NAME) | .name' ) echo "Getting API Management Subscription info ... [3/3]" # Call the Azure REST API to get subscription keys output=$(curl -s -S -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Content-Length: 0" \ "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$APIM_RESOURCE_GROUP/providers/Microsoft.ApiManagement/service/$APIM_SERVICE_NAME/subscriptions/$API_SUBSCRIPTION_ID/listSecrets?api-version=2022-08-01") # Extract the subscription keys PRIMARY_KEY=$(echo "$output" | jq -r '.primaryKey') if [[ "$MULTI_REGION" == "true" ]]; then APPGWNAME_DASHES="${APPGATEWAY_FQDN//./-}" TRAFFIC_MANAGER_FQDN="${APPGWNAME_DASHES}.trafficmanager.net" #testUri="curl -k -v https://${TRAFFIC_MANAGER_FQDN}/status-0123456789abcdef" testUri="curl -k -v -H 'Ocp-Apim-Subscription-Key: ${PRIMARY_KEY}' -H 'Content-Type: application/json' https://${TRAFFIC_MANAGER_FQDN}/echo/resource?param1=sample" echo "Testing against ${TRAFFIC_MANAGER_FQDN}" eval ${testUri} echo "Test the deployment by running the following command: ${testUri}" echo -e "\n" else APPGATEWAYPUBLICIPADDRESS=$(az network public-ip show --resource-group "$NETWORK_RESOURCE_GROUP" --name "$APPGATEWAY_PIP" --query ipAddress -o tsv) testUri="curl -k -v -H 'Host: ${APPGATEWAY_FQDN}' -H 'Ocp-Apim-Subscription-Key: ${PRIMARY_KEY}' -H 'Content-Type: application/json' https://${APPGATEWAYPUBLICIPADDRESS}/echo/resource?param1=sample" echo "Testing against ${APPGATEWAY_FQDN}" eval ${testUri} echo "Test the deployment by running the following command: ${testUri}" echo -e "\n" fi ================================================ FILE: scenarios/workload-functions/README.md ================================================ # Scenario 2: Azure API Management - Azure Functions as backend This reference implementation demonstrates how to provision a single region API Management instance within an internal VNet exposed through Application Gateway for external traffic with Azure Functions as the backend (exposed through private endpoint). This scenario is built on top of the [apim secure baseline](../apim-baseline/README.md) scenario. By the end of this deployment guide, you would have deployed an an private Azure functions backend that is available publicly through Application Gateway and manged by API Management. ![Architectural diagram showing an Azure API Management deployment in a virtual network with funcations as backend.](../../docs/images/apim-workload-functions.jpg) ## Core architecture components - Azure Functions - Azure Private Endpoint - Azure Private DNS Zones ## Deploy the reference implementation This reference implementation is provided with the following infrastructure as code options. Select the deployment guide you are interested in. They both deploy the same implementation. :arrow_forward: [Bicep-based deployment guide](./bicep/README.md) :arrow_forward: Terraform-based deployment guide (Work in progress) ================================================ FILE: scenarios/workload-functions/bicep/README.md ================================================ # Scenario 2: Azure API Management - Azure Functions as backend [Bicep] This is the Bicep-based deployment guide for [Scenario 2: Azure API Management - Azure Functions as backend](../README.md). ## Prerequisites This scenario requires the completion of the [Azure API Management - Secure Baseline](../apim-baseline/README.md) scenario. ## Steps Run the following command to deploy the scenarios ```bash ./scripts/bicep/deploy-workload-function.sh ``` Test the hello api using hte generated command from the output ## Troubleshooting If you see the message `-bash: ./deploy-workload-function.sh: /bin/bash^M: bad interpreter: No such file or directory` when running the script, you can fix this by running the following command: ```bash sed -i -e 's/\r$//' deploy-workload-function.sh ``` ================================================ FILE: scenarios/workload-functions/bicep/apim/config.bicep ================================================ param apimName string param backendHostName string var backendUri = '${backendHostName}/api/HttpExample' resource apiManagementInstance 'Microsoft.ApiManagement/service@2022-09-01-preview' existing = { name: apimName } resource helloApi 'Microsoft.ApiManagement/service/apis@2020-12-01' = { name: 'hello' parent: apiManagementInstance properties: { path: 'hello' apiRevision: '1' displayName: 'Hello Api' description: 'Hello Api' subscriptionRequired: true serviceUrl: backendUri protocols: [ 'https' ] } } resource helloApiPolicies 'Microsoft.ApiManagement/service/apis/policies@2020-12-01' = { name: 'policy' parent: helloApi properties: { value: '\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n' format: 'xml' } } resource getOperation 'Microsoft.ApiManagement/service/apis/operations@2023-05-01-preview' = { name: 'say' parent: helloApi properties: { description: 'Say Hello' displayName: 'say' method: 'GET' urlTemplate: '/' request: { description: 'Request description' queryParameters: [ { name: 'name' required: true type: 'string' } ] } } } // Basic product resource basicProduct 'Microsoft.ApiManagement/service/products@2020-12-01' = { name: 'hellobasic' parent: apiManagementInstance properties: { displayName: 'hellow-basic' description: 'Basic hellow product' subscriptionRequired: true approvalRequired: true state: 'published' subscriptionsLimit: 1 terms: 'These are the terms of use ...' } dependsOn: [helloApi] } resource basicProductPolicies 'Microsoft.ApiManagement/service/products/policies@2020-12-01' = { name: 'policy' parent: basicProduct properties: { value: '\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n' format: 'xml' } } resource linkHelloApiToBasicProduct 'Microsoft.ApiManagement/service/products/apis@2020-12-01' = { name: 'hello' parent: basicProduct dependsOn: [helloApi] } resource starterProduct 'Microsoft.ApiManagement/service/products@2020-12-01' existing = { name: 'starter' parent: apiManagementInstance } resource linkHelloApiToStarterProduct 'Microsoft.ApiManagement/service/products/apis@2020-12-01' = { name: 'hello' parent: starterProduct dependsOn: [helloApi] } ================================================ FILE: scenarios/workload-functions/bicep/backend/backend.bicep ================================================ param resourceSuffix string param vnetName string param networkingResourceGroupName string param privateEndpointSubnetid string param location string @description('The language worker runtime to load in the function app.') @allowed([ 'dotnet' 'node' 'python' 'java' ]) param functionWorkerRuntime string = 'node' @description('Specifies the OS used for the Azure Function hosting plan.') @allowed([ 'Windows' 'Linux' ]) param functionPlanOS string = 'Windows' @description('Only required for Linux app to represent runtime stack in the format of \'runtime|runtimeVersion\'. For example: \'python|3.9\'') param linuxFxVersion string = '' var owner = 'APIM Const Set' var storageAccounts_saapimcsbackend_name = toLower(take(replace('stbknd${resourceSuffix}', '-',''), 24)) var storageAccounts_location = location var storageAccounts_skuName = 'Standard_LRS' var storageAccounts_kind = 'StorageV2' var functionContentShareName = 'func-contents' var storageAccounts_minTLSVersion = 'TLS1_2' var privateEndpoint_storageaccount_queue_Name = 'pep-sa-queue-${resourceSuffix}' var privateEndpoint_storageaccount_blob_Name = 'pep-sa-blob-${resourceSuffix}' var privateEndpoint_storageaccount_file_Name = 'pep-sa-file-${resourceSuffix}' var privateEndpoint_storageaccount_table_Name = 'pep-sa-table-${resourceSuffix}' var serverfarms_appsvcplanAPIMCSBackend_name = 'plan-be-${resourceSuffix}' var serverfarms_appsvcplanAPIMCSBackend_location = location var functionAppPlanSku = 'EP1' var functionAppPlanSize = 'EP1' var functionAppPlanFamily = 'EP' var functionAppPlanTier = 'ElasticPremium' var functionAppPlanWorkerCount = 1 var isReserved = ((functionPlanOS == 'Linux') ? true : false) var sites_funcappAPIMCSBackendMicroServiceA_identity = 'mi-func-code-be-${resourceSuffix}' var sites_funcappAPIMCSBackendMicroServiceA_name = 'func-code-be-${resourceSuffix}' var sites_funcappAPIMCSBackendMicroServiceA_location = location var sites_funcappAPIMCSBackendMicroServiceA_siteHostname = 'func-code-be-${resourceSuffix}.azurewebsites.net' var sites_funcappAPIMCSBackendMicroServiceA_repositoryHostname = 'func-code-be-${resourceSuffix}.scm.azurewebsites.net' var sites_funcappAPIMCSBackendMicroServiceA_siteName = 'funccodebe${resourceSuffix}' var privateEndpoint_funcappAPIMCSBackendMicroServiceA_name = 'pep-func-code-be-${resourceSuffix}' module networking './modules/networking.bicep' = { name: 'networkingresources' scope: resourceGroup(networkingResourceGroupName) params: { location: location resourceSuffix: resourceSuffix vnetName: vnetName } } var backendSubnetId = networking.outputs.backEndSubnetid // // Definitions // // Azure Storage Account resource storageAccounts_saapimcsbackend_name_resource 'Microsoft.Storage/storageAccounts@2021-06-01' = { name: storageAccounts_saapimcsbackend_name location: storageAccounts_location tags: { Owner: owner } sku: { name: storageAccounts_skuName } kind: storageAccounts_kind properties: { minimumTlsVersion: storageAccounts_minTLSVersion publicNetworkAccess: 'Disabled' allowBlobPublicAccess: false networkAcls: { bypass: 'None' defaultAction: 'Deny' } supportsHttpsTrafficOnly: true encryption: { services: { file: { keyType: 'Account' enabled: true } blob: { keyType: 'Account' enabled: true } } keySource: 'Microsoft.Storage' } accessTier: 'Hot' } } module queueStoragePrivateEndpoint '../../../apim-baseline/bicep/shared/modules/privateendpoint.bicep' = { name: privateEndpoint_storageaccount_queue_Name params: { location: location privateEndpointName: privateEndpoint_storageaccount_queue_Name domain: 'privatelink.queue.${environment().suffixes.storage}' groupId: 'queue' serviceResourceId: storageAccounts_saapimcsbackend_name_resource.id vnetName: vnetName networkingResourceGroupName: networkingResourceGroupName subnetId: privateEndpointSubnetid } } module blobStoragePrivateEndpoint '../../../apim-baseline/bicep/shared/modules/privateendpoint.bicep' = { name: privateEndpoint_storageaccount_blob_Name params: { location: location privateEndpointName: privateEndpoint_storageaccount_blob_Name groupId: 'blob' domain: 'privatelink.blob.${environment().suffixes.storage}' serviceResourceId: storageAccounts_saapimcsbackend_name_resource.id vnetName: vnetName networkingResourceGroupName: networkingResourceGroupName subnetId: privateEndpointSubnetid } } module tableStoragePrivateEndpoint '../../../apim-baseline/bicep/shared/modules/privateendpoint.bicep' = { name: privateEndpoint_storageaccount_table_Name params: { location: location privateEndpointName: privateEndpoint_storageaccount_table_Name groupId: 'table' domain: 'privatelink.table.${environment().suffixes.storage}' serviceResourceId: storageAccounts_saapimcsbackend_name_resource.id vnetName: vnetName networkingResourceGroupName: networkingResourceGroupName subnetId: privateEndpointSubnetid } } module fileStoragePrivateEndpoint '../../../apim-baseline/bicep/shared/modules/privateendpoint.bicep' = { name: privateEndpoint_storageaccount_file_Name params: { location: location privateEndpointName: privateEndpoint_storageaccount_file_Name groupId: 'file' domain: 'privatelink.file.${environment().suffixes.storage}' serviceResourceId: storageAccounts_saapimcsbackend_name_resource.id vnetName: vnetName networkingResourceGroupName: networkingResourceGroupName subnetId: privateEndpointSubnetid } } resource fileShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2021-04-01' = { name: '${storageAccounts_saapimcsbackend_name_resource.name}/default/${functionContentShareName}' } // Azure Application Service Plan resource serverfarms_appsvcplanAPIMCSBackend_name_resource 'Microsoft.Web/serverfarms@2018-02-01' = { name: serverfarms_appsvcplanAPIMCSBackend_name location: serverfarms_appsvcplanAPIMCSBackend_location tags: { Owner: owner } sku: { name: functionAppPlanSku tier: functionAppPlanTier size: functionAppPlanSize family: functionAppPlanFamily } kind: 'elastic' properties: { maximumElasticWorkerCount: functionAppPlanWorkerCount reserved: isReserved } } resource funcAppIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { name: sites_funcappAPIMCSBackendMicroServiceA_identity location: sites_funcappAPIMCSBackendMicroServiceA_location } // Azure Function App (Linux, .NET Core 3.1) resource sites_funcappAPIMCSBackendMicroServiceA_name_resource 'Microsoft.Web/sites@2018-11-01' = { name: sites_funcappAPIMCSBackendMicroServiceA_name location: sites_funcappAPIMCSBackendMicroServiceA_location tags: { Owner: owner } kind: (isReserved ? 'functionapp,linux' : 'functionapp') identity: { type: 'UserAssigned' userAssignedIdentities: { '${funcAppIdentity.id}': {} } } properties: { reserved: isReserved serverFarmId: serverfarms_appsvcplanAPIMCSBackend_name_resource.id enabled: true hostNameSslStates: [ { name: sites_funcappAPIMCSBackendMicroServiceA_siteHostname sslState: 'Disabled' hostType: 'Standard' } { name: sites_funcappAPIMCSBackendMicroServiceA_repositoryHostname sslState: 'Disabled' hostType: 'Repository' } ] siteConfig: { linuxFxVersion: (isReserved ? linuxFxVersion : null) appSettings: [ { name: 'AzureWebJobsStorage' value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccounts_saapimcsbackend_name};AccountKey=${storageAccounts_saapimcsbackend_name_resource.listKeys().keys[0].value}' } { name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccounts_saapimcsbackend_name};AccountKey=${storageAccounts_saapimcsbackend_name_resource.listKeys().keys[0].value}' } { name: 'FUNCTIONS_EXTENSION_VERSION' value: '~4' } { name: 'FUNCTIONS_WORKER_RUNTIME' value: functionWorkerRuntime } { name: 'WEBSITE_CONTENTOVERVNET' value: '1' } { name: 'WEBSITE_CONTENTSHARE' value: functionContentShareName } { name: 'WEBSITE_VNET_ROUTE_ALL' value: '1' } { name: 'WEBSITE_NODE_DEFAULT_VERSION' value: '~20' } ] } scmSiteAlsoStopped: false clientAffinityEnabled: false clientCertEnabled: false hostNamesDisabled: false containerSize: 1536 dailyMemoryTimeQuota: 0 httpsOnly: true redundancyMode: 'None' } dependsOn: [ queueStoragePrivateEndpoint blobStoragePrivateEndpoint tableStoragePrivateEndpoint fileStoragePrivateEndpoint ] } resource sites_funcappAPIMCSBackendMicroServiceA_name_sites_funcappAPIMCSBackendMicroServiceA_name_azurewebsites_net 'Microsoft.Web/sites/hostNameBindings@2018-11-01' = { parent: sites_funcappAPIMCSBackendMicroServiceA_name_resource name: '${sites_funcappAPIMCSBackendMicroServiceA_name}.azurewebsites.net' properties: { siteName: sites_funcappAPIMCSBackendMicroServiceA_siteName hostNameType: 'Verified' } } resource planNetworkConfig 'Microsoft.Web/sites/networkConfig@2021-01-01' = { parent: sites_funcappAPIMCSBackendMicroServiceA_name_resource name: 'virtualNetwork' properties: { subnetResourceId: backendSubnetId swiftSupported: true } } var privateDNSZoneName = 'privatelink.azurewebsites.net' resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' existing = { name: vnetName scope: resourceGroup(networkingResourceGroupName) } resource privateEndpoint 'Microsoft.Network/privateEndpoints@2021-03-01' = { name: privateEndpoint_funcappAPIMCSBackendMicroServiceA_name location: location properties: { subnet: { id: privateEndpointSubnetid } privateLinkServiceConnections: [ { name: privateEndpoint_funcappAPIMCSBackendMicroServiceA_name properties: { privateLinkServiceId: sites_funcappAPIMCSBackendMicroServiceA_name_resource.id groupIds: [ 'sites' ] } } ] } } resource privateDnsZones 'Microsoft.Network/privateDnsZones@2018-09-01' = { name: privateDNSZoneName location: 'global' } resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2018-09-01' = { parent: privateDnsZones name: uniqueString(vnet.id) location: 'global' properties: { registrationEnabled: false virtualNetwork: { id: vnet.id } } dependsOn: [ privateEndpoint ] } resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2020-03-01' = { parent: privateEndpoint name: 'default' properties: { privateDnsZoneConfigs: [ { name: '${sites_funcappAPIMCSBackendMicroServiceA_siteHostname}-azurewebsites-net' properties: { privateDnsZoneId: privateDnsZones.id } } ] } dependsOn: [ privateDnsZoneLink ] } output funcAppIdentityName string = funcAppIdentity.name output funcAppName string = sites_funcappAPIMCSBackendMicroServiceA_name_resource.name output backendHostName string = 'https://${sites_funcappAPIMCSBackendMicroServiceA_name}.azurewebsites.net' ================================================ FILE: scenarios/workload-functions/bicep/backend/modules/networking.bicep ================================================ param resourceSuffix string param location string param vnetName string param backEndAddressPrefix string = '10.2.6.0/24' var backEndSubnetName = 'snet-bcke-${resourceSuffix}' var backEndSNNSG = 'nsg-bcke-${resourceSuffix}' resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' existing = { name: vnetName } resource subnetBackend 'Microsoft.Network/virtualNetworks/subnets@2021-02-01' = { name: backEndSubnetName parent: vnet properties: { addressPrefix: backEndAddressPrefix delegations: [ { name: 'delegation' properties: { serviceName: 'Microsoft.Web/serverfarms' } } ] privateEndpointNetworkPolicies: 'Enabled' networkSecurityGroup: { id: backEndNSG.id } } } resource backEndNSG 'Microsoft.Network/networkSecurityGroups@2020-06-01' = { name: backEndSNNSG location: location properties: { securityRules: [ ] } } output backEndSubnetName string = backEndSubnetName output backEndSubnetid string = subnetBackend.id ================================================ FILE: scenarios/workload-functions/bicep/deploy/deploy.bicep ================================================ param resourceSuffix string param location string param funcAppName string param deploymentIdentityName string param deploymentSubnetId string param deploymentStorageName string param deploymentIdentityResourceGroupName string param utcValue string = utcNow() resource functionApp 'Microsoft.Web/sites@2018-11-01' existing = { name: funcAppName } resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { scope: resourceGroup(deploymentIdentityResourceGroupName) name: deploymentIdentityName } resource generalContributor 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { name: 'b24988ac-6180-42a0-ab88-20f7382dd24c' // Storage File Data Privileged Contributor scope: tenant() } resource roleAssignmentFunctionApp 'Microsoft.Authorization/roleAssignments@2022-04-01' = { scope: functionApp name: guid(generalContributor.id, userAssignedIdentity.id, functionApp.id) properties: { principalId: userAssignedIdentity.properties.principalId roleDefinitionId: generalContributor.id principalType: 'ServicePrincipal' } } resource dsFunctionApp 'Microsoft.Resources/deploymentScripts@2023-08-01' = { name: 'deploy-script-${resourceSuffix}' location: location identity: { type: 'userAssigned' userAssignedIdentities: { '${userAssignedIdentity.id}': {} } } kind: 'AzureCLI' properties: { forceUpdateTag: utcValue azCliVersion: '2.52.0' storageAccountSettings: { storageAccountName: deploymentStorageName } containerSettings: { subnetIds: [ { id: deploymentSubnetId } ] } scriptContent: 'git clone https://github.com/Azure-Samples/functions-quickstart-javascript; cd functions-quickstart-javascript; zip -r helloworld-latest.zip .; az functionapp deployment source config-zip -g ${resourceGroup().name} -n ${funcAppName} --src helloworld-latest.zip' retentionInterval: 'P1D' cleanupPreference: 'OnExpiration' } dependsOn: [ roleAssignmentFunctionApp ] } ================================================ FILE: scenarios/workload-functions/bicep/deploy/modules/networking.bicep ================================================ param vnetName string param resourceSuffix string param deploymentAddressPrefix string = '10.2.8.0/24' var deploymentSubnetName = 'snet-deploy-${resourceSuffix}' resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' existing = { name: vnetName } resource subnetDeploy 'Microsoft.Network/virtualNetworks/subnets@2021-02-01' = { name: deploymentSubnetName parent: vnet properties: { addressPrefix: deploymentAddressPrefix serviceEndpoints: [ { service: 'Microsoft.Storage' } ] delegations: [ { name: 'Microsoft.ContainerInstance.containerGroups' properties: { serviceName: 'Microsoft.ContainerInstance/containerGroups' } } ] } } output subnetDeployId string = subnetDeploy.id output subnetDeployName string = subnetDeploy.name ================================================ FILE: scenarios/workload-functions/bicep/main.bicep ================================================ targetScope='subscription' param resourceSuffix string param networkingResourceGroupName string param apimResourceGroupName string param apimName string param vnetName string param privateEndpointSubnetid string param deploymentIdentityName string param deploymentSubnetId string param deploymentStorageName string param sharedResourceGroupName string param location string = deployment().location @description('Enable sending usage and telemetry feedback to Microsoft.') param enableTelemetry bool = true var telemetryId = 'ab1e5729-7452-41b2-9fbb-945cc51d9cd0-${location}-apimsb-functions' var workloadResourceGroupName = 'rg-functions-${resourceSuffix}' resource workloadResourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { name: workloadResourceGroupName location: location } module backend './backend/backend.bicep' = { name: 'backendresources' scope: resourceGroup(workloadResourceGroup.name) params: { location: location resourceSuffix: resourceSuffix vnetName: vnetName networkingResourceGroupName: networkingResourceGroupName privateEndpointSubnetid: privateEndpointSubnetid } } module deploy './deploy/deploy.bicep' = { name: 'deploy' scope: resourceGroup(workloadResourceGroup.name) params: { location: location resourceSuffix: resourceSuffix funcAppName: backend.outputs.funcAppName deploymentIdentityName: deploymentIdentityName deploymentSubnetId: deploymentSubnetId deploymentStorageName: deploymentStorageName deploymentIdentityResourceGroupName: sharedResourceGroupName } dependsOn: [ backend ] } module apimConfig './apim/config.bicep' = { name: 'apimConfig' scope: resourceGroup(apimResourceGroupName) params: { apimName: apimName backendHostName: backend.outputs.backendHostName } dependsOn: [ deploy ] } @description('Microsoft telemetry deployment.') #disable-next-line no-deployments-resources resource telemetrydeployment 'Microsoft.Resources/deployments@2021-04-01' = if (enableTelemetry) { location: location name: telemetryId properties: { mode: 'Incremental' template: { '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' contentVersion: '1.0.0.0' resources: {} } } } output backendHostName string = backend.outputs.backendHostName ================================================ FILE: scenarios/workload-genai/README.md ================================================ # Scenario 3: Azure API Management - Generative AI resources as backend This reference implementation demonstrates how to provision and interact with Generative AI resources through API Management. The implementation is on top of the [APIM baseline](./../apim-baseline/README.md) and additionally includes private deployments of Azure OpenAI endpoints, and the policies for the [following capabilities](#genai-gateway-capabilities) that are specifically tailored for GenAI use cases. By the end of this deployment guide, you would have deployed private Azure OpenAI endpoints and an opinionated set of policies in APIM to manage traffic to these endpoints. You can then test the policies by sending requests to the APIM gateway, and can modify either to include the policy fragments [listed here](#scenarios-handled-by-this-accelerator) or to include your own custom policies. ## Architecture ![Architectural diagram showing an Azure API Management deployment in a virtual network with AOAI as backend.](../../docs/images/apim-workload-ai.jpeg) ### Core components - Azure OpenAI endpoints - Azure Event Hub - Azure Private Endpoint - Azure Private DNS Zones ### GenAI Gateway capabilities ![GenAI capabilities](../../docs/images/genai-capabilities.jpg) ## Deploy the reference implementation This reference implementation is provided with the following infrastructure as code options. Select the deployment guide you are interested in. They both deploy the same implementation. :arrow_forward: [Bicep-based deployment guide](./bicep/README.md) :arrow_forward: [Terraform-based deployment guide](./terraform/README.md) ## GenAI Gateway A "GenAI Gateway" serves as an intelligent interface/middleware that dynamically balances incoming traffic across backend resources to achieve optimizing resource utilization. In addition to load balancing, GenAI Gateway can be equipped with extra capabilities to address the challenges around billing, monitoring etc. To read more about considerations when implementing a GenAI Gateway, see [this article](https://learn.microsoft.com/ai/playbook/technology-guidance/generative-ai/dev-starters/genai-gateway/). This accelerator contains APIM policies showing how to implement different [GenAI Gateway capabilities](#genai-gateway-capabilities) in APIM, along with code to enable you to deploy the policies and see them in action. ### Scenarios handled by this accelerator This repo currently contains the policies showing how to implement these GenAI Gateway capabilities: | Capability | Description | | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | [Load balancing (round-robin)](./policies/fragments/load-balancing/README.md) | Load balance traffic across PAYG endpoints using simple and weighted round-robin algorithm. | | [Managing spikes with PAYG](./policies/fragments/manage-spikes-with-payg/README.md) | Manage spikes in traffic by routing traffic to PAYG endpoints when a PTU is out of capacity. | | [Adaptive rate limiting](./policies/fragments/rate-limiting/README.md) | Dynamically adjust rate-limits applied to different workloads| | [Tracking token usage](./policies/fragments/usage-tracking//README.md) | Record the token consumption for usage tracking and attribution| | [Multi-tenancy](./policies/multi-tenancy/README.md)| Implementing multi-tenancy using Products and Product Policies| ### Test/Demo setup If you are looking for a quick way to test or demo these capabilities with a minimalistic non production like APIM setup against a Azure OpenAI simulator, check out this repository. :arrow_forward: [APIM GenAI Gateway Toolkit](https://github.com/Azure-Samples/apim-genai-gateway-toolkit) ## AI Hub Gateway capabilities Looking for comprehensive reference implementation to provision your AI Hub Gateway? Check out AI Hub Gateway scenario. :arrow_forward: [AI Hub Gateway](https://github.com/Azure-Samples/ai-hub-gateway-solution-accelerator) ================================================ FILE: scenarios/workload-genai/bicep/README.md ================================================ # Scenario 3: Azure API Management - Gen AI Backend [Bicep] This is the Bicep-based deployment guide for [Scenario 3: Azure API Management - Gen AI Backend](../README.md). ## Prerequisites This scenario requires the completion of the [Azure API Management - Secure Baseline](../../apim-baseline/README.md) scenario. ## Steps Run the following command to deploy the scenarios ```bash ./scripts/bicep/deploy-workload-genai.sh ``` Test the hello api using hte generated command from the output ## Troubleshooting If you see the message `-bash: ./deploy-workload-genai.sh: /bin/bash^M: bad interpreter: No such file or directory` when running the script, you can fix this by running the following command: ```bash sed -i -e 's/\r$//' deploy-workload-genai.sh ``` ================================================ FILE: scenarios/workload-genai/bicep/apim-policies/api-specs/openapi-spec.json ================================================ { "openapi": "3.0.0", "info": { "title": "Azure OpenAI Service API", "description": "Azure OpenAI APIs for completions and search", "version": "2024-02-01" }, "servers": [ { "url": "https://change-this.com/openai" } ], "security": [ { "bearer": [ "api.read" ] }, { "apiKey": [] } ], "paths": { "/deployments/{deployment-id}/completions": { "post": { "summary": "Creates a completion for the provided prompt, parameters and chosen model.", "operationId": "Completions_Create", "parameters": [ { "in": "path", "name": "deployment-id", "required": true, "schema": { "type": "string", "example": "davinci", "description": "Deployment id of the model which was deployed." } }, { "in": "query", "name": "api-version", "required": true, "schema": { "type": "string", "example": "2024-02-01", "description": "api version" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "prompt": { "description": "The prompt(s) to generate completions for, encoded as a string or array of strings.\nNote that <|endoftext|> is the document separator that the model sees during training, so if a prompt is not specified the model will generate as if from the beginning of a new document. Maximum allowed size of string list is 2048.", "oneOf": [ { "type": "string", "default": "", "example": "This is a test.", "nullable": true }, { "type": "array", "items": { "type": "string", "default": "", "example": "This is a test.", "nullable": false }, "description": "Array size minimum of 1 and maximum of 2048" } ] }, "max_tokens": { "description": "The token count of your prompt plus max_tokens cannot exceed the model's context length. Most models have a context length of 2048 tokens (except for the newest models, which support 4096). Has minimum of 0.", "type": "integer", "default": 16, "example": 16, "nullable": true }, "temperature": { "description": "What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer.\nWe generally recommend altering this or top_p but not both.", "type": "number", "default": 1, "example": 1, "nullable": true }, "top_p": { "description": "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.\nWe generally recommend altering this or temperature but not both.", "type": "number", "default": 1, "example": 1, "nullable": true }, "logit_bias": { "description": "Defaults to null. Modify the likelihood of specified tokens appearing in the completion. Accepts a json object that maps tokens (specified by their token ID in the GPT tokenizer) to an associated bias value from -100 to 100. You can use this tokenizer tool (which works for both GPT-2 and GPT-3) to convert text to token IDs. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token. As an example, you can pass {\"50256\" : -100} to prevent the <|endoftext|> token from being generated.", "type": "object", "nullable": false }, "user": { "description": "A unique identifier representing your end-user, which can help monitoring and detecting abuse", "type": "string", "nullable": false }, "n": { "description": "How many completions to generate for each prompt. Minimum of 1 and maximum of 128 allowed.\nNote: Because this parameter generates many completions, it can quickly consume your token quota. Use carefully and ensure that you have reasonable settings for max_tokens and stop.", "type": "integer", "default": 1, "example": 1, "nullable": true }, "stream": { "description": "Whether to stream back partial progress. If set, tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a data: [DONE] message.", "type": "boolean", "nullable": true, "default": false }, "logprobs": { "description": "Include the log probabilities on the logprobs most likely tokens, as well the chosen tokens. For example, if logprobs is 5, the API will return a list of the 5 most likely tokens. The API will always return the logprob of the sampled token, so there may be up to logprobs+1 elements in the response.\nMinimum of 0 and maximum of 5 allowed.", "type": "integer", "default": null, "nullable": true }, "suffix": { "type": "string", "nullable": true, "description": "The suffix that comes after a completion of inserted text." }, "echo": { "description": "Echo back the prompt in addition to the completion", "type": "boolean", "default": false, "nullable": true }, "stop": { "description": "Up to 4 sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence.", "oneOf": [ { "type": "string", "default": "<|endoftext|>", "example": "\n", "nullable": true }, { "type": "array", "items": { "type": "string", "example": "\n", "nullable": false }, "description": "Array minimum size of 1 and maximum of 4" } ] }, "completion_config": { "type": "string", "nullable": true }, "presence_penalty": { "description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.", "type": "number", "default": 0 }, "frequency_penalty": { "description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.", "type": "number", "default": 0 }, "best_of": { "description": "Generates best_of completions server-side and returns the \"best\" (the one with the highest log probability per token). Results cannot be streamed.\nWhen used with n, best_of controls the number of candidate completions and n specifies how many to return - best_of must be greater than n.\nNote: Because this parameter generates many completions, it can quickly consume your token quota. Use carefully and ensure that you have reasonable settings for max_tokens and stop. Has maximum value of 128.", "type": "integer" } } }, "example": { "prompt": "Negate the following sentence.The price for bubblegum increased on thursday.\n\n Negated Sentence:", "max_tokens": 50 } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "string" }, "object": { "type": "string" }, "created": { "type": "integer" }, "model": { "type": "string" }, "prompt_filter_results": { "$ref": "#/components/schemas/promptFilterResults" }, "choices": { "type": "array", "items": { "type": "object", "properties": { "text": { "type": "string" }, "index": { "type": "integer" }, "logprobs": { "type": "object", "properties": { "tokens": { "type": "array", "items": { "type": "string" } }, "token_logprobs": { "type": "array", "items": { "type": "number" } }, "top_logprobs": { "type": "array", "items": { "type": "object", "additionalProperties": { "type": "number" } } }, "text_offset": { "type": "array", "items": { "type": "integer" } } }, "nullable": true }, "finish_reason": { "type": "string" }, "content_filter_results": { "$ref": "#/components/schemas/contentFilterChoiceResults" } } } }, "usage": { "type": "object", "properties": { "completion_tokens": { "type": "number", "format": "int32" }, "prompt_tokens": { "type": "number", "format": "int32" }, "total_tokens": { "type": "number", "format": "int32" } }, "required": [ "prompt_tokens", "total_tokens", "completion_tokens" ] } }, "required": [ "id", "object", "created", "model", "choices" ] }, "example": { "model": "davinci", "object": "text_completion", "id": "cmpl-4509KAos68kxOqpE2uYGw81j6m7uo", "created": 1637097562, "choices": [ { "index": 0, "text": "The price for bubblegum decreased on thursday.", "logprobs": null, "finish_reason": "stop" } ] } } }, "headers": { "apim-request-id": { "description": "Request ID for troubleshooting purposes", "schema": { "type": "string" } } } }, "default": { "description": "Service unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/errorResponse" } } }, "headers": { "apim-request-id": { "description": "Request ID for troubleshooting purposes", "schema": { "type": "string" } } } } }, "x-ms-examples": { "Create a completion.": { "$ref": "./examples/completions.json" } } } }, "/deployments/{deployment-id}/embeddings": { "post": { "summary": "Get a vector representation of a given input that can be easily consumed by machine learning models and algorithms.", "operationId": "embeddings_create", "parameters": [ { "in": "path", "name": "deployment-id", "required": true, "schema": { "type": "string", "example": "ada-search-index-v1" }, "description": "The deployment id of the model which was deployed." }, { "in": "query", "name": "api-version", "required": true, "schema": { "type": "string", "example": "2024-02-01", "description": "api version" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true, "properties": { "input": { "description": "Input text to get embeddings for, encoded as a string. To get embeddings for multiple inputs in a single request, pass an array of strings. Each input must not exceed 2048 tokens in length.\nUnless you are embedding code, we suggest replacing newlines (\\n) in your input with a single space, as we have observed inferior results when newlines are present.", "oneOf": [ { "type": "string", "default": "", "example": "This is a test.", "nullable": true }, { "type": "array", "minItems": 1, "maxItems": 2048, "items": { "type": "string", "minLength": 1, "example": "This is a test.", "nullable": false } } ] }, "user": { "description": "A unique identifier representing your end-user, which can help monitoring and detecting abuse.", "type": "string", "nullable": false }, "input_type": { "description": "input type of embedding search to use", "type": "string", "example": "query" } }, "required": [ "input" ] } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "object": { "type": "string" }, "model": { "type": "string" }, "data": { "type": "array", "items": { "type": "object", "properties": { "index": { "type": "integer" }, "object": { "type": "string" }, "embedding": { "type": "array", "items": { "type": "number" } } }, "required": [ "index", "object", "embedding" ] } }, "usage": { "type": "object", "properties": { "prompt_tokens": { "type": "integer" }, "total_tokens": { "type": "integer" } }, "required": [ "prompt_tokens", "total_tokens" ] } }, "required": [ "object", "model", "data", "usage" ] } } } } }, "x-ms-examples": { "Create a embeddings.": { "$ref": "./examples/embeddings.json" } } } }, "/deployments/{deployment-id}/chat/completions": { "post": { "summary": "Creates a completion for the chat message", "operationId": "ChatCompletions_Create", "parameters": [ { "in": "path", "name": "deployment-id", "required": true, "schema": { "type": "string", "description": "Deployment id of the model which was deployed." } }, { "in": "query", "name": "api-version", "required": true, "schema": { "type": "string", "example": "2024-02-01", "description": "api version" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/createChatCompletionRequest" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/createChatCompletionResponse" } } }, "headers": { "apim-request-id": { "description": "Request ID for troubleshooting purposes", "schema": { "type": "string" } } } }, "default": { "description": "Service unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/errorResponse" } } }, "headers": { "apim-request-id": { "description": "Request ID for troubleshooting purposes", "schema": { "type": "string" } } } } }, "x-ms-examples": { "Create a chat completion.": { "$ref": "./examples/chat_completions.json" }, "Creates a completion based on Azure Search data and system-assigned managed identity.": { "$ref": "./examples/chat_completions_azure_search_minimum.json" }, "Creates a completion based on Azure Search vector data, previous assistant message and user-assigned managed identity.": { "$ref": "./examples/chat_completions_azure_search_advanced.json" }, "Creates a completion for the provided Azure Cosmos DB.": { "$ref": "./examples/chat_completions_cosmos_db.json" } } } } }, "components": { "schemas": { "errorResponse": { "type": "object", "properties": { "error": { "$ref": "#/components/schemas/error" } } }, "errorBase": { "type": "object", "properties": { "code": { "type": "string" }, "message": { "type": "string" } } }, "error": { "type": "object", "allOf": [ { "$ref": "#/components/schemas/errorBase" } ], "properties": { "param": { "type": "string" }, "type": { "type": "string" }, "inner_error": { "$ref": "#/components/schemas/innerError" } } }, "innerError": { "description": "Inner error with additional details.", "type": "object", "properties": { "code": { "$ref": "#/components/schemas/innerErrorCode" }, "content_filter_results": { "$ref": "#/components/schemas/contentFilterPromptResults" } } }, "innerErrorCode": { "description": "Error codes for the inner error object.", "enum": [ "ResponsibleAIPolicyViolation" ], "type": "string", "x-ms-enum": { "name": "InnerErrorCode", "modelAsString": true, "values": [ { "value": "ResponsibleAIPolicyViolation", "description": "The prompt violated one of more content filter rules." } ] } }, "dalleErrorResponse": { "type": "object", "properties": { "error": { "$ref": "#/components/schemas/dalleError" } } }, "dalleError": { "type": "object", "allOf": [ { "$ref": "#/components/schemas/errorBase" } ], "properties": { "param": { "type": "string" }, "type": { "type": "string" }, "inner_error": { "$ref": "#/components/schemas/dalleInnerError" } } }, "dalleInnerError": { "description": "Inner error with additional details.", "type": "object", "properties": { "code": { "$ref": "#/components/schemas/innerErrorCode" }, "content_filter_results": { "$ref": "#/components/schemas/dalleFilterResults" }, "revised_prompt": { "type": "string", "description": "The prompt that was used to generate the image, if there was any revision to the prompt." } } }, "contentFilterResultBase": { "type": "object", "properties": { "filtered": { "type": "boolean" } }, "required": [ "filtered" ] }, "contentFilterSeverityResult": { "type": "object", "allOf": [ { "$ref": "#/components/schemas/contentFilterResultBase" }, { "properties": { "severity": { "type": "string", "enum": [ "safe", "low", "medium", "high" ], "x-ms-enum": { "name": "ContentFilterSeverity", "modelAsString": true, "values": [ { "value": "safe", "description": "General content or related content in generic or non-harmful contexts." }, { "value": "low", "description": "Harmful content at a low intensity and risk level." }, { "value": "medium", "description": "Harmful content at a medium intensity and risk level." }, { "value": "high", "description": "Harmful content at a high intensity and risk level." } ] } } } } ], "required": [ "severity", "filtered" ] }, "contentFilterDetectedResult": { "type": "object", "allOf": [ { "$ref": "#/components/schemas/contentFilterResultBase" }, { "properties": { "detected": { "type": "boolean" } } } ], "required": [ "detected", "filtered" ] }, "contentFilterDetectedWithCitationResult": { "type": "object", "allOf": [ { "$ref": "#/components/schemas/contentFilterDetectedResult" }, { "properties": { "citation": { "type": "object", "properties": { "URL": { "type": "string" }, "license": { "type": "string" } } } } } ], "required": [ "detected", "filtered" ] }, "contentFilterResultsBase": { "type": "object", "description": "Information about the content filtering results.", "properties": { "sexual": { "$ref": "#/components/schemas/contentFilterSeverityResult" }, "violence": { "$ref": "#/components/schemas/contentFilterSeverityResult" }, "hate": { "$ref": "#/components/schemas/contentFilterSeverityResult" }, "self_harm": { "$ref": "#/components/schemas/contentFilterSeverityResult" }, "profanity": { "$ref": "#/components/schemas/contentFilterDetectedResult" }, "error": { "$ref": "#/components/schemas/errorBase" } } }, "contentFilterPromptResults": { "type": "object", "description": "Information about the content filtering category (hate, sexual, violence, self_harm), if it has been detected, as well as the severity level (very_low, low, medium, high-scale that determines the intensity and risk level of harmful content) and if it has been filtered or not. Information about jailbreak content and profanity, if it has been detected, and if it has been filtered or not. And information about customer block list, if it has been filtered and its id.", "allOf": [ { "$ref": "#/components/schemas/contentFilterResultsBase" }, { "properties": { "jailbreak": { "$ref": "#/components/schemas/contentFilterDetectedResult" } } } ] }, "contentFilterChoiceResults": { "type": "object", "description": "Information about the content filtering category (hate, sexual, violence, self_harm), if it has been detected, as well as the severity level (very_low, low, medium, high-scale that determines the intensity and risk level of harmful content) and if it has been filtered or not. Information about third party text and profanity, if it has been detected, and if it has been filtered or not. And information about customer block list, if it has been filtered and its id.", "allOf": [ { "$ref": "#/components/schemas/contentFilterResultsBase" }, { "properties": { "protected_material_text": { "$ref": "#/components/schemas/contentFilterDetectedResult" } } }, { "properties": { "protected_material_code": { "$ref": "#/components/schemas/contentFilterDetectedWithCitationResult" } } } ] }, "promptFilterResult": { "type": "object", "description": "Content filtering results for a single prompt in the request.", "properties": { "prompt_index": { "type": "integer" }, "content_filter_results": { "$ref": "#/components/schemas/contentFilterPromptResults" } } }, "promptFilterResults": { "type": "array", "description": "Content filtering results for zero or more prompts in the request. In a streaming request, results for different prompts may arrive at different times or in different orders.", "items": { "$ref": "#/components/schemas/promptFilterResult" } }, "dalleContentFilterResults": { "type": "object", "description": "Information about the content filtering results.", "properties": { "sexual": { "$ref": "#/components/schemas/contentFilterSeverityResult" }, "violence": { "$ref": "#/components/schemas/contentFilterSeverityResult" }, "hate": { "$ref": "#/components/schemas/contentFilterSeverityResult" }, "self_harm": { "$ref": "#/components/schemas/contentFilterSeverityResult" } } }, "dalleFilterResults": { "type": "object", "description": "Information about the content filtering category (hate, sexual, violence, self_harm), if it has been detected, as well as the severity level (very_low, low, medium, high-scale that determines the intensity and risk level of harmful content) and if it has been filtered or not. Information about jailbreak content and profanity, if it has been detected, and if it has been filtered or not. And information about customer block list, if it has been filtered and its id.", "allOf": [ { "$ref": "#/components/schemas/dalleContentFilterResults" }, { "properties": { "profanity": { "$ref": "#/components/schemas/contentFilterDetectedResult" }, "jailbreak": { "$ref": "#/components/schemas/contentFilterDetectedResult" } } } ] }, "chatCompletionsRequestCommon": { "type": "object", "properties": { "temperature": { "description": "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\nWe generally recommend altering this or `top_p` but not both.", "type": "number", "minimum": 0, "maximum": 2, "default": 1, "example": 1, "nullable": true }, "top_p": { "description": "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.\nWe generally recommend altering this or `temperature` but not both.", "type": "number", "minimum": 0, "maximum": 1, "default": 1, "example": 1, "nullable": true }, "stream": { "description": "If set, partial message deltas will be sent, like in ChatGPT. Tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a `data: [DONE]` message.", "type": "boolean", "nullable": true, "default": false }, "stop": { "description": "Up to 4 sequences where the API will stop generating further tokens.", "oneOf": [ { "type": "string", "nullable": true }, { "type": "array", "items": { "type": "string", "nullable": false }, "minItems": 1, "maxItems": 4, "description": "Array minimum size of 1 and maximum of 4" } ], "default": null }, "max_tokens": { "description": "The maximum number of tokens allowed for the generated answer. By default, the number of tokens the model can return will be (4096 - prompt tokens).", "type": "integer", "default": 4096 }, "presence_penalty": { "description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.", "type": "number", "default": 0, "minimum": -2, "maximum": 2 }, "frequency_penalty": { "description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.", "type": "number", "default": 0, "minimum": -2, "maximum": 2 }, "logit_bias": { "description": "Modify the likelihood of specified tokens appearing in the completion. Accepts a json object that maps tokens (specified by their token ID in the tokenizer) to an associated bias value from -100 to 100. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token.", "type": "object", "nullable": true }, "user": { "description": "A unique identifier representing your end-user, which can help Azure OpenAI to monitor and detect abuse.", "type": "string", "example": "user-1234", "nullable": false } } }, "createChatCompletionRequest": { "type": "object", "allOf": [ { "$ref": "#/components/schemas/chatCompletionsRequestCommon" }, { "properties": { "messages": { "description": "A list of messages comprising the conversation so far. [Example Python code](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_format_inputs_to_ChatGPT_models.ipynb).", "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/chatCompletionRequestMessage" } }, "data_sources": { "type": "array", "description": " The configuration entries for Azure OpenAI chat extensions that use them.\n This additional specification is only compatible with Azure OpenAI.", "items": { "$ref": "#/components/schemas/azureChatExtensionConfiguration" } }, "n": { "type": "integer", "minimum": 1, "maximum": 128, "default": 1, "example": 1, "nullable": true, "description": "How many chat completion choices to generate for each input message." }, "seed": { "type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807, "default": 0, "example": 1, "nullable": true, "description": "If specified, our system will make a best effort to sample deterministically, such that repeated requests with the same `seed` and parameters should return the same result.Determinism is not guaranteed, and you should refer to the `system_fingerprint` response parameter to monitor changes in the backend." }, "response_format": { "type": "object", "description": "An object specifying the format that the model must output. Used to enable JSON mode.", "properties": { "type": { "$ref": "#/components/schemas/chatCompletionResponseFormat" } } }, "tools": { "description": "A list of tools the model may call. Currently, only functions are supported as a tool. Use this to provide a list of functions the model may generate JSON inputs for.", "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/chatCompletionTool" } }, "tool_choice": { "$ref": "#/components/schemas/chatCompletionToolChoiceOption" }, "functions": { "description": "Deprecated in favor of `tools`. A list of functions the model may generate JSON inputs for.", "type": "array", "minItems": 1, "maxItems": 128, "items": { "$ref": "#/components/schemas/chatCompletionFunction" } }, "function_call": { "description": "Deprecated in favor of `tool_choice`. Controls how the model responds to function calls. \"none\" means the model does not call a function, and responds to the end-user. \"auto\" means the model can pick between an end-user or calling a function. Specifying a particular function via `{\"name\":\\ \"my_function\"}` forces the model to call that function. \"none\" is the default when no functions are present. \"auto\" is the default if functions are present.", "oneOf": [ { "type": "string", "enum": [ "none", "auto" ], "description": "`none` means the model will not call a function and instead generates a message. `auto` means the model can pick between generating a message or calling a function." }, { "type": "object", "description": "Specifying a particular function via `{\"name\": \"my_function\"}` forces the model to call that function.", "properties": { "name": { "type": "string", "description": "The name of the function to call." } }, "required": [ "name" ] } ] } } } ], "required": [ "messages" ] }, "chatCompletionResponseFormat": { "type": "string", "enum": [ "text", "json_object" ], "default": "text", "example": "json_object", "nullable": true, "description": "Setting to `json_object` enables JSON mode. This guarantees that the message the model generates is valid JSON.", "x-ms-enum": { "name": "ChatCompletionResponseFormat", "modelAsString": true, "values": [ { "value": "text", "description": "Response format is a plain text string." }, { "value": "json_object", "description": "Response format is a JSON object." } ] } }, "chatCompletionFunction": { "type": "object", "properties": { "name": { "type": "string", "description": "The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64." }, "description": { "type": "string", "description": "The description of what the function does." }, "parameters": { "$ref": "#/components/schemas/chatCompletionFunctionParameters" } }, "required": [ "name" ] }, "chatCompletionFunctionParameters": { "type": "object", "description": "The parameters the functions accepts, described as a JSON Schema object. See the [guide](/docs/guides/gpt/function-calling) for examples, and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format.", "additionalProperties": true }, "chatCompletionRequestMessage": { "type": "object", "properties": { "role": { "$ref": "#/components/schemas/chatCompletionRequestMessageRole" } }, "discriminator": { "propertyName": "role", "mapping": { "system": "#/components/schemas/chatCompletionRequestMessageSystem", "user": "#/components/schemas/chatCompletionRequestMessageUser", "assistant": "#/components/schemas/chatCompletionRequestMessageAssistant", "tool": "#/components/schemas/chatCompletionRequestMessageTool", "function": "#/components/schemas/chatCompletionRequestMessageFunction" } }, "required": [ "role" ] }, "chatCompletionRequestMessageRole": { "type": "string", "enum": [ "system", "user", "assistant", "tool", "function" ], "description": "The role of the messages author.", "x-ms-enum": { "name": "ChatCompletionRequestMessageRole", "modelAsString": true, "values": [ { "value": "system", "description": "The message author role is system." }, { "value": "user", "description": "The message author role is user." }, { "value": "assistant", "description": "The message author role is assistant." }, { "value": "tool", "description": "The message author role is tool." }, { "value": "function", "description": "Deprecated. The message author role is function." } ] } }, "chatCompletionRequestMessageSystem": { "allOf": [ { "$ref": "#/components/schemas/chatCompletionRequestMessage" }, { "type": "object", "properties": { "content": { "type": "string", "description": "The contents of the message.", "nullable": true } } } ], "required": [ "content" ] }, "chatCompletionRequestMessageUser": { "allOf": [ { "$ref": "#/components/schemas/chatCompletionRequestMessage" }, { "type": "object", "properties": { "content": { "oneOf": [ { "type": "string", "description": "The contents of the message." }, { "type": "array", "description": "An array of content parts with a defined type, each can be of type `text` or `image_url` when passing in images. You can pass multiple images by adding multiple `image_url` content parts. Image input is only supported when using the `gpt-4-visual-preview` model.", "minimum": 1, "items": { "$ref": "#/components/schemas/chatCompletionRequestMessageContentPart" } } ], "nullable": true } } } ], "required": [ "content" ] }, "chatCompletionRequestMessageContentPart": { "type": "object", "properties": { "type": { "$ref": "#/components/schemas/chatCompletionRequestMessageContentPartType" } }, "discriminator": { "propertyName": "type", "mapping": { "text": "#/components/schemas/chatCompletionRequestMessageContentPartText", "image_url": "#/components/schemas/chatCompletionRequestMessageContentPartImage" } }, "required": [ "type" ] }, "chatCompletionRequestMessageContentPartType": { "type": "string", "enum": [ "text", "image_url" ], "description": "The type of the content part.", "x-ms-enum": { "name": "ChatCompletionRequestMessageContentPartType", "modelAsString": true, "values": [ { "value": "text", "description": "The content part type is text." }, { "value": "image_url", "description": "The content part type is image_url." } ] } }, "chatCompletionRequestMessageContentPartText": { "allOf": [ { "$ref": "#/components/schemas/chatCompletionRequestMessageContentPart" }, { "type": "object", "properties": { "text": { "type": "string", "description": "The text content." } } } ], "required": [ "text" ] }, "chatCompletionRequestMessageContentPartImage": { "allOf": [ { "$ref": "#/components/schemas/chatCompletionRequestMessageContentPart" }, { "type": "object", "properties": { "url": { "type": "string", "description": "Either a URL of the image or the base64 encoded image data.", "format": "uri" }, "detail": { "$ref": "#/components/schemas/imageDetailLevel" } } } ], "required": [ "url" ] }, "imageDetailLevel": { "type": "string", "description": "Specifies the detail level of the image.", "enum": [ "auto", "low", "high" ], "default": "auto", "x-ms-enum": { "name": "ImageDetailLevel", "modelAsString": true, "values": [ { "value": "auto", "description": "The image detail level is auto." }, { "value": "low", "description": "The image detail level is low." }, { "value": "high", "description": "The image detail level is high." } ] } }, "chatCompletionRequestMessageAssistant": { "allOf": [ { "$ref": "#/components/schemas/chatCompletionRequestMessage" }, { "type": "object", "properties": { "content": { "type": "string", "description": "The contents of the message.", "nullable": true }, "tool_calls": { "type": "array", "description": "The tool calls generated by the model, such as function calls.", "items": { "$ref": "#/components/schemas/chatCompletionMessageToolCall" } }, "context": { "$ref": "#/components/schemas/azureChatExtensionsMessageContext" } } } ], "required": [ "content" ] }, "azureChatExtensionConfiguration": { "required": [ "type" ], "type": "object", "properties": { "type": { "$ref": "#/components/schemas/azureChatExtensionType" } }, "description": " A representation of configuration data for a single Azure OpenAI chat extension. This will be used by a chat\n completions request that should use Azure OpenAI chat extensions to augment the response behavior.\n The use of this configuration is compatible only with Azure OpenAI.", "discriminator": { "propertyName": "type", "mapping": { "azure_search": "#/components/schemas/azureSearchChatExtensionConfiguration", "azure_cosmos_db": "#/components/schemas/azureCosmosDBChatExtensionConfiguration" } } }, "azureChatExtensionType": { "type": "string", "description": " A representation of configuration data for a single Azure OpenAI chat extension. This will be used by a chat\n completions request that should use Azure OpenAI chat extensions to augment the response behavior.\n The use of this configuration is compatible only with Azure OpenAI.", "enum": [ "azure_search", "azure_cosmos_db" ], "x-ms-enum": { "name": "AzureChatExtensionType", "modelAsString": true, "values": [ { "name": "azureSearch", "value": "azure_search", "description": "Represents the use of Azure Search as an Azure OpenAI chat extension." }, { "name": "azureCosmosDB", "value": "azure_cosmos_db", "description": "Represents the use of Azure Cosmos DB as an Azure OpenAI chat extension." } ] } }, "azureSearchChatExtensionConfiguration": { "required": [ "parameters" ], "description": "A specific representation of configurable options for Azure Search when using it as an Azure OpenAI chat\nextension.", "allOf": [ { "$ref": "#/components/schemas/azureChatExtensionConfiguration" }, { "properties": { "parameters": { "$ref": "#/components/schemas/azureSearchChatExtensionParameters" } } } ], "x-ms-discriminator-value": "azure_search" }, "azureSearchChatExtensionParameters": { "required": [ "authentication", "endpoint", "index_name" ], "type": "object", "properties": { "authentication": { "oneOf": [ { "$ref": "#/components/schemas/onYourDataApiKeyAuthenticationOptions" }, { "$ref": "#/components/schemas/onYourDataSystemAssignedManagedIdentityAuthenticationOptions" }, { "$ref": "#/components/schemas/onYourDataUserAssignedManagedIdentityAuthenticationOptions" } ] }, "top_n_documents": { "type": "integer", "description": "The configured top number of documents to feature for the configured query.", "format": "int32" }, "in_scope": { "type": "boolean", "description": "Whether queries should be restricted to use of indexed data." }, "strictness": { "maximum": 5, "minimum": 1, "type": "integer", "description": "The configured strictness of the search relevance filtering. The higher of strictness, the higher of the precision but lower recall of the answer.", "format": "int32" }, "role_information": { "type": "string", "description": "Give the model instructions about how it should behave and any context it should reference when generating a response. You can describe the assistant's personality and tell it how to format responses. There's a 100 token limit for it, and it counts against the overall token limit." }, "endpoint": { "type": "string", "description": "The absolute endpoint path for the Azure Search resource to use.", "format": "uri" }, "index_name": { "type": "string", "description": "The name of the index to use as available in the referenced Azure Search resource." }, "fields_mapping": { "$ref": "#/components/schemas/azureSearchIndexFieldMappingOptions" }, "query_type": { "$ref": "#/components/schemas/azureSearchQueryType" }, "semantic_configuration": { "type": "string", "description": "The additional semantic configuration for the query." }, "filter": { "type": "string", "description": "Search filter." }, "embedding_dependency": { "oneOf": [ { "$ref": "#/components/schemas/onYourDataEndpointVectorizationSource" }, { "$ref": "#/components/schemas/onYourDataDeploymentNameVectorizationSource" } ] } }, "description": "Parameters for Azure Search when used as an Azure OpenAI chat extension." }, "azureSearchIndexFieldMappingOptions": { "type": "object", "properties": { "title_field": { "type": "string", "description": "The name of the index field to use as a title." }, "url_field": { "type": "string", "description": "The name of the index field to use as a URL." }, "filepath_field": { "type": "string", "description": "The name of the index field to use as a filepath." }, "content_fields": { "type": "array", "description": "The names of index fields that should be treated as content.", "items": { "type": "string" } }, "content_fields_separator": { "type": "string", "description": "The separator pattern that content fields should use." }, "vector_fields": { "type": "array", "description": "The names of fields that represent vector data.", "items": { "type": "string" } } }, "description": "Optional settings to control how fields are processed when using a configured Azure Search resource." }, "azureSearchQueryType": { "type": "string", "description": "The type of Azure Search retrieval query that should be executed when using it as an Azure OpenAI chat extension.", "enum": [ "simple", "semantic", "vector", "vector_simple_hybrid", "vector_semantic_hybrid" ], "x-ms-enum": { "name": "azureSearchQueryType", "modelAsString": true, "values": [ { "name": "simple", "value": "simple", "description": "Represents the default, simple query parser." }, { "name": "semantic", "value": "semantic", "description": "Represents the semantic query parser for advanced semantic modeling." }, { "name": "vector", "value": "vector", "description": "Represents vector search over computed data." }, { "name": "vectorSimpleHybrid", "value": "vector_simple_hybrid", "description": "Represents a combination of the simple query strategy with vector data." }, { "name": "vectorSemanticHybrid", "value": "vector_semantic_hybrid", "description": "Represents a combination of semantic search and vector data querying." } ] } }, "azureCosmosDBChatExtensionConfiguration": { "required": [ "parameters" ], "description": "A specific representation of configurable options for Azure Cosmos DB when using it as an Azure OpenAI chat\nextension.", "allOf": [ { "$ref": "#/components/schemas/azureChatExtensionConfiguration" }, { "properties": { "parameters": { "$ref": "#/components/schemas/azureCosmosDBChatExtensionParameters" } } } ], "x-ms-discriminator-value": "azure_cosmos_db" }, "azureCosmosDBChatExtensionParameters": { "required": [ "authentication", "container_name", "database_name", "embedding_dependency", "fields_mapping", "index_name" ], "type": "object", "properties": { "authentication": { "$ref": "#/components/schemas/onYourDataConnectionStringAuthenticationOptions" }, "top_n_documents": { "type": "integer", "description": "The configured top number of documents to feature for the configured query.", "format": "int32" }, "in_scope": { "type": "boolean", "description": "Whether queries should be restricted to use of indexed data." }, "strictness": { "maximum": 5, "minimum": 1, "type": "integer", "description": "The configured strictness of the search relevance filtering. The higher of strictness, the higher of the precision but lower recall of the answer.", "format": "int32" }, "role_information": { "type": "string", "description": "Give the model instructions about how it should behave and any context it should reference when generating a response. You can describe the assistant's personality and tell it how to format responses. There's a 100 token limit for it, and it counts against the overall token limit." }, "database_name": { "type": "string", "description": "The MongoDB vCore database name to use with Azure Cosmos DB." }, "container_name": { "type": "string", "description": "The name of the Azure Cosmos DB resource container." }, "index_name": { "type": "string", "description": "The MongoDB vCore index name to use with Azure Cosmos DB." }, "fields_mapping": { "$ref": "#/components/schemas/azureCosmosDBFieldMappingOptions" }, "embedding_dependency": { "oneOf": [ { "$ref": "#/components/schemas/onYourDataEndpointVectorizationSource" }, { "$ref": "#/components/schemas/onYourDataDeploymentNameVectorizationSource" } ] } }, "description": "Parameters to use when configuring Azure OpenAI On Your Data chat extensions when using Azure Cosmos DB for\nMongoDB vCore." }, "azureCosmosDBFieldMappingOptions": { "required": [ "content_fields", "vector_fields" ], "type": "object", "properties": { "title_field": { "type": "string", "description": "The name of the index field to use as a title." }, "url_field": { "type": "string", "description": "The name of the index field to use as a URL." }, "filepath_field": { "type": "string", "description": "The name of the index field to use as a filepath." }, "content_fields": { "type": "array", "description": "The names of index fields that should be treated as content.", "items": { "type": "string" } }, "content_fields_separator": { "type": "string", "description": "The separator pattern that content fields should use." }, "vector_fields": { "type": "array", "description": "The names of fields that represent vector data.", "items": { "type": "string" } } }, "description": "Optional settings to control how fields are processed when using a configured Azure Cosmos DB resource." }, "onYourDataAuthenticationOptions": { "required": [ "type" ], "type": "object", "properties": { "type": { "$ref": "#/components/schemas/onYourDataAuthenticationType" } }, "description": "The authentication options for Azure OpenAI On Your Data.", "discriminator": { "propertyName": "type", "mapping": { "api_key": "#/components/schemas/onYourDataApiKeyAuthenticationOptions", "connection_string": "#/components/schemas/onYourDataConnectionStringAuthenticationOptions", "system_assigned_managed_identity": "#/components/schemas/onYourDataSystemAssignedManagedIdentityAuthenticationOptions", "user_assigned_managed_identity": "#/components/schemas/onYourDataUserAssignedManagedIdentityAuthenticationOptions" } } }, "onYourDataAuthenticationType": { "type": "string", "description": "The authentication types supported with Azure OpenAI On Your Data.", "enum": [ "api_key", "connection_string", "system_assigned_managed_identity", "user_assigned_managed_identity" ], "x-ms-enum": { "name": "OnYourDataAuthenticationType", "modelAsString": true, "values": [ { "name": "apiKey", "value": "api_key", "description": "Authentication via API key." }, { "name": "connectionString", "value": "connection_string", "description": "Authentication via connection string." }, { "name": "systemAssignedManagedIdentity", "value": "system_assigned_managed_identity", "description": "Authentication via system-assigned managed identity." }, { "name": "userAssignedManagedIdentity", "value": "user_assigned_managed_identity", "description": "Authentication via user-assigned managed identity." } ] } }, "onYourDataApiKeyAuthenticationOptions": { "required": [ "key" ], "description": "The authentication options for Azure OpenAI On Your Data when using an API key.", "allOf": [ { "$ref": "#/components/schemas/onYourDataAuthenticationOptions" }, { "properties": { "key": { "type": "string", "description": "The API key to use for authentication." } } } ], "x-ms-discriminator-value": "api_key" }, "onYourDataConnectionStringAuthenticationOptions": { "required": [ "connection_string" ], "description": "The authentication options for Azure OpenAI On Your Data when using a connection string.", "allOf": [ { "$ref": "#/components/schemas/onYourDataAuthenticationOptions" }, { "properties": { "connection_string": { "type": "string", "description": "The connection string to use for authentication." } } } ], "x-ms-discriminator-value": "connection_string" }, "onYourDataSystemAssignedManagedIdentityAuthenticationOptions": { "description": "The authentication options for Azure OpenAI On Your Data when using a system-assigned managed identity.", "allOf": [ { "$ref": "#/components/schemas/onYourDataAuthenticationOptions" } ], "x-ms-discriminator-value": "system_assigned_managed_identity" }, "onYourDataUserAssignedManagedIdentityAuthenticationOptions": { "required": [ "managed_identity_resource_id" ], "description": "The authentication options for Azure OpenAI On Your Data when using a user-assigned managed identity.", "allOf": [ { "$ref": "#/components/schemas/onYourDataAuthenticationOptions" }, { "properties": { "managed_identity_resource_id": { "type": "string", "description": "The resource ID of the user-assigned managed identity to use for authentication." } } } ], "x-ms-discriminator-value": "user_assigned_managed_identity" }, "onYourDataVectorizationSource": { "required": [ "type" ], "type": "object", "properties": { "type": { "$ref": "#/components/schemas/onYourDataVectorizationSourceType" } }, "description": "An abstract representation of a vectorization source for Azure OpenAI On Your Data with vector search.", "discriminator": { "propertyName": "type", "mapping": { "endpoint": "#/components/schemas/onYourDataEndpointVectorizationSource", "deployment_name": "#/components/schemas/onYourDataDeploymentNameVectorizationSource" } } }, "onYourDataVectorizationSourceType": { "type": "string", "description": "Represents the available sources Azure OpenAI On Your Data can use to configure vectorization of data for use with\nvector search.", "enum": [ "endpoint", "deployment_name" ], "x-ms-enum": { "name": "OnYourDataVectorizationSourceType", "modelAsString": true, "values": [ { "name": "endpoint", "value": "endpoint", "description": "Represents vectorization performed by public service calls to an Azure OpenAI embedding model." }, { "name": "deploymentName", "value": "deployment_name", "description": "Represents an Ada model deployment name to use. This model deployment must be in the same Azure OpenAI resource, but\nOn Your Data will use this model deployment via an internal call rather than a public one, which enables vector\nsearch even in private networks." } ] } }, "onYourDataDeploymentNameVectorizationSource": { "required": [ "deployment_name" ], "description": "The details of a a vectorization source, used by Azure OpenAI On Your Data when applying vector search, that is based\non an internal embeddings model deployment name in the same Azure OpenAI resource.", "allOf": [ { "$ref": "#/components/schemas/onYourDataVectorizationSource" }, { "properties": { "deployment_name": { "type": "string", "description": "Specifies the name of the model deployment to use for vectorization. This model deployment must be in the same Azure OpenAI resource, but On Your Data will use this model deployment via an internal call rather than a public one, which enables vector search even in private networks." } } } ], "x-ms-discriminator-value": "deployment_name" }, "onYourDataEndpointVectorizationSource": { "required": [ "authentication", "endpoint" ], "description": "The details of a a vectorization source, used by Azure OpenAI On Your Data when applying vector search, that is based\non a public Azure OpenAI endpoint call for embeddings.", "allOf": [ { "$ref": "#/components/schemas/onYourDataVectorizationSource" }, { "properties": { "authentication": { "$ref": "#/components/schemas/onYourDataApiKeyAuthenticationOptions" }, "endpoint": { "type": "string", "description": "Specifies the endpoint to use for vectorization. This endpoint must be in the same Azure OpenAI resource, but On Your Data will use this endpoint via an internal call rather than a public one, which enables vector search even in private networks.", "format": "uri" } } } ], "x-ms-discriminator-value": "endpoint" }, "azureChatExtensionsMessageContext": { "type": "object", "properties": { "citations": { "type": "array", "description": "The data source retrieval result, used to generate the assistant message in the response.", "items": { "$ref": "#/components/schemas/citation" }, "x-ms-identifiers": [] }, "intent": { "type": "string", "description": "The detected intent from the chat history, used to pass to the next turn to carry over the context." } }, "description": " A representation of the additional context information available when Azure OpenAI chat extensions are involved\n in the generation of a corresponding chat completions response. This context information is only populated when\n using an Azure OpenAI request configured to use a matching extension." }, "citation": { "required": [ "content" ], "type": "object", "properties": { "content": { "type": "string", "description": "The content of the citation." }, "title": { "type": "string", "description": "The title of the citation." }, "url": { "type": "string", "description": "The URL of the citation." }, "filepath": { "type": "string", "description": "The file path of the citation." }, "chunk_id": { "type": "string", "description": "The chunk ID of the citation." } }, "description": "citation information for a chat completions response message." }, "chatCompletionMessageToolCall": { "type": "object", "properties": { "id": { "type": "string", "description": "The ID of the tool call." }, "type": { "$ref": "#/components/schemas/toolCallType" }, "function": { "type": "object", "description": "The function that the model called.", "properties": { "name": { "type": "string", "description": "The name of the function to call." }, "arguments": { "type": "string", "description": "The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function." } }, "required": [ "name", "arguments" ] } }, "required": [ "id", "type", "function" ] }, "toolCallType": { "type": "string", "enum": [ "function" ], "description": "The type of the tool call, in this case `function`.", "x-ms-enum": { "name": "ToolCallType", "modelAsString": true, "values": [ { "value": "function", "description": "The tool call type is function." } ] } }, "chatCompletionRequestMessageTool": { "allOf": [ { "$ref": "#/components/schemas/chatCompletionRequestMessage" }, { "type": "object", "nullable": true, "properties": { "tool_call_id": { "type": "string", "description": "Tool call that this message is responding to." }, "content": { "type": "string", "description": "The contents of the message.", "nullable": true } } } ], "required": [ "tool_call_id", "content" ] }, "chatCompletionRequestMessageFunction": { "allOf": [ { "$ref": "#/components/schemas/chatCompletionRequestMessage" }, { "type": "object", "description": "Deprecated. Message that represents a function.", "nullable": true, "properties": { "role": { "type": "string", "enum": [ "function" ], "description": "The role of the messages author, in this case `function`." }, "name": { "type": "string", "description": "The contents of the message." }, "content": { "type": "string", "description": "The contents of the message.", "nullable": true } } } ], "required": [ "function_call_id", "content" ] }, "createChatCompletionResponse": { "type": "object", "allOf": [ { "$ref": "#/components/schemas/chatCompletionsResponseCommon" }, { "properties": { "prompt_filter_results": { "$ref": "#/components/schemas/promptFilterResults" }, "choices": { "type": "array", "items": { "type": "object", "allOf": [ { "$ref": "#/components/schemas/chatCompletionChoiceCommon" }, { "properties": { "message": { "$ref": "#/components/schemas/chatCompletionResponseMessage" }, "content_filter_results": { "$ref": "#/components/schemas/contentFilterChoiceResults" } } } ] } } } } ], "required": [ "id", "object", "created", "model", "choices" ] }, "chatCompletionResponseMessage": { "type": "object", "description": "A chat completion message generated by the model.", "properties": { "role": { "$ref": "#/components/schemas/chatCompletionResponseMessageRole" }, "content": { "type": "string", "description": "The contents of the message.", "nullable": true }, "tool_calls": { "type": "array", "description": "The tool calls generated by the model, such as function calls.", "items": { "$ref": "#/components/schemas/chatCompletionMessageToolCall" } }, "function_call": { "$ref": "#/components/schemas/chatCompletionFunctionCall" }, "context": { "$ref": "#/components/schemas/azureChatExtensionsMessageContext" } } }, "chatCompletionResponseMessageRole": { "type": "string", "enum": [ "assistant" ], "description": "The role of the author of the response message." }, "chatCompletionToolChoiceOption": { "description": "Controls which (if any) function is called by the model. `none` means the model will not call a function and instead generates a message. `auto` means the model can pick between generating a message or calling a function. Specifying a particular function via `{\"type\": \"function\", \"function\": {\"name\": \"my_function\"}}` forces the model to call that function.", "oneOf": [ { "type": "string", "description": "`none` means the model will not call a function and instead generates a message. `auto` means the model can pick between generating a message or calling a function.", "enum": [ "none", "auto" ] }, { "$ref": "#/components/schemas/chatCompletionNamedToolChoice" } ] }, "chatCompletionNamedToolChoice": { "type": "object", "description": "Specifies a tool the model should use. Use to force the model to call a specific function.", "properties": { "type": { "type": "string", "enum": [ "function" ], "description": "The type of the tool. Currently, only `function` is supported." }, "function": { "type": "object", "properties": { "name": { "type": "string", "description": "The name of the function to call." } }, "required": [ "name" ] } } }, "chatCompletionFunctionCall": { "type": "object", "description": "Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model.", "properties": { "name": { "type": "string", "description": "The name of the function to call." }, "arguments": { "type": "string", "description": "The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function." } }, "required": [ "name", "arguments" ] }, "chatCompletionsResponseCommon": { "type": "object", "properties": { "id": { "type": "string", "description": "A unique identifier for the chat completion." }, "object": { "$ref": "#/components/schemas/chatCompletionResponseObject" }, "created": { "type": "integer", "format": "unixtime", "description": "The Unix timestamp (in seconds) of when the chat completion was created." }, "model": { "type": "string", "description": "The model used for the chat completion." }, "usage": { "$ref": "#/components/schemas/completionUsage" }, "system_fingerprint": { "type": "string", "description": "Can be used in conjunction with the `seed` request parameter to understand when backend changes have been made that might impact determinism." } }, "required": [ "id", "object", "created", "model" ] }, "chatCompletionResponseObject": { "type": "string", "description": "The object type.", "enum": [ "chat.completion" ], "x-ms-enum": { "name": "ChatCompletionResponseObject", "modelAsString": true, "values": [ { "value": "chat.completion", "description": "The object type is chat completion." } ] } }, "completionUsage": { "type": "object", "description": "Usage statistics for the completion request.", "properties": { "prompt_tokens": { "type": "integer", "description": "Number of tokens in the prompt." }, "completion_tokens": { "type": "integer", "description": "Number of tokens in the generated completion." }, "total_tokens": { "type": "integer", "description": "Total number of tokens used in the request (prompt + completion)." } }, "required": [ "prompt_tokens", "completion_tokens", "total_tokens" ] }, "chatCompletionTool": { "type": "object", "properties": { "type": { "$ref": "#/components/schemas/chatCompletionToolType" }, "function": { "type": "object", "properties": { "description": { "type": "string", "description": "A description of what the function does, used by the model to choose when and how to call the function." }, "name": { "type": "string", "description": "The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64." }, "parameters": { "$ref": "#/components/schemas/chatCompletionFunctionParameters" } }, "required": [ "name", "parameters" ] } }, "required": [ "type", "function" ] }, "chatCompletionToolType": { "type": "string", "enum": [ "function" ], "description": "The type of the tool. Currently, only `function` is supported.", "x-ms-enum": { "name": "ChatCompletionToolType", "modelAsString": true, "values": [ { "value": "function", "description": "The tool type is function." } ] } }, "chatCompletionChoiceCommon": { "type": "object", "properties": { "index": { "type": "integer" }, "finish_reason": { "type": "string" } } }, "createTranslationRequest": { "type": "object", "description": "Translation request.", "properties": { "file": { "type": "string", "description": "The audio file to translate.", "format": "binary" }, "prompt": { "type": "string", "description": "An optional text to guide the model's style or continue a previous audio segment. The prompt should be in English." }, "response_format": { "$ref": "#/components/schemas/audioResponseFormat" }, "temperature": { "type": "number", "default": 0, "description": "The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit." } }, "required": [ "file" ] }, "audioResponse": { "description": "Translation or transcription response when response_format was json", "type": "object", "properties": { "text": { "type": "string", "description": "Translated or transcribed text." } }, "required": [ "text" ] }, "audioVerboseResponse": { "description": "Translation or transcription response when response_format was verbose_json", "type": "object", "allOf": [ { "$ref": "#/components/schemas/audioResponse" }, { "properties": { "task": { "type": "string", "description": "Type of audio task.", "enum": [ "transcribe", "translate" ], "x-ms-enum": { "modelAsString": true } }, "language": { "type": "string", "description": "Language." }, "duration": { "type": "number", "description": "Duration." }, "segments": { "type": "array", "items": { "$ref": "#/components/schemas/audioSegment" } } } } ], "required": [ "text" ] }, "audioResponseFormat": { "title": "AudioResponseFormat", "description": "Defines the format of the output.", "enum": [ "json", "text", "srt", "verbose_json", "vtt" ], "type": "string", "x-ms-enum": { "modelAsString": true } }, "createTranscriptionRequest": { "type": "object", "description": "Transcription request.", "properties": { "file": { "type": "string", "description": "The audio file object to transcribe.", "format": "binary" }, "prompt": { "type": "string", "description": "An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language." }, "response_format": { "$ref": "#/components/schemas/audioResponseFormat" }, "temperature": { "type": "number", "default": 0, "description": "The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit." }, "language": { "type": "string", "description": "The language of the input audio. Supplying the input language in ISO-639-1 format will improve accuracy and latency." } }, "required": [ "file" ] }, "audioSegment": { "type": "object", "description": "Transcription or translation segment.", "properties": { "id": { "type": "integer", "description": "Segment identifier." }, "seek": { "type": "number", "description": "Offset of the segment." }, "start": { "type": "number", "description": "Segment start offset." }, "end": { "type": "number", "description": "Segment end offset." }, "text": { "type": "string", "description": "Segment text." }, "tokens": { "type": "array", "items": { "type": "number", "nullable": false }, "description": "Tokens of the text." }, "temperature": { "type": "number", "description": "Temperature." }, "avg_logprob": { "type": "number", "description": "Average log probability." }, "compression_ratio": { "type": "number", "description": "Compression ratio." }, "no_speech_prob": { "type": "number", "description": "Probability of 'no speech'." } } }, "imageQuality": { "description": "The quality of the image that will be generated.", "type": "string", "enum": [ "standard", "hd" ], "default": "standard", "x-ms-enum": { "name": "Quality", "modelAsString": true, "values": [ { "value": "standard", "description": "Standard quality creates images with standard quality.", "name": "Standard" }, { "value": "hd", "description": "HD quality creates images with finer details and greater consistency across the image.", "name": "HD" } ] } }, "imagesResponseFormat": { "description": "The format in which the generated images are returned.", "type": "string", "enum": [ "url", "b64_json" ], "default": "url", "x-ms-enum": { "name": "ImagesResponseFormat", "modelAsString": true, "values": [ { "value": "url", "description": "The URL that provides temporary access to download the generated images.", "name": "Url" }, { "value": "b64_json", "description": "The generated images are returned as base64 encoded string.", "name": "Base64Json" } ] } }, "imageSize": { "description": "The size of the generated images.", "type": "string", "enum": [ "1792x1024", "1024x1792", "1024x1024" ], "default": "1024x1024", "x-ms-enum": { "name": "Size", "modelAsString": true, "values": [ { "value": "1792x1024", "description": "The desired size of the generated image is 1792x1024 pixels.", "name": "Size1792x1024" }, { "value": "1024x1792", "description": "The desired size of the generated image is 1024x1792 pixels.", "name": "Size1024x1792" }, { "value": "1024x1024", "description": "The desired size of the generated image is 1024x1024 pixels.", "name": "Size1024x1024" } ] } }, "imageStyle": { "description": "The style of the generated images.", "type": "string", "enum": [ "vivid", "natural" ], "default": "vivid", "x-ms-enum": { "name": "Style", "modelAsString": true, "values": [ { "value": "vivid", "description": "Vivid creates images that are hyper-realistic and dramatic.", "name": "Vivid" }, { "value": "natural", "description": "Natural creates images that are more natural and less hyper-realistic.", "name": "Natural" } ] } }, "imageGenerationsRequest": { "type": "object", "properties": { "prompt": { "description": "A text description of the desired image(s). The maximum length is 4000 characters.", "type": "string", "format": "string", "example": "a corgi in a field", "minLength": 1 }, "n": { "description": "The number of images to generate.", "type": "integer", "minimum": 1, "maximum": 1, "default": 1 }, "size": { "$ref": "#/components/schemas/imageSize" }, "response_format": { "$ref": "#/components/schemas/imagesResponseFormat" }, "user": { "description": "A unique identifier representing your end-user, which can help to monitor and detect abuse.", "type": "string", "format": "string", "example": "user123456" }, "quality": { "$ref": "#/components/schemas/imageQuality" }, "style": { "$ref": "#/components/schemas/imageStyle" } }, "required": [ "prompt" ] }, "generateImagesResponse": { "type": "object", "properties": { "created": { "type": "integer", "format": "unixtime", "description": "The unix timestamp when the operation was created.", "example": "1676540381" }, "data": { "type": "array", "description": "The result data of the operation, if successful", "items": { "$ref": "#/components/schemas/imageResult" } } }, "required": [ "created", "data" ] }, "imageResult": { "type": "object", "description": "The image url or encoded image if successful, and an error otherwise.", "properties": { "url": { "type": "string", "description": "The image url.", "example": "https://www.contoso.com" }, "b64_json": { "type": "string", "description": "The base64 encoded image" }, "content_filter_results": { "$ref": "#/components/schemas/dalleContentFilterResults" }, "revised_prompt": { "type": "string", "description": "The prompt that was used to generate the image, if there was any revision to the prompt." }, "prompt_filter_results": { "$ref": "#/components/schemas/dalleFilterResults" } } } }, "securitySchemes": { "bearer": { "type": "oauth2", "flows": { "implicit": { "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", "scopes": {} } }, "x-tokenInfoFunc": "api.middleware.auth.bearer_auth", "x-scopeValidateFunc": "api.middleware.auth.validate_scopes" }, "apiKey": { "type": "apiKey", "name": "api-key", "in": "header" } } } } ================================================ FILE: scenarios/workload-genai/bicep/apim-policies/apiManagement.bicep ================================================ @description('The name of the API Management service instance') param apiManagementServiceName string @description('The base url of the first Azure Open AI Service PTU deployment (e.g. https://{your-resource-name}.openai.azure.com/openai/deployments/{deployment-id}/)') param ptuDeploymentOneBaseUrl string @description('The base url of the first Azure Open AI Service Pay-As-You-Go deployment (e.g. https://{your-resource-name}.openai.azure.com/openai/deployments/{deployment-id}/)') param payAsYouGoDeploymentOneBaseUrl string @description('The base url of the second Azure Open AI Service Pay-As-You-Go deployment (e.g. https://{your-resource-name}.openai.azure.com/openai/deployments/{deployment-id}/)') param payAsYouGoDeploymentTwoBaseUrl string @description('The name of the Event Hub Namespace to log to') param eventHubNamespaceName string @description('The name of the Event Hub to log utilization data to') param eventHubName string param apimIdentityName string var apimIdentityNameValue = 'apim-identity' resource apiManagementService 'Microsoft.ApiManagement/service@2023-05-01-preview' existing = { name: apiManagementServiceName } resource apimIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { name: apimIdentityName } resource azureOpenAIApi 'Microsoft.ApiManagement/service/apis@2023-05-01-preview' = { parent: apiManagementService name: 'azure-openai-api' properties: { path: '/openai' displayName: 'AzureOpenAI' protocols: ['https'] value: loadTextContent('./api-specs/openapi-spec.json') format: 'openapi+json' } } resource azureOpenAIProduct 'Microsoft.ApiManagement/service/products@2023-05-01-preview' = { parent: apiManagementService name: 'aoai-product' properties: { displayName: 'aoai-product' subscriptionRequired: true state: 'published' approvalRequired: false } } resource multiTenantProduct1 'Microsoft.ApiManagement/service/products@2023-05-01-preview' = { parent: apiManagementService name: 'multi-tenant-product1' properties: { displayName: 'multi-tenant-product1' subscriptionRequired: true state: 'published' approvalRequired: false } } resource multiTenantProduct2 'Microsoft.ApiManagement/service/products@2023-05-01-preview' = { parent: apiManagementService name: 'multi-tenant-product2' properties: { displayName: 'multi-tenant-product2' subscriptionRequired: true state: 'published' approvalRequired: false } } var azureOpenAIAPINames = [ azureOpenAIApi.name ] resource azureOpenAIProductAPIAssociation 'Microsoft.ApiManagement/service/products/apis@2023-05-01-preview' = [ for apiName in azureOpenAIAPINames: { name: '${apiManagementServiceName}/${azureOpenAIProduct.name}/${apiName}' } ] resource multiTenantProduct1APIAssociation 'Microsoft.ApiManagement/service/products/apis@2023-05-01-preview' = [ for apiName in azureOpenAIAPINames: { name: '${apiManagementServiceName}/${multiTenantProduct1.name}/${apiName}' } ] resource multiTenantProduct2APIAssociation 'Microsoft.ApiManagement/service/products/apis@2023-05-01-preview' = [ for apiName in azureOpenAIAPINames: { name: '${apiManagementServiceName}/${multiTenantProduct2.name}/${apiName}' } ] resource ptuBackendOne 'Microsoft.ApiManagement/service/backends@2023-05-01-preview' = { parent: apiManagementService name: 'ptu-backend-1' properties:{ protocol: 'http' url: ptuDeploymentOneBaseUrl } } resource payAsYouGoBackendOne 'Microsoft.ApiManagement/service/backends@2023-05-01-preview' = { parent: apiManagementService name: 'payg-backend-1' properties:{ protocol: 'http' url: payAsYouGoDeploymentOneBaseUrl } } resource payAsYouGoBackendTwo 'Microsoft.ApiManagement/service/backends@2023-05-01-preview' = { parent: apiManagementService name: 'payg-backend-2' properties:{ protocol: 'http' url: payAsYouGoDeploymentTwoBaseUrl } } resource azureOpenAIProductSubscription 'Microsoft.ApiManagement/service/subscriptions@2023-05-01-preview' = { parent: apiManagementService name: 'aoai-product-subscription' properties: { displayName: 'aoai-product-subscription' state: 'active' scope: azureOpenAIProduct.id } } resource multiTenantProduct1Subscription 'Microsoft.ApiManagement/service/subscriptions@2023-05-01-preview' = { parent: apiManagementService name: 'multi-tenant-product1-subscription' properties: { displayName: 'multi-tenant-product1-subscription' state: 'active' scope: multiTenantProduct1.id } } resource multiTenantProduct2Subscription 'Microsoft.ApiManagement/service/subscriptions@2023-05-01-preview' = { parent: apiManagementService name: 'multi-tenant-product2-subscription' properties: { displayName: 'multi-tenant-product2-subscription' state: 'active' scope: multiTenantProduct2.id } } resource simpleRoundRobinPolicyFragment 'Microsoft.ApiManagement/service/policyFragments@2023-05-01-preview' = { parent: apiManagementService name: 'simple-priority-weighted' properties: { value: loadTextContent('../../policies/fragments/load-balancing/simple-priority-weighted.xml') format: 'rawxml' } dependsOn: [payAsYouGoBackendOne, payAsYouGoBackendTwo] } resource simpleRateLimitingPolicyFragment 'Microsoft.ApiManagement/service/policyFragments@2023-05-01-preview' = { parent: apiManagementService name: 'rate-limiting-by-tokens' properties: { value: loadTextContent('../../policies/fragments/rate-limiting/rate-limiting-by-tokens.xml') format: 'rawxml' } dependsOn: [payAsYouGoBackendOne, ptuBackendOne] } resource adaptiveRateLimitingPolicyFragment 'Microsoft.ApiManagement/service/policyFragments@2023-05-01-preview' = { parent: apiManagementService name: 'adaptive-rate-limiting' properties: { value: loadTextContent('../../policies/fragments/rate-limiting/adaptive-rate-limiting.xml') format: 'rawxml' } dependsOn: [payAsYouGoBackendOne, ptuBackendOne] } resource adaptiveRateLimitingWorkAroundPolicyFragment 'Microsoft.ApiManagement/service/policyFragments@2023-05-01-preview' = { parent: apiManagementService name: 'rate-limiting-workaround' properties: { value: loadTextContent('../../policies/fragments/rate-limiting/rate-limiting-workaround.xml') format: 'rawxml' } dependsOn: [payAsYouGoBackendOne, ptuBackendOne] } resource usageTrackingEHPolicyFragment 'Microsoft.ApiManagement/service/policyFragments@2023-05-01-preview' = { parent: apiManagementService name: 'usage-tracking-with-eventhub' properties: { value: loadTextContent('../../policies/fragments/usage-tracking/usage-tracking-with-eventhub.xml') format: 'rawxml' } dependsOn: [eventHubLogger] } resource usageTrackingWithAppInsightsPolicyFragment 'Microsoft.ApiManagement/service/policyFragments@2023-05-01-preview' = { parent: apiManagementService name: 'usage-tracking-with-appinsights' properties: { value: loadTextContent('../../policies/fragments/usage-tracking/usage-tracking-with-appinsights.xml') format: 'rawxml' } dependsOn: [eventHubLogger] } //Load-balancing with Circuit Breaker policy module apiBackend './load-balancing/backends.bicep' = { name: 'apiBackend' params: { apiManagementServiceName: apiManagementServiceName backendUris: ['${ptuDeploymentOneBaseUrl}/', '${payAsYouGoDeploymentOneBaseUrl}/', '${payAsYouGoDeploymentTwoBaseUrl}/'] } } module apiLBPool './load-balancing/lb-pool.bicep' = { name: 'apimLBPool' params: { apiManagementServiceName: apiManagementServiceName backends: apiBackend.outputs.backendNames } dependsOn: [ apiBackend ] } //Load the policies resource azureOpenAIApiPolicy 'Microsoft.ApiManagement/service/apis/policies@2023-05-01-preview' = { parent: azureOpenAIApi name: 'policy' properties: { value: loadTextContent('../../policies/genai-policy.xml') format: 'rawxml' } dependsOn: [ simpleRoundRobinPolicyFragment adaptiveRateLimitingPolicyFragment usageTrackingWithAppInsightsPolicyFragment] } resource multiTenantProduct1Policy 'Microsoft.ApiManagement/service/products/policies@2024-06-01-preview' = { parent: multiTenantProduct1 name: 'policy' properties: { value: loadTextContent('../../policies/multi-tenancy/multi-tenant-product1-policy.xml') format: 'rawxml' } dependsOn: [apiBackend] } resource multiTenantProduct2Policy 'Microsoft.ApiManagement/service/products/policies@2024-06-01-preview' = { parent: multiTenantProduct2 name: 'policy' properties: { value: loadTextContent('../../policies/multi-tenancy/multi-tenant-product2-policy.xml') format: 'rawxml' } dependsOn: [apiBackend] } resource apimOpenaiApiUamiNamedValue 'Microsoft.ApiManagement/service/namedValues@2022-08-01' = { name: apimIdentityNameValue parent: apiManagementService properties: { displayName: apimIdentityNameValue secret: true value: apimIdentity.properties.clientId } } resource eventHubLogger 'Microsoft.ApiManagement/service/loggers@2022-04-01-preview' = { name: 'eventhub-logger' parent: apiManagementService properties: { loggerType: 'azureEventHub' description: 'Event hub logger with system-assigned managed identity' credentials: { endpointAddress: '${eventHubNamespaceName}.servicebus.windows.net' identityClientId: apimIdentity.properties.clientId name: eventHubName } } } output apiManagementServiceName string = apiManagementService.name output apiManagementAzureOpenAIProductSubscriptionKey string = azureOpenAIProductSubscription.listSecrets().primaryKey output apiManagementMultitenantProduct1SubscriptionKey string = multiTenantProduct1Subscription.listSecrets().primaryKey output apiManagementMultitenantProduct2SubscriptionKey string = multiTenantProduct2Subscription.listSecrets().primaryKey ================================================ FILE: scenarios/workload-genai/bicep/apim-policies/load-balancing/backends.bicep ================================================ param apiManagementServiceName string param backendUris array resource apiManagementService 'Microsoft.ApiManagement/service@2023-05-01-preview' existing = { name: apiManagementServiceName } resource backend 'Microsoft.ApiManagement/service/backends@2023-05-01-preview' = [for (backendUri, i) in backendUris: { parent: apiManagementService name: 'aoai-${i}' properties: { url: backendUri protocol: 'http' circuitBreaker: { rules: [{ name: 'breakerRule' failureCondition: { count: 1 interval: 'PT1M' statusCodeRanges: [ { min: 429 max: 429 }] errorReasons: ['timeout'] } tripDuration: 'PT1M' acceptRetryAfter: true }] } } } ] output backendNames array = [for i in range(0, length(backendUris)): backend[i].name] ================================================ FILE: scenarios/workload-genai/bicep/apim-policies/load-balancing/lb-pool.bicep ================================================ param apiManagementServiceName string param backends array resource apiManagementService 'Microsoft.ApiManagement/service@2023-05-01-preview' existing = { name: apiManagementServiceName } resource backend 'Microsoft.ApiManagement/service/backends@2023-05-01-preview' = { parent: apiManagementService name: 'aoai-lb-pool' properties: { title: 'aoai-lb-pool' type: 'Pool' pool: { services: [for (backend, i) in backends: { id: '/backends/${backend}' priority: i%2 == 0 ? 1 : 2 weight: i+1 }] } } } ================================================ FILE: scenarios/workload-genai/bicep/eventhub/eventHub.bicep ================================================ @description('The name of the Event Hub Namespace') param eventHubNamespaceName string @description('The name of the Event Hub') param eventHubName string @description('The messaging tier for Event Hub Namespace.') @allowed([ 'Basic' 'Standard' ]) param eventHubSku string = 'Standard' param apimIdentityName string param apimResourceGroupName string @description('Location for all resources.') param location string = resourceGroup().location resource eventHubNamespace 'Microsoft.EventHub/namespaces@2021-11-01' = { name: eventHubNamespaceName location: location sku: { name: eventHubSku tier: eventHubSku capacity: 1 } properties: { isAutoInflateEnabled: false maximumThroughputUnits: 0 } } resource eventHub 'Microsoft.EventHub/namespaces/eventhubs@2021-11-01' = { parent: eventHubNamespace name: eventHubName properties: { messageRetentionInDays: 7 partitionCount: 1 } } resource apimIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' existing = { name: apimIdentityName scope: resourceGroup(apimResourceGroupName) } resource eventHubsDataSenderRoleDefinition 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = { name: '2b629674-e913-4c01-ae53-ef4638d8f975' // https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#azure-event-hubs-data-sender scope: tenant() } resource assignEventHubsDataSenderToApiManagement 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { name: guid(resourceGroup().id, eventHubNamespace.name, apimIdentity.id, 'assignEventHubsDataSenderToApiManagement') scope: eventHubNamespace properties: { description: 'Assign EventHubsDataSender role to API Management' principalId: apimIdentity.properties.principalId principalType: 'ServicePrincipal' roleDefinitionId: eventHubsDataSenderRoleDefinition.id } } output eventHubNamespaceName string = eventHubNamespace.name output eventHubName string = eventHub.name ================================================ FILE: scenarios/workload-genai/bicep/main.bicep ================================================ targetScope = 'subscription' @description('The name of the API Management service instance') param apiManagementServiceName string param location string = deployment().location param resourceSuffix string param apimResourceGroupName string param apimIdentityName string param vnetName string param privateEndpointSubnetid string param networkingResourceGroupName string @description('Enable sending usage and telemetry feedback to Microsoft.') param enableTelemetry bool = true var telemetryId = 'ab1e5729-7452-41b2-9fbb-945cc51d9cd0-${location}-apimsb-genai' var workloadResourceGroupName = 'rg-openai-${resourceSuffix}' var eventHubNamespaceName = 'eh-ns-${resourceSuffix}' var eventHubName = 'apim-utilization-reporting' var ptuAoaiDeploymentName = 'ptu-${resourceSuffix}' var paygoOneAoaiDeploymentName = 'paygo-one-${resourceSuffix}' var paygoTwoAoaiDeploymentName = 'paygo-two-${resourceSuffix}' resource workloadResourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { name: workloadResourceGroupName location: location } module eventHub 'eventhub/eventHub.bicep' = { name: 'eventHubDeploy' scope: resourceGroup(workloadResourceGroup.name) params: { eventHubName: eventHubName eventHubNamespaceName: eventHubNamespaceName location: location apimIdentityName: apimIdentityName apimResourceGroupName: apimResourceGroupName } } var openaiDnsZoneName = 'privatelink.openai.azure.com' module dnsZone '../../apim-baseline/bicep/shared/modules/dnszone.bicep' = { scope: resourceGroup(workloadResourceGroup.name) name: take('${replace(openaiDnsZoneName, '.', '-')}-deploy', 64) params: { vnetName: vnetName networkingResourceGroupName: networkingResourceGroupName domain: openaiDnsZoneName } } module simulatedPTUDeployment './openai/openai.bicep' = { name: 'simulatedPTUDeployment' scope: resourceGroup(workloadResourceGroup.name) params: { name: ptuAoaiDeploymentName location: location apimIdentityName: apimIdentityName apimResourceGroupName: apimResourceGroupName deploymentName: 'aoai' vnetName: vnetName privateEndpointSubnetid: privateEndpointSubnetid networkingResourceGroupName: networkingResourceGroupName } dependsOn: [ dnsZone ] } module simulatedPaygoOneDeployment './openai/openai.bicep' = { name: 'simulatedPaygoOneDeployment' scope: resourceGroup(workloadResourceGroup.name) params: { name: paygoOneAoaiDeploymentName location: location apimIdentityName: apimIdentityName apimResourceGroupName: apimResourceGroupName deploymentName: 'aoai' vnetName: vnetName privateEndpointSubnetid: privateEndpointSubnetid networkingResourceGroupName: networkingResourceGroupName } dependsOn: [ dnsZone ] } module simulatedPaygoTwoDeployment './openai/openai.bicep' = { name: 'simulatedPaygoTwoDeployment' scope: resourceGroup(workloadResourceGroup.name) params: { name: paygoTwoAoaiDeploymentName location: location apimIdentityName: apimIdentityName apimResourceGroupName: apimResourceGroupName deploymentName: 'aoai' vnetName: vnetName privateEndpointSubnetid: privateEndpointSubnetid networkingResourceGroupName: networkingResourceGroupName } dependsOn: [ dnsZone ] } module apiManagement 'apim-policies/apiManagement.bicep' = { name: 'apiManagementDeploy' scope: resourceGroup(apimResourceGroupName) params: { apiManagementServiceName: apiManagementServiceName ptuDeploymentOneBaseUrl: '${simulatedPTUDeployment.outputs.endpoint}openai' payAsYouGoDeploymentOneBaseUrl: '${simulatedPaygoOneDeployment.outputs.endpoint}openai' payAsYouGoDeploymentTwoBaseUrl: '${simulatedPaygoTwoDeployment.outputs.endpoint}openai' eventHubNamespaceName: eventHub.outputs.eventHubNamespaceName eventHubName: eventHub.outputs.eventHubName apimIdentityName: apimIdentityName } } @description('Microsoft telemetry deployment.') #disable-next-line no-deployments-resources resource telemetrydeployment 'Microsoft.Resources/deployments@2021-04-01' = if (enableTelemetry) { location: location name: telemetryId properties: { mode: 'Incremental' template: { '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' contentVersion: '1.0.0.0' resources: {} } } } output apiManagementName string = apiManagement.outputs.apiManagementServiceName output apiManagementAzureOpenAIProductSubscriptionKey string = apiManagement.outputs.apiManagementAzureOpenAIProductSubscriptionKey output apiManagementMultitenantProduct1SubscriptionKey string = apiManagement.outputs.apiManagementMultitenantProduct1SubscriptionKey output apiManagementMultitenantProduct2SubscriptionKey string = apiManagement.outputs.apiManagementMultitenantProduct2SubscriptionKey ================================================ FILE: scenarios/workload-genai/bicep/openai/openai.bicep ================================================ @description('Name of the resource.') param name string @description('Location to deploy the resource. Defaults to the location of the resource group.') param location string = resourceGroup().location @description('Tags for the resource.') param tags object = {} param deploymentName string = 'aoai' param apimIdentityName string param apimResourceGroupName string param vnetName string param privateEndpointSubnetid string param networkingResourceGroupName string @description('Whether to enable public network access. Defaults to Enabled.') @allowed([ 'Enabled' 'Disabled' ]) param publicNetworkAccess string = 'Disabled' @description('The model name to be deployed. The model name can be found in the OpenAI portal.') param modelName string = 'gpt-35-turbo' @description('The model version to be deployed. At the time of writing this is the latest version is eastus2.') param modelVersion string = '0613' resource cognitiveServices 'Microsoft.CognitiveServices/accounts@2023-10-01-preview' = { name: name location: location tags: tags kind: 'OpenAI' properties: { customSubDomainName: toLower(name) publicNetworkAccess: publicNetworkAccess } sku: { name: 'S0' } } resource cognitiveServicesOpenAIUser 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { name: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' // Cognitive Services OpenAI User scope: tenant() } resource apimIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { scope: resourceGroup(apimResourceGroupName) name: apimIdentityName } resource assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(cognitiveServices.id, apimIdentity.id, cognitiveServicesOpenAIUser.id) scope: cognitiveServices properties: { principalId: apimIdentity.properties.principalId roleDefinitionId: cognitiveServicesOpenAIUser.id principalType: 'ServicePrincipal' } } resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = { name: deploymentName parent: cognitiveServices sku: { name: 'Standard' capacity: 1 } properties: { raiPolicyName: 'Microsoft.Default' model: { format: 'OpenAI' name: modelName version: modelVersion } } } var privateEndpoint_openai_Name = 'pep-${name}' var openaiDnsZoneName = 'privatelink.openai.azure.com' module openaiPrivateEndpoint '../../../apim-baseline/bicep/shared/modules/privateendpoint.bicep' = { name: privateEndpoint_openai_Name params: { location: location privateEndpointName: privateEndpoint_openai_Name groupId: 'account' serviceResourceId: cognitiveServices.id vnetName: vnetName networkingResourceGroupName: networkingResourceGroupName subnetId: privateEndpointSubnetid domain: openaiDnsZoneName createDnsZone: false } } @description('ID for the deployed Cognitive Services resource.') output id string = cognitiveServices.id @description('Name for the deployed Cognitive Services resource.') output name string = cognitiveServices.name @description('Endpoint for the deployed Cognitive Services resource.') output endpoint string = cognitiveServices.properties.endpoint @description('Host for the deployed Cognitive Services resource.') output host string = split(cognitiveServices.properties.endpoint, '/')[2] ================================================ FILE: scenarios/workload-genai/policies/fragments/load-balancing/README.md ================================================ # Load balancing across PAYG, PTU instances ## Capability API Management supports the following load balancing options for backend pools: - *Round-robin:* By default, requests are distributed evenly across the backends in the pool. - *Weighted*: Weights are assigned to the backends in the pool, and requests are distributed across the backends based on the relative weight assigned to each backend. Use this option for scenarios such as conducting a blue-green deployment. - *Priority-based:* Backends are organized in priority groups, and requests are sent to the backends in order of the priority groups. Within a priority group, requests are distributed either evenly across the backends, or (if assigned) according to the relative weight assigned to each backend. ## Examples ### Managing spikes with PAYG The priority based load balancing policy can be used to manage spikes in traffic by routing traffic to PAYG endpoints when a PTU is out of capacity. ================================================ FILE: scenarios/workload-genai/policies/fragments/load-balancing/simple-priority-weighted.xml ================================================ @("Bearer " + (string)context.Variables["msi-access-token"]) ================================================ FILE: scenarios/workload-genai/policies/fragments/manage-spikes-with-payg/README.md ================================================ # Managing spike across PTU instances using PAYG deployments. ## Capability In this capability, the traffic is routed to the PTU1 instance as the primary backend. When the PTU1 instance returns a 429 Retry response the request is re-submitted to the PAYG1 instance. ## How the policy works - This capability leverages the APIM [`retry` policy](https://learn.microsoft.com/en-us/azure/api-management/retry-policy) - The segment in the retry policy will execute **at least once** and when the response is null (request entering first time into the retry segment) then it will be routed to the PTU instance. - If the PTU instance responds back with 429, then the request will be routed to the PAYG instance. ================================================ FILE: scenarios/workload-genai/policies/fragments/manage-spikes-with-payg/retry-with-payg.xml ================================================ @("Bearer " + (string)context.Variables["msi-access-token"]) @("Bearer " + (string)context.Variables["msi-access-token"]) @((string)context.Variables["body"]) ================================================ FILE: scenarios/workload-genai/policies/fragments/rate-limiting/README.md ================================================ # Rate limiting using Tokens consumed per request In Azure OpenAI, the rate limiting policy is based on the number of tokens consumed by the requests. In this example, there are samples of simple rate limiting by tokens and adaptive rate limiting scenarios. ## Rate limiting by tokens Rate limiting by tokens is implemented in 2 ways here. One using `azure-openai-token-limit` and the other using `rate-limit-by-key` 1. Policy reference: [`rate-limiting-by-tokens.xml`](./rate-limiting-by-tokens.xml) In MSFT build 2024, a new policy to rate limit by tokens for both streaming and non-streaming Azure OpenAI endpoints was launched. [This policy](https://learn.microsoft.com/en-us/azure/api-management/azure-openai-token-limit-policy) allows you to set rate limits based on the number of tokens consumed by the requests. 2. Policy reference: [`rate-limiting-workaround.xml`](./rate-limiting-workaround.xml) In scenarios, where the new rate limiting policy is not available, or if you are interacting with the non AOAI endpoints, it's worth considering the existing rate limiting policy in APIM. **Example scenarios** - Rate limiting a DallE endpoint with the number of images. The request to the DallE endpoint, will contain the number of images that needs to be returned in the response. This information can be parsed and can be used to increment the counter. - Rate limiting a non Azure OpenAI endpoint, where the response structure contains the token usage but in a different format. **Caveats** - This policy only applies to non-streaming requests. - It operates reactively, meaning it doesn't preemptively calculate tokens but instead waits for requests to breach the rate limit before blocking subsequent requests. ## Adaptive rate limiting Policy reference: [`adaptive-rate-limiting.xml`](./adaptive-rate-limiting.xml) In this setup, multiple services have their own rate limits. They start with default limits but can increase them dynamically within a set maximum if there's spare capacity due to low usage by other services. **Explanation** The rate-limit-by-key policy in Azure API Management (APIM) is a powerful tool for controlling access to your APIs based on the number of tokens consumed. Here's a step-by-step breakdown of how it operates: 1. **Token Consumption**: When a client makes a request to your API, the response includes information about the tokens consumed by that specific request. These tokens represent the resources utilized by the request, such as data transfer or processing. 2. **Incrementing Rate Limit Counters**: The policy extracts the token consumption data from the response and increments the corresponding rate limit counters. These counters track the usage of resources and enforce the defined rate limits. 3. **Global Rate Limit Counter**: In this example, there's a global rate limit counter set to the maximum rate limit initially. With each request, this counter decreases based on the tokens consumed. It resets at regular intervals, typically every 60 seconds, ensuring that the rate limits are enforced consistently over time. 4. **Dynamic Local Counters**: Alongside the global counter, there are dynamic local counters (triggered for specific request if the conditions are met) with different default rate limits. These counters can be adjusted based on the availability of the global rate limit counter. If there's spare capacity due to low usage by other services, these local counters can increase within a set maximum threshold, allowing services to temporarily access more resources. ================================================ FILE: scenarios/workload-genai/policies/fragments/rate-limiting/adaptive-rate-limiting.xml ================================================ 0) { defaultHigherLimit += (int)(globalRemainingTokens * 0.1); } return (int)defaultHigherLimit; }" /> 0) { defaultLowerRateLimit += (int)(globalRemainingTokens * 0.1); } return defaultLowerRateLimit; }" /> ================================================ FILE: scenarios/workload-genai/policies/fragments/rate-limiting/rate-limiting-by-tokens.xml ================================================ ================================================ FILE: scenarios/workload-genai/policies/fragments/rate-limiting/rate-limiting-workaround.xml ================================================ ())" remaining-calls-variable-name="globalRemainingTokens" remaining-calls-header-name="x-apim-global-remaining-tokens"/> ================================================ FILE: scenarios/workload-genai/policies/fragments/usage-tracking/README.md ================================================ # Usage tracking of tokens ## Capability In this setup, you can track the usage of your APIs by sending token usage data to Application Insights or Azure Event Hub. By adding custom dimension of SubscriptionId, TokenUsage, OperationName, RequestId in the Application Insights (or Event Hub), you can track the token usage by specific dimensions. ## Using Application Insights In MSFT build 2024, a new policy to track the token usage as metric was launched. [This policy](https://learn.microsoft.com/en-us/azure/api-management/azure-openai-emit-token-metric-policy) allows APIM to log token count metrics (Total Tokens, Prompt Tokens and Completion Tokens) to the customer's Azure Application Insights metrics tied to the customer's APIM resource. A KQL Query to list the token consumption by the subscription id is as follows: ```kql customMetrics | extend subId = tostring(parse_json(customDimensions).SubscriptionId) | summarize totalValueSum = sum(valueSum) by name, subId ``` This policy supports streaming endpoints in Azure OpenAI. ## Using Azure Event Hub ### Azure EventHub over Custom Metrics to Azure Monitor It is also possible to track the token consumption by sending the token count as a metrics to the Azure monitor. With Azure EventHub adds the following advantages: - It is possible to track some additional context (in form of text) other than just numbers like in metrics. - The event streams will be near real time, in comparison to Azure monitor. - Varied opportunities to consume the data from EventHub for further processing, like Azure Stream Analytics, Azure Functions, Logic Apps, etc. ### How the policy works - Azure OpenAI response will contain the token usage data. This policy extracts the token usage data from the response and sends it to Azure Event Hub. - This policy fragment needs to be included in the `outbound` section of the APIM policy. ### Caveats - This policy only applies to non-streaming requests. ================================================ FILE: scenarios/workload-genai/policies/fragments/usage-tracking/usage-tracking-with-appinsights.xml ================================================ ================================================ FILE: scenarios/workload-genai/policies/fragments/usage-tracking/usage-tracking-with-eventhub.xml ================================================ @{ return new JObject( new JProperty("Type", "Message"), new JProperty("EventTime", DateTime.UtcNow.ToString()), new JProperty("ServiceName", context.Deployment.ServiceName), new JProperty("GatewayId", context.Deployment.Gateway.Id), new JProperty("RequestId", context.RequestId), new JProperty("RequestIp", context.Request.IpAddress), new JProperty("OperationName", context.Operation.Name), new JProperty("SubscriptionId", context.Subscription.Id), new JProperty("TotalTokens", context.Response.Body.As<JObject>(preserveContent: true).SelectToken("usage.total_tokens").ToString()) ).ToString(); } ================================================ FILE: scenarios/workload-genai/policies/genai-policy.xml ================================================ ================================================ FILE: scenarios/workload-genai/policies/multi-tenancy/README.md ================================================ # Multi-Tenancy using Azure API Management Customers may sometimes would also like to have a multi-tenancy model on top of their backend APIs. This is a typical requirement for customers/businesses operating in SaaS-based models. Multi-tenancy in such scenarios is typically defined using following two business concepts: 1. **Tiers**: Tiers govern the _quality of service_ exposed to the users based on their pricing model. For instance, a _Freemium_ tier can be thought of as for consumer groups who would like to explore the service at no cost thus having very limited quota and rate limiting, likewise a _Premium_ tier can be defined for consumers who would like to have the most premium-grade service experience with the maximum possible rate limiting and quota. 2. **Entitlements**: Apart from _tiers_, businesses would also like to define _entitlements_ ,which means _giving access of only selected APIs_ for a particular consumer group. For instance, access to only chat based APIs for consumer A or only image APIs for consumer B. ## Initial Approach An initial approach can be to define separate APIs for different customers based on their _tiers_ & _entitlement_ combinations by defining the policies at the API level.The following image describes this approach - ![Rudimentary Solution Approach](../../../../docs/images/multi-tenancy-without-products.png) ### Downsides As we can clearly observe, this solution results in a lot of redundancy of APIs and API policies, overall resulting in a very convoluted design. The API policy code is also now bloated with unnecessary responsibilities which does not fall under the scope of API(for e.g., with the above design, any new policy we want to include has to be defined as part of the API policies). Further, it's also hard to define the entitlements using this model. ## Solution Approach A better and effective solution can be built by leveraging the concept of APIM [Products](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-add-products?tabs=azure-portal&pivots=interactive), helping us to cater to our "_entitlement_" requirement by grouping APIs related to that specific entitlement in a logical container and cater to our requirement of "_tier_" by leveraging Product's policies for the respective tier (like quota, rate limiting along with the respective backend model for e.g.: either a PAYG or PTU). Lastly, by defining [subscriptions](https://learn.microsoft.com/en-us/azure/api-management/api-management-subscriptions) at the Product level and giving access of only the Product's subscriptions to the end-user group, the users can only interact with the service via the specific Product's subscription. Following design demonstrates this approach further - ![Solution Approach using Products](../../../../docs/images/multi-tenancy-using-products.png) Product policy essentially here is helping us to define our "tenant" specific policies. ### Benefits This solution not only helps to cater to the multi-tenancy requirement in an effective manner but also makes the overall design modular and extensible by having the capability to define n-number of products and APIs and their different combinations with clear separation of concerns and adherence to the DRY(Do not Repeat Yourself) principle. _Note: As this a general pattern, this solution is not only limited to the GenAI backend but can be used with any general backend as well._ ### References Following blog post further describes this scenario in detail - https://devblogs.microsoft.com/ise/multitenant-genai-gateway-using-apim/ ## Products and Policies To summarize: - Products: Acts as logical container of APIs for a specific consumer group (e.g., Chat APIs or Embedding APIs). - Product Policies: For defining tenant policies (e.g., rate limits, quotas). And as part of this capability's example scenario, we will apply a new _quota policy_ at the product level, such that if the number of requests to APIM via that Product's subscription exceed as per the defined "calls" attribute value, then the product policy will accordingly block the subsequent requests from that subscription until the quota is refreshed based on the defined "renewal-period" and "counter-key" attributes. For this setup, we create two sample products(`multi-tenant-product1`,`multi-tenant-product2`) with different counter keys(`-mt-product1`,`-mt-product2`) respectively and with the following policies : - [`multi-tenant-product1-policy.xml`](multi-tenant-product1-policy.xml) - [`multi-tenant-product2-policy.xml`](multi-tenant-product2-policy.xml) The Product Policy can thus be extended with any number of higher-level policies (for e.g., defining quota or rate limits) and any attributes (for e.g., setting the name of the backend pool) as per the respective _tenant's_ requirement. ## Note This capability/pattern is over the top of the existing core capabilities, which can be played around & tested separately and hence does not impact the existing setup. However, ifmulti-tenancy capability is not needed i.e. these resources created as part of our deployment, then the respective code blocks can be commented from the bicep and terraform scripts. ================================================ FILE: scenarios/workload-genai/policies/multi-tenancy/multi-tenant-product1-policy.xml ================================================ ================================================ FILE: scenarios/workload-genai/policies/multi-tenancy/multi-tenant-product2-policy.xml ================================================ ================================================ FILE: scenarios/workload-genai/terraform/README.md ================================================ # Scenario 3: Azure API Management - Gen AI Backend [Terraform] This is the Terraform-based deployment guide for [Scenario 3: Azure API Management - Gen AI Backend](../README.md). ## Prerequisites This scenario requires the completion of the [Azure API Management - Secure Baseline](../../apim-baseline/README.md) scenario ([using the terraform-based deployment](../../apim-baseline/terraform/README.md)). ## Steps Run the following command to deploy the scenarios ```bash ./scripts/terraform/deploy-workload-genai.sh ``` Test the hello api using the generated command from the output ## Troubleshooting If you see the message `-bash: ./deploy-workload-genai.sh: /bin/bash^M: bad interpreter: No such file or directory` when running the script, you can fix this by running the following command: ```bash sed -i -e 's/\r$//' deploy-workload-genai.sh ``` ================================================ FILE: scenarios/workload-genai/terraform/backend.tf.sample ================================================ # terraform { # backend "azurerm" { # # ---------------------- # # Will be passing in these arguments via CLI as the state file \ # # is now being overwritten via local testing environments # # > https://developer.hashicorp.com/terraform/language/settings/backends/configuration#command-line-key-value-pairs # # ---------------------- # # e.g: terraform init \ # # -backend-config="resource_group_name=rg-tfstate-auseast" \ # # -backend-config="storage_account_name=tfstateauseaststorage" \ # # -backend-config="container_name=apimlza" \ # # -backend-config="key=terraform-apimlza-dev-v2.tfstate" # # ---------------------- # # resource_group_name = "rg-tfstate-auseast" # # storage_account_name = "tfstateauseaststorage" # # container_name = "apimlza" # # key = "terraform-apimlza-dev-v6.tfstate" # } # } ================================================ FILE: scenarios/workload-genai/terraform/main.tf ================================================ locals { resourceSuffix = "${var.workloadName}-${var.environment}-${var.location}-${var.identifier}" networkingResourceGroupName = "rg-networking-${local.resourceSuffix}" apimResourceGroupName = "rg-apim-${local.resourceSuffix}" apimName = "apim-${local.resourceSuffix}" openaiResourceGroupName = "rg-openai-${local.resourceSuffix}" apim_cs_vnet_name = "vnet-apim-cs-${local.resourceSuffix}" deploy_subnet_name = "snet-deploy-${local.resourceSuffix}" private_endpoint_subnet_name = "snet-prep-${local.resourceSuffix}" eventHubNamespaceName = "eh-ns-${local.resourceSuffix}" apimIdentityName = "identity-${local.apimName}" } data "azurerm_client_config" "current" { } data "azurerm_api_management" "apim" { name = local.apimName resource_group_name = local.apimResourceGroupName } data "azurerm_resource_group" "networking" { name = local.networkingResourceGroupName } data "azurerm_resource_group" "apim" { name = local.apimResourceGroupName } data "azurerm_virtual_network" "apim_cs_vnet" { name = local.apim_cs_vnet_name resource_group_name = local.networkingResourceGroupName } data "azurerm_subnet" "private_endpoint_subnet" { name = local.private_endpoint_subnet_name resource_group_name = local.networkingResourceGroupName virtual_network_name = local.apim_cs_vnet_name } data "azurerm_subnet" "deploy_subnet" { name = local.deploy_subnet_name resource_group_name = local.networkingResourceGroupName virtual_network_name = local.apim_cs_vnet_name } data "azurerm_user_assigned_identity" "apimIdentity" { name = local.apimIdentityName resource_group_name = local.apimResourceGroupName } resource "azurerm_resource_group" "rg" { name = local.openaiResourceGroupName location = var.location } module "openai_private_dns_zone" { source = "./modules/private_dns_zone" name = "privatelink.openai.azure.com" resource_group_name = azurerm_resource_group.rg.name virtual_networks_to_link_id = data.azurerm_virtual_network.apim_cs_vnet.id } module "openai_simulatedPTUDeployment_private_endpoint" { source = "./modules/private_endpoint" name = "pep-${module.simulatedPTUDeployment.name}" location = var.location resource_group_name = azurerm_resource_group.rg.name subnet_id = data.azurerm_subnet.private_endpoint_subnet.id private_connection_resource_id = module.simulatedPTUDeployment.id is_manual_connection = false subresource_name = "account" private_dns_zone_group_name = "OpenAiPrivateDnsZoneGroup" private_dns_zone_group_ids = [module.openai_private_dns_zone.id] } module "openai_simulatedPaygoOneDeployment_private_endpoint" { source = "./modules/private_endpoint" name = "pep-${module.simulatedPaygoOneDeployment.name}" location = var.location resource_group_name = azurerm_resource_group.rg.name subnet_id = data.azurerm_subnet.private_endpoint_subnet.id private_connection_resource_id = module.simulatedPaygoOneDeployment.id is_manual_connection = false subresource_name = "account" private_dns_zone_group_name = "OpenAiPrivateDnsZoneGroup" private_dns_zone_group_ids = [module.openai_private_dns_zone.id] } module "openai_simulatedPaygoTwoDeployment_private_endpoint" { source = "./modules/private_endpoint" name = "pep-${module.simulatedPaygoTwoDeployment.name}" location = var.location resource_group_name = azurerm_resource_group.rg.name subnet_id = data.azurerm_subnet.private_endpoint_subnet.id private_connection_resource_id = module.simulatedPaygoTwoDeployment.id is_manual_connection = false subresource_name = "account" private_dns_zone_group_name = "OpenAiPrivateDnsZoneGroup" private_dns_zone_group_ids = [module.openai_private_dns_zone.id] } module "simulatedPTUDeployment" { source = "./modules/openai" name = "ptu-${local.resourceSuffix}" location = var.location resource_group_name = azurerm_resource_group.rg.name sku_name = var.openai_sku_name deployments = var.openai_deployments custom_subdomain_name = lower("${local.resourceSuffix}${var.openai_name}-ptu") public_network_access_enabled = var.openai_public_network_access_enabled apimIdentityName = data.azurerm_user_assigned_identity.apimIdentity.name apimResourceGroupName = local.apimResourceGroupName } module "simulatedPaygoOneDeployment" { source = "./modules/openai" name = "paygo-one-${local.resourceSuffix}" location = var.location resource_group_name = azurerm_resource_group.rg.name sku_name = var.openai_sku_name deployments = var.openai_deployments custom_subdomain_name = lower("${local.resourceSuffix}${var.openai_name}-paygo-one") public_network_access_enabled = var.openai_public_network_access_enabled apimIdentityName = data.azurerm_user_assigned_identity.apimIdentity.name apimResourceGroupName = local.apimResourceGroupName } module "simulatedPaygoTwoDeployment" { source = "./modules/openai" name = "paygo-two-${local.resourceSuffix}" location = var.location resource_group_name = azurerm_resource_group.rg.name sku_name = var.openai_sku_name deployments = var.openai_deployments custom_subdomain_name = lower("${local.resourceSuffix}${var.openai_name}-paygo-two") public_network_access_enabled = var.openai_public_network_access_enabled apimIdentityName = data.azurerm_user_assigned_identity.apimIdentity.name apimResourceGroupName = local.apimResourceGroupName } module "eventHub" { source = "./modules/eventhub" eventHubName = var.eventHubName eventHubNamespaceName = local.eventHubNamespaceName location = var.location apimIdentityName = data.azurerm_user_assigned_identity.apimIdentity.name apimResourceGroupName = data.azurerm_resource_group.apim.name openaiResourceGroupName = azurerm_resource_group.rg.name } module "apiManagement" { source = "./modules/apim_policies" location = var.location openaiResourceGroupName = local.openaiResourceGroupName resourceGroupName = local.apimResourceGroupName apiManagementServiceName = local.apimName ptuDeploymentOneBaseUrl = "${module.simulatedPTUDeployment.endpoint}openai" payAsYouGoDeploymentOneBaseUrl = "${module.simulatedPaygoOneDeployment.endpoint}openai" payAsYouGoDeploymentTwoBaseUrl = "${module.simulatedPaygoTwoDeployment.endpoint}openai" eventHubNamespaceName = module.eventHub.eventHubNamespaceName eventHubName = module.eventHub.eventHubName apimIdentityName = data.azurerm_user_assigned_identity.apimIdentity.name depends_on = [ module.eventHub ] } ================================================ FILE: scenarios/workload-genai/terraform/modules/apim_policies/api-specs/openapi-spec.json ================================================ { "openapi": "3.0.0", "info": { "title": "Azure OpenAI Service API", "description": "Azure OpenAI APIs for completions and search", "version": "2024-02-01" }, "servers": [ { "url": "https://change-this.com/openai" } ], "security": [ { "bearer": [ "api.read" ] }, { "apiKey": [] } ], "paths": { "/deployments/{deployment-id}/completions": { "post": { "summary": "Creates a completion for the provided prompt, parameters and chosen model.", "operationId": "Completions_Create", "parameters": [ { "in": "path", "name": "deployment-id", "required": true, "schema": { "type": "string", "example": "davinci", "description": "Deployment id of the model which was deployed." } }, { "in": "query", "name": "api-version", "required": true, "schema": { "type": "string", "example": "2024-02-01", "description": "api version" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "prompt": { "description": "The prompt(s) to generate completions for, encoded as a string or array of strings.\nNote that <|endoftext|> is the document separator that the model sees during training, so if a prompt is not specified the model will generate as if from the beginning of a new document. Maximum allowed size of string list is 2048.", "oneOf": [ { "type": "string", "default": "", "example": "This is a test.", "nullable": true }, { "type": "array", "items": { "type": "string", "default": "", "example": "This is a test.", "nullable": false }, "description": "Array size minimum of 1 and maximum of 2048" } ] }, "max_tokens": { "description": "The token count of your prompt plus max_tokens cannot exceed the model's context length. Most models have a context length of 2048 tokens (except for the newest models, which support 4096). Has minimum of 0.", "type": "integer", "default": 16, "example": 16, "nullable": true }, "temperature": { "description": "What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer.\nWe generally recommend altering this or top_p but not both.", "type": "number", "default": 1, "example": 1, "nullable": true }, "top_p": { "description": "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.\nWe generally recommend altering this or temperature but not both.", "type": "number", "default": 1, "example": 1, "nullable": true }, "logit_bias": { "description": "Defaults to null. Modify the likelihood of specified tokens appearing in the completion. Accepts a json object that maps tokens (specified by their token ID in the GPT tokenizer) to an associated bias value from -100 to 100. You can use this tokenizer tool (which works for both GPT-2 and GPT-3) to convert text to token IDs. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token. As an example, you can pass {\"50256\" : -100} to prevent the <|endoftext|> token from being generated.", "type": "object", "nullable": false }, "user": { "description": "A unique identifier representing your end-user, which can help monitoring and detecting abuse", "type": "string", "nullable": false }, "n": { "description": "How many completions to generate for each prompt. Minimum of 1 and maximum of 128 allowed.\nNote: Because this parameter generates many completions, it can quickly consume your token quota. Use carefully and ensure that you have reasonable settings for max_tokens and stop.", "type": "integer", "default": 1, "example": 1, "nullable": true }, "stream": { "description": "Whether to stream back partial progress. If set, tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a data: [DONE] message.", "type": "boolean", "nullable": true, "default": false }, "logprobs": { "description": "Include the log probabilities on the logprobs most likely tokens, as well the chosen tokens. For example, if logprobs is 5, the API will return a list of the 5 most likely tokens. The API will always return the logprob of the sampled token, so there may be up to logprobs+1 elements in the response.\nMinimum of 0 and maximum of 5 allowed.", "type": "integer", "default": null, "nullable": true }, "suffix": { "type": "string", "nullable": true, "description": "The suffix that comes after a completion of inserted text." }, "echo": { "description": "Echo back the prompt in addition to the completion", "type": "boolean", "default": false, "nullable": true }, "stop": { "description": "Up to 4 sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence.", "oneOf": [ { "type": "string", "default": "<|endoftext|>", "example": "\n", "nullable": true }, { "type": "array", "items": { "type": "string", "example": "\n", "nullable": false }, "description": "Array minimum size of 1 and maximum of 4" } ] }, "completion_config": { "type": "string", "nullable": true }, "presence_penalty": { "description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.", "type": "number", "default": 0 }, "frequency_penalty": { "description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.", "type": "number", "default": 0 }, "best_of": { "description": "Generates best_of completions server-side and returns the \"best\" (the one with the highest log probability per token). Results cannot be streamed.\nWhen used with n, best_of controls the number of candidate completions and n specifies how many to return - best_of must be greater than n.\nNote: Because this parameter generates many completions, it can quickly consume your token quota. Use carefully and ensure that you have reasonable settings for max_tokens and stop. Has maximum value of 128.", "type": "integer" } } }, "example": { "prompt": "Negate the following sentence.The price for bubblegum increased on thursday.\n\n Negated Sentence:", "max_tokens": 50 } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "string" }, "object": { "type": "string" }, "created": { "type": "integer" }, "model": { "type": "string" }, "prompt_filter_results": { "$ref": "#/components/schemas/promptFilterResults" }, "choices": { "type": "array", "items": { "type": "object", "properties": { "text": { "type": "string" }, "index": { "type": "integer" }, "logprobs": { "type": "object", "properties": { "tokens": { "type": "array", "items": { "type": "string" } }, "token_logprobs": { "type": "array", "items": { "type": "number" } }, "top_logprobs": { "type": "array", "items": { "type": "object", "additionalProperties": { "type": "number" } } }, "text_offset": { "type": "array", "items": { "type": "integer" } } }, "nullable": true }, "finish_reason": { "type": "string" }, "content_filter_results": { "$ref": "#/components/schemas/contentFilterChoiceResults" } } } }, "usage": { "type": "object", "properties": { "completion_tokens": { "type": "number", "format": "int32" }, "prompt_tokens": { "type": "number", "format": "int32" }, "total_tokens": { "type": "number", "format": "int32" } }, "required": [ "prompt_tokens", "total_tokens", "completion_tokens" ] } }, "required": [ "id", "object", "created", "model", "choices" ] }, "example": { "model": "davinci", "object": "text_completion", "id": "cmpl-4509KAos68kxOqpE2uYGw81j6m7uo", "created": 1637097562, "choices": [ { "index": 0, "text": "The price for bubblegum decreased on thursday.", "logprobs": null, "finish_reason": "stop" } ] } } }, "headers": { "apim-request-id": { "description": "Request ID for troubleshooting purposes", "schema": { "type": "string" } } } }, "default": { "description": "Service unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/errorResponse" } } }, "headers": { "apim-request-id": { "description": "Request ID for troubleshooting purposes", "schema": { "type": "string" } } } } }, "x-ms-examples": { "Create a completion.": { "$ref": "./examples/completions.json" } } } }, "/deployments/{deployment-id}/embeddings": { "post": { "summary": "Get a vector representation of a given input that can be easily consumed by machine learning models and algorithms.", "operationId": "embeddings_create", "parameters": [ { "in": "path", "name": "deployment-id", "required": true, "schema": { "type": "string", "example": "ada-search-index-v1" }, "description": "The deployment id of the model which was deployed." }, { "in": "query", "name": "api-version", "required": true, "schema": { "type": "string", "example": "2024-02-01", "description": "api version" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true, "properties": { "input": { "description": "Input text to get embeddings for, encoded as a string. To get embeddings for multiple inputs in a single request, pass an array of strings. Each input must not exceed 2048 tokens in length.\nUnless you are embedding code, we suggest replacing newlines (\\n) in your input with a single space, as we have observed inferior results when newlines are present.", "oneOf": [ { "type": "string", "default": "", "example": "This is a test.", "nullable": true }, { "type": "array", "minItems": 1, "maxItems": 2048, "items": { "type": "string", "minLength": 1, "example": "This is a test.", "nullable": false } } ] }, "user": { "description": "A unique identifier representing your end-user, which can help monitoring and detecting abuse.", "type": "string", "nullable": false }, "input_type": { "description": "input type of embedding search to use", "type": "string", "example": "query" } }, "required": [ "input" ] } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "object": { "type": "string" }, "model": { "type": "string" }, "data": { "type": "array", "items": { "type": "object", "properties": { "index": { "type": "integer" }, "object": { "type": "string" }, "embedding": { "type": "array", "items": { "type": "number" } } }, "required": [ "index", "object", "embedding" ] } }, "usage": { "type": "object", "properties": { "prompt_tokens": { "type": "integer" }, "total_tokens": { "type": "integer" } }, "required": [ "prompt_tokens", "total_tokens" ] } }, "required": [ "object", "model", "data", "usage" ] } } } } }, "x-ms-examples": { "Create a embeddings.": { "$ref": "./examples/embeddings.json" } } } }, "/deployments/{deployment-id}/chat/completions": { "post": { "summary": "Creates a completion for the chat message", "operationId": "ChatCompletions_Create", "parameters": [ { "in": "path", "name": "deployment-id", "required": true, "schema": { "type": "string", "description": "Deployment id of the model which was deployed." } }, { "in": "query", "name": "api-version", "required": true, "schema": { "type": "string", "example": "2024-02-01", "description": "api version" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/createChatCompletionRequest" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/createChatCompletionResponse" } } }, "headers": { "apim-request-id": { "description": "Request ID for troubleshooting purposes", "schema": { "type": "string" } } } }, "default": { "description": "Service unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/errorResponse" } } }, "headers": { "apim-request-id": { "description": "Request ID for troubleshooting purposes", "schema": { "type": "string" } } } } }, "x-ms-examples": { "Create a chat completion.": { "$ref": "./examples/chat_completions.json" }, "Creates a completion based on Azure Search data and system-assigned managed identity.": { "$ref": "./examples/chat_completions_azure_search_minimum.json" }, "Creates a completion based on Azure Search vector data, previous assistant message and user-assigned managed identity.": { "$ref": "./examples/chat_completions_azure_search_advanced.json" }, "Creates a completion for the provided Azure Cosmos DB.": { "$ref": "./examples/chat_completions_cosmos_db.json" } } } } }, "components": { "schemas": { "errorResponse": { "type": "object", "properties": { "error": { "$ref": "#/components/schemas/error" } } }, "errorBase": { "type": "object", "properties": { "code": { "type": "string" }, "message": { "type": "string" } } }, "error": { "type": "object", "allOf": [ { "$ref": "#/components/schemas/errorBase" } ], "properties": { "param": { "type": "string" }, "type": { "type": "string" }, "inner_error": { "$ref": "#/components/schemas/innerError" } } }, "innerError": { "description": "Inner error with additional details.", "type": "object", "properties": { "code": { "$ref": "#/components/schemas/innerErrorCode" }, "content_filter_results": { "$ref": "#/components/schemas/contentFilterPromptResults" } } }, "innerErrorCode": { "description": "Error codes for the inner error object.", "enum": [ "ResponsibleAIPolicyViolation" ], "type": "string", "x-ms-enum": { "name": "InnerErrorCode", "modelAsString": true, "values": [ { "value": "ResponsibleAIPolicyViolation", "description": "The prompt violated one of more content filter rules." } ] } }, "dalleErrorResponse": { "type": "object", "properties": { "error": { "$ref": "#/components/schemas/dalleError" } } }, "dalleError": { "type": "object", "allOf": [ { "$ref": "#/components/schemas/errorBase" } ], "properties": { "param": { "type": "string" }, "type": { "type": "string" }, "inner_error": { "$ref": "#/components/schemas/dalleInnerError" } } }, "dalleInnerError": { "description": "Inner error with additional details.", "type": "object", "properties": { "code": { "$ref": "#/components/schemas/innerErrorCode" }, "content_filter_results": { "$ref": "#/components/schemas/dalleFilterResults" }, "revised_prompt": { "type": "string", "description": "The prompt that was used to generate the image, if there was any revision to the prompt." } } }, "contentFilterResultBase": { "type": "object", "properties": { "filtered": { "type": "boolean" } }, "required": [ "filtered" ] }, "contentFilterSeverityResult": { "type": "object", "allOf": [ { "$ref": "#/components/schemas/contentFilterResultBase" }, { "properties": { "severity": { "type": "string", "enum": [ "safe", "low", "medium", "high" ], "x-ms-enum": { "name": "ContentFilterSeverity", "modelAsString": true, "values": [ { "value": "safe", "description": "General content or related content in generic or non-harmful contexts." }, { "value": "low", "description": "Harmful content at a low intensity and risk level." }, { "value": "medium", "description": "Harmful content at a medium intensity and risk level." }, { "value": "high", "description": "Harmful content at a high intensity and risk level." } ] } } } } ], "required": [ "severity", "filtered" ] }, "contentFilterDetectedResult": { "type": "object", "allOf": [ { "$ref": "#/components/schemas/contentFilterResultBase" }, { "properties": { "detected": { "type": "boolean" } } } ], "required": [ "detected", "filtered" ] }, "contentFilterDetectedWithCitationResult": { "type": "object", "allOf": [ { "$ref": "#/components/schemas/contentFilterDetectedResult" }, { "properties": { "citation": { "type": "object", "properties": { "URL": { "type": "string" }, "license": { "type": "string" } } } } } ], "required": [ "detected", "filtered" ] }, "contentFilterResultsBase": { "type": "object", "description": "Information about the content filtering results.", "properties": { "sexual": { "$ref": "#/components/schemas/contentFilterSeverityResult" }, "violence": { "$ref": "#/components/schemas/contentFilterSeverityResult" }, "hate": { "$ref": "#/components/schemas/contentFilterSeverityResult" }, "self_harm": { "$ref": "#/components/schemas/contentFilterSeverityResult" }, "profanity": { "$ref": "#/components/schemas/contentFilterDetectedResult" }, "error": { "$ref": "#/components/schemas/errorBase" } } }, "contentFilterPromptResults": { "type": "object", "description": "Information about the content filtering category (hate, sexual, violence, self_harm), if it has been detected, as well as the severity level (very_low, low, medium, high-scale that determines the intensity and risk level of harmful content) and if it has been filtered or not. Information about jailbreak content and profanity, if it has been detected, and if it has been filtered or not. And information about customer block list, if it has been filtered and its id.", "allOf": [ { "$ref": "#/components/schemas/contentFilterResultsBase" }, { "properties": { "jailbreak": { "$ref": "#/components/schemas/contentFilterDetectedResult" } } } ] }, "contentFilterChoiceResults": { "type": "object", "description": "Information about the content filtering category (hate, sexual, violence, self_harm), if it has been detected, as well as the severity level (very_low, low, medium, high-scale that determines the intensity and risk level of harmful content) and if it has been filtered or not. Information about third party text and profanity, if it has been detected, and if it has been filtered or not. And information about customer block list, if it has been filtered and its id.", "allOf": [ { "$ref": "#/components/schemas/contentFilterResultsBase" }, { "properties": { "protected_material_text": { "$ref": "#/components/schemas/contentFilterDetectedResult" } } }, { "properties": { "protected_material_code": { "$ref": "#/components/schemas/contentFilterDetectedWithCitationResult" } } } ] }, "promptFilterResult": { "type": "object", "description": "Content filtering results for a single prompt in the request.", "properties": { "prompt_index": { "type": "integer" }, "content_filter_results": { "$ref": "#/components/schemas/contentFilterPromptResults" } } }, "promptFilterResults": { "type": "array", "description": "Content filtering results for zero or more prompts in the request. In a streaming request, results for different prompts may arrive at different times or in different orders.", "items": { "$ref": "#/components/schemas/promptFilterResult" } }, "dalleContentFilterResults": { "type": "object", "description": "Information about the content filtering results.", "properties": { "sexual": { "$ref": "#/components/schemas/contentFilterSeverityResult" }, "violence": { "$ref": "#/components/schemas/contentFilterSeverityResult" }, "hate": { "$ref": "#/components/schemas/contentFilterSeverityResult" }, "self_harm": { "$ref": "#/components/schemas/contentFilterSeverityResult" } } }, "dalleFilterResults": { "type": "object", "description": "Information about the content filtering category (hate, sexual, violence, self_harm), if it has been detected, as well as the severity level (very_low, low, medium, high-scale that determines the intensity and risk level of harmful content) and if it has been filtered or not. Information about jailbreak content and profanity, if it has been detected, and if it has been filtered or not. And information about customer block list, if it has been filtered and its id.", "allOf": [ { "$ref": "#/components/schemas/dalleContentFilterResults" }, { "properties": { "profanity": { "$ref": "#/components/schemas/contentFilterDetectedResult" }, "jailbreak": { "$ref": "#/components/schemas/contentFilterDetectedResult" } } } ] }, "chatCompletionsRequestCommon": { "type": "object", "properties": { "temperature": { "description": "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\nWe generally recommend altering this or `top_p` but not both.", "type": "number", "minimum": 0, "maximum": 2, "default": 1, "example": 1, "nullable": true }, "top_p": { "description": "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.\nWe generally recommend altering this or `temperature` but not both.", "type": "number", "minimum": 0, "maximum": 1, "default": 1, "example": 1, "nullable": true }, "stream": { "description": "If set, partial message deltas will be sent, like in ChatGPT. Tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a `data: [DONE]` message.", "type": "boolean", "nullable": true, "default": false }, "stop": { "description": "Up to 4 sequences where the API will stop generating further tokens.", "oneOf": [ { "type": "string", "nullable": true }, { "type": "array", "items": { "type": "string", "nullable": false }, "minItems": 1, "maxItems": 4, "description": "Array minimum size of 1 and maximum of 4" } ], "default": null }, "max_tokens": { "description": "The maximum number of tokens allowed for the generated answer. By default, the number of tokens the model can return will be (4096 - prompt tokens).", "type": "integer", "default": 4096 }, "presence_penalty": { "description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.", "type": "number", "default": 0, "minimum": -2, "maximum": 2 }, "frequency_penalty": { "description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.", "type": "number", "default": 0, "minimum": -2, "maximum": 2 }, "logit_bias": { "description": "Modify the likelihood of specified tokens appearing in the completion. Accepts a json object that maps tokens (specified by their token ID in the tokenizer) to an associated bias value from -100 to 100. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token.", "type": "object", "nullable": true }, "user": { "description": "A unique identifier representing your end-user, which can help Azure OpenAI to monitor and detect abuse.", "type": "string", "example": "user-1234", "nullable": false } } }, "createChatCompletionRequest": { "type": "object", "allOf": [ { "$ref": "#/components/schemas/chatCompletionsRequestCommon" }, { "properties": { "messages": { "description": "A list of messages comprising the conversation so far. [Example Python code](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_format_inputs_to_ChatGPT_models.ipynb).", "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/chatCompletionRequestMessage" } }, "data_sources": { "type": "array", "description": " The configuration entries for Azure OpenAI chat extensions that use them.\n This additional specification is only compatible with Azure OpenAI.", "items": { "$ref": "#/components/schemas/azureChatExtensionConfiguration" } }, "n": { "type": "integer", "minimum": 1, "maximum": 128, "default": 1, "example": 1, "nullable": true, "description": "How many chat completion choices to generate for each input message." }, "seed": { "type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807, "default": 0, "example": 1, "nullable": true, "description": "If specified, our system will make a best effort to sample deterministically, such that repeated requests with the same `seed` and parameters should return the same result.Determinism is not guaranteed, and you should refer to the `system_fingerprint` response parameter to monitor changes in the backend." }, "response_format": { "type": "object", "description": "An object specifying the format that the model must output. Used to enable JSON mode.", "properties": { "type": { "$ref": "#/components/schemas/chatCompletionResponseFormat" } } }, "tools": { "description": "A list of tools the model may call. Currently, only functions are supported as a tool. Use this to provide a list of functions the model may generate JSON inputs for.", "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/chatCompletionTool" } }, "tool_choice": { "$ref": "#/components/schemas/chatCompletionToolChoiceOption" }, "functions": { "description": "Deprecated in favor of `tools`. A list of functions the model may generate JSON inputs for.", "type": "array", "minItems": 1, "maxItems": 128, "items": { "$ref": "#/components/schemas/chatCompletionFunction" } }, "function_call": { "description": "Deprecated in favor of `tool_choice`. Controls how the model responds to function calls. \"none\" means the model does not call a function, and responds to the end-user. \"auto\" means the model can pick between an end-user or calling a function. Specifying a particular function via `{\"name\":\\ \"my_function\"}` forces the model to call that function. \"none\" is the default when no functions are present. \"auto\" is the default if functions are present.", "oneOf": [ { "type": "string", "enum": [ "none", "auto" ], "description": "`none` means the model will not call a function and instead generates a message. `auto` means the model can pick between generating a message or calling a function." }, { "type": "object", "description": "Specifying a particular function via `{\"name\": \"my_function\"}` forces the model to call that function.", "properties": { "name": { "type": "string", "description": "The name of the function to call." } }, "required": [ "name" ] } ] } } } ], "required": [ "messages" ] }, "chatCompletionResponseFormat": { "type": "string", "enum": [ "text", "json_object" ], "default": "text", "example": "json_object", "nullable": true, "description": "Setting to `json_object` enables JSON mode. This guarantees that the message the model generates is valid JSON.", "x-ms-enum": { "name": "ChatCompletionResponseFormat", "modelAsString": true, "values": [ { "value": "text", "description": "Response format is a plain text string." }, { "value": "json_object", "description": "Response format is a JSON object." } ] } }, "chatCompletionFunction": { "type": "object", "properties": { "name": { "type": "string", "description": "The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64." }, "description": { "type": "string", "description": "The description of what the function does." }, "parameters": { "$ref": "#/components/schemas/chatCompletionFunctionParameters" } }, "required": [ "name" ] }, "chatCompletionFunctionParameters": { "type": "object", "description": "The parameters the functions accepts, described as a JSON Schema object. See the [guide](/docs/guides/gpt/function-calling) for examples, and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format.", "additionalProperties": true }, "chatCompletionRequestMessage": { "type": "object", "properties": { "role": { "$ref": "#/components/schemas/chatCompletionRequestMessageRole" } }, "discriminator": { "propertyName": "role", "mapping": { "system": "#/components/schemas/chatCompletionRequestMessageSystem", "user": "#/components/schemas/chatCompletionRequestMessageUser", "assistant": "#/components/schemas/chatCompletionRequestMessageAssistant", "tool": "#/components/schemas/chatCompletionRequestMessageTool", "function": "#/components/schemas/chatCompletionRequestMessageFunction" } }, "required": [ "role" ] }, "chatCompletionRequestMessageRole": { "type": "string", "enum": [ "system", "user", "assistant", "tool", "function" ], "description": "The role of the messages author.", "x-ms-enum": { "name": "ChatCompletionRequestMessageRole", "modelAsString": true, "values": [ { "value": "system", "description": "The message author role is system." }, { "value": "user", "description": "The message author role is user." }, { "value": "assistant", "description": "The message author role is assistant." }, { "value": "tool", "description": "The message author role is tool." }, { "value": "function", "description": "Deprecated. The message author role is function." } ] } }, "chatCompletionRequestMessageSystem": { "allOf": [ { "$ref": "#/components/schemas/chatCompletionRequestMessage" }, { "type": "object", "properties": { "content": { "type": "string", "description": "The contents of the message.", "nullable": true } } } ], "required": [ "content" ] }, "chatCompletionRequestMessageUser": { "allOf": [ { "$ref": "#/components/schemas/chatCompletionRequestMessage" }, { "type": "object", "properties": { "content": { "oneOf": [ { "type": "string", "description": "The contents of the message." }, { "type": "array", "description": "An array of content parts with a defined type, each can be of type `text` or `image_url` when passing in images. You can pass multiple images by adding multiple `image_url` content parts. Image input is only supported when using the `gpt-4-visual-preview` model.", "minimum": 1, "items": { "$ref": "#/components/schemas/chatCompletionRequestMessageContentPart" } } ], "nullable": true } } } ], "required": [ "content" ] }, "chatCompletionRequestMessageContentPart": { "type": "object", "properties": { "type": { "$ref": "#/components/schemas/chatCompletionRequestMessageContentPartType" } }, "discriminator": { "propertyName": "type", "mapping": { "text": "#/components/schemas/chatCompletionRequestMessageContentPartText", "image_url": "#/components/schemas/chatCompletionRequestMessageContentPartImage" } }, "required": [ "type" ] }, "chatCompletionRequestMessageContentPartType": { "type": "string", "enum": [ "text", "image_url" ], "description": "The type of the content part.", "x-ms-enum": { "name": "ChatCompletionRequestMessageContentPartType", "modelAsString": true, "values": [ { "value": "text", "description": "The content part type is text." }, { "value": "image_url", "description": "The content part type is image_url." } ] } }, "chatCompletionRequestMessageContentPartText": { "allOf": [ { "$ref": "#/components/schemas/chatCompletionRequestMessageContentPart" }, { "type": "object", "properties": { "text": { "type": "string", "description": "The text content." } } } ], "required": [ "text" ] }, "chatCompletionRequestMessageContentPartImage": { "allOf": [ { "$ref": "#/components/schemas/chatCompletionRequestMessageContentPart" }, { "type": "object", "properties": { "url": { "type": "string", "description": "Either a URL of the image or the base64 encoded image data.", "format": "uri" }, "detail": { "$ref": "#/components/schemas/imageDetailLevel" } } } ], "required": [ "url" ] }, "imageDetailLevel": { "type": "string", "description": "Specifies the detail level of the image.", "enum": [ "auto", "low", "high" ], "default": "auto", "x-ms-enum": { "name": "ImageDetailLevel", "modelAsString": true, "values": [ { "value": "auto", "description": "The image detail level is auto." }, { "value": "low", "description": "The image detail level is low." }, { "value": "high", "description": "The image detail level is high." } ] } }, "chatCompletionRequestMessageAssistant": { "allOf": [ { "$ref": "#/components/schemas/chatCompletionRequestMessage" }, { "type": "object", "properties": { "content": { "type": "string", "description": "The contents of the message.", "nullable": true }, "tool_calls": { "type": "array", "description": "The tool calls generated by the model, such as function calls.", "items": { "$ref": "#/components/schemas/chatCompletionMessageToolCall" } }, "context": { "$ref": "#/components/schemas/azureChatExtensionsMessageContext" } } } ], "required": [ "content" ] }, "azureChatExtensionConfiguration": { "required": [ "type" ], "type": "object", "properties": { "type": { "$ref": "#/components/schemas/azureChatExtensionType" } }, "description": " A representation of configuration data for a single Azure OpenAI chat extension. This will be used by a chat\n completions request that should use Azure OpenAI chat extensions to augment the response behavior.\n The use of this configuration is compatible only with Azure OpenAI.", "discriminator": { "propertyName": "type", "mapping": { "azure_search": "#/components/schemas/azureSearchChatExtensionConfiguration", "azure_cosmos_db": "#/components/schemas/azureCosmosDBChatExtensionConfiguration" } } }, "azureChatExtensionType": { "type": "string", "description": " A representation of configuration data for a single Azure OpenAI chat extension. This will be used by a chat\n completions request that should use Azure OpenAI chat extensions to augment the response behavior.\n The use of this configuration is compatible only with Azure OpenAI.", "enum": [ "azure_search", "azure_cosmos_db" ], "x-ms-enum": { "name": "AzureChatExtensionType", "modelAsString": true, "values": [ { "name": "azureSearch", "value": "azure_search", "description": "Represents the use of Azure Search as an Azure OpenAI chat extension." }, { "name": "azureCosmosDB", "value": "azure_cosmos_db", "description": "Represents the use of Azure Cosmos DB as an Azure OpenAI chat extension." } ] } }, "azureSearchChatExtensionConfiguration": { "required": [ "parameters" ], "description": "A specific representation of configurable options for Azure Search when using it as an Azure OpenAI chat\nextension.", "allOf": [ { "$ref": "#/components/schemas/azureChatExtensionConfiguration" }, { "properties": { "parameters": { "$ref": "#/components/schemas/azureSearchChatExtensionParameters" } } } ], "x-ms-discriminator-value": "azure_search" }, "azureSearchChatExtensionParameters": { "required": [ "authentication", "endpoint", "index_name" ], "type": "object", "properties": { "authentication": { "oneOf": [ { "$ref": "#/components/schemas/onYourDataApiKeyAuthenticationOptions" }, { "$ref": "#/components/schemas/onYourDataSystemAssignedManagedIdentityAuthenticationOptions" }, { "$ref": "#/components/schemas/onYourDataUserAssignedManagedIdentityAuthenticationOptions" } ] }, "top_n_documents": { "type": "integer", "description": "The configured top number of documents to feature for the configured query.", "format": "int32" }, "in_scope": { "type": "boolean", "description": "Whether queries should be restricted to use of indexed data." }, "strictness": { "maximum": 5, "minimum": 1, "type": "integer", "description": "The configured strictness of the search relevance filtering. The higher of strictness, the higher of the precision but lower recall of the answer.", "format": "int32" }, "role_information": { "type": "string", "description": "Give the model instructions about how it should behave and any context it should reference when generating a response. You can describe the assistant's personality and tell it how to format responses. There's a 100 token limit for it, and it counts against the overall token limit." }, "endpoint": { "type": "string", "description": "The absolute endpoint path for the Azure Search resource to use.", "format": "uri" }, "index_name": { "type": "string", "description": "The name of the index to use as available in the referenced Azure Search resource." }, "fields_mapping": { "$ref": "#/components/schemas/azureSearchIndexFieldMappingOptions" }, "query_type": { "$ref": "#/components/schemas/azureSearchQueryType" }, "semantic_configuration": { "type": "string", "description": "The additional semantic configuration for the query." }, "filter": { "type": "string", "description": "Search filter." }, "embedding_dependency": { "oneOf": [ { "$ref": "#/components/schemas/onYourDataEndpointVectorizationSource" }, { "$ref": "#/components/schemas/onYourDataDeploymentNameVectorizationSource" } ] } }, "description": "Parameters for Azure Search when used as an Azure OpenAI chat extension." }, "azureSearchIndexFieldMappingOptions": { "type": "object", "properties": { "title_field": { "type": "string", "description": "The name of the index field to use as a title." }, "url_field": { "type": "string", "description": "The name of the index field to use as a URL." }, "filepath_field": { "type": "string", "description": "The name of the index field to use as a filepath." }, "content_fields": { "type": "array", "description": "The names of index fields that should be treated as content.", "items": { "type": "string" } }, "content_fields_separator": { "type": "string", "description": "The separator pattern that content fields should use." }, "vector_fields": { "type": "array", "description": "The names of fields that represent vector data.", "items": { "type": "string" } } }, "description": "Optional settings to control how fields are processed when using a configured Azure Search resource." }, "azureSearchQueryType": { "type": "string", "description": "The type of Azure Search retrieval query that should be executed when using it as an Azure OpenAI chat extension.", "enum": [ "simple", "semantic", "vector", "vector_simple_hybrid", "vector_semantic_hybrid" ], "x-ms-enum": { "name": "azureSearchQueryType", "modelAsString": true, "values": [ { "name": "simple", "value": "simple", "description": "Represents the default, simple query parser." }, { "name": "semantic", "value": "semantic", "description": "Represents the semantic query parser for advanced semantic modeling." }, { "name": "vector", "value": "vector", "description": "Represents vector search over computed data." }, { "name": "vectorSimpleHybrid", "value": "vector_simple_hybrid", "description": "Represents a combination of the simple query strategy with vector data." }, { "name": "vectorSemanticHybrid", "value": "vector_semantic_hybrid", "description": "Represents a combination of semantic search and vector data querying." } ] } }, "azureCosmosDBChatExtensionConfiguration": { "required": [ "parameters" ], "description": "A specific representation of configurable options for Azure Cosmos DB when using it as an Azure OpenAI chat\nextension.", "allOf": [ { "$ref": "#/components/schemas/azureChatExtensionConfiguration" }, { "properties": { "parameters": { "$ref": "#/components/schemas/azureCosmosDBChatExtensionParameters" } } } ], "x-ms-discriminator-value": "azure_cosmos_db" }, "azureCosmosDBChatExtensionParameters": { "required": [ "authentication", "container_name", "database_name", "embedding_dependency", "fields_mapping", "index_name" ], "type": "object", "properties": { "authentication": { "$ref": "#/components/schemas/onYourDataConnectionStringAuthenticationOptions" }, "top_n_documents": { "type": "integer", "description": "The configured top number of documents to feature for the configured query.", "format": "int32" }, "in_scope": { "type": "boolean", "description": "Whether queries should be restricted to use of indexed data." }, "strictness": { "maximum": 5, "minimum": 1, "type": "integer", "description": "The configured strictness of the search relevance filtering. The higher of strictness, the higher of the precision but lower recall of the answer.", "format": "int32" }, "role_information": { "type": "string", "description": "Give the model instructions about how it should behave and any context it should reference when generating a response. You can describe the assistant's personality and tell it how to format responses. There's a 100 token limit for it, and it counts against the overall token limit." }, "database_name": { "type": "string", "description": "The MongoDB vCore database name to use with Azure Cosmos DB." }, "container_name": { "type": "string", "description": "The name of the Azure Cosmos DB resource container." }, "index_name": { "type": "string", "description": "The MongoDB vCore index name to use with Azure Cosmos DB." }, "fields_mapping": { "$ref": "#/components/schemas/azureCosmosDBFieldMappingOptions" }, "embedding_dependency": { "oneOf": [ { "$ref": "#/components/schemas/onYourDataEndpointVectorizationSource" }, { "$ref": "#/components/schemas/onYourDataDeploymentNameVectorizationSource" } ] } }, "description": "Parameters to use when configuring Azure OpenAI On Your Data chat extensions when using Azure Cosmos DB for\nMongoDB vCore." }, "azureCosmosDBFieldMappingOptions": { "required": [ "content_fields", "vector_fields" ], "type": "object", "properties": { "title_field": { "type": "string", "description": "The name of the index field to use as a title." }, "url_field": { "type": "string", "description": "The name of the index field to use as a URL." }, "filepath_field": { "type": "string", "description": "The name of the index field to use as a filepath." }, "content_fields": { "type": "array", "description": "The names of index fields that should be treated as content.", "items": { "type": "string" } }, "content_fields_separator": { "type": "string", "description": "The separator pattern that content fields should use." }, "vector_fields": { "type": "array", "description": "The names of fields that represent vector data.", "items": { "type": "string" } } }, "description": "Optional settings to control how fields are processed when using a configured Azure Cosmos DB resource." }, "onYourDataAuthenticationOptions": { "required": [ "type" ], "type": "object", "properties": { "type": { "$ref": "#/components/schemas/onYourDataAuthenticationType" } }, "description": "The authentication options for Azure OpenAI On Your Data.", "discriminator": { "propertyName": "type", "mapping": { "api_key": "#/components/schemas/onYourDataApiKeyAuthenticationOptions", "connection_string": "#/components/schemas/onYourDataConnectionStringAuthenticationOptions", "system_assigned_managed_identity": "#/components/schemas/onYourDataSystemAssignedManagedIdentityAuthenticationOptions", "user_assigned_managed_identity": "#/components/schemas/onYourDataUserAssignedManagedIdentityAuthenticationOptions" } } }, "onYourDataAuthenticationType": { "type": "string", "description": "The authentication types supported with Azure OpenAI On Your Data.", "enum": [ "api_key", "connection_string", "system_assigned_managed_identity", "user_assigned_managed_identity" ], "x-ms-enum": { "name": "OnYourDataAuthenticationType", "modelAsString": true, "values": [ { "name": "apiKey", "value": "api_key", "description": "Authentication via API key." }, { "name": "connectionString", "value": "connection_string", "description": "Authentication via connection string." }, { "name": "systemAssignedManagedIdentity", "value": "system_assigned_managed_identity", "description": "Authentication via system-assigned managed identity." }, { "name": "userAssignedManagedIdentity", "value": "user_assigned_managed_identity", "description": "Authentication via user-assigned managed identity." } ] } }, "onYourDataApiKeyAuthenticationOptions": { "required": [ "key" ], "description": "The authentication options for Azure OpenAI On Your Data when using an API key.", "allOf": [ { "$ref": "#/components/schemas/onYourDataAuthenticationOptions" }, { "properties": { "key": { "type": "string", "description": "The API key to use for authentication." } } } ], "x-ms-discriminator-value": "api_key" }, "onYourDataConnectionStringAuthenticationOptions": { "required": [ "connection_string" ], "description": "The authentication options for Azure OpenAI On Your Data when using a connection string.", "allOf": [ { "$ref": "#/components/schemas/onYourDataAuthenticationOptions" }, { "properties": { "connection_string": { "type": "string", "description": "The connection string to use for authentication." } } } ], "x-ms-discriminator-value": "connection_string" }, "onYourDataSystemAssignedManagedIdentityAuthenticationOptions": { "description": "The authentication options for Azure OpenAI On Your Data when using a system-assigned managed identity.", "allOf": [ { "$ref": "#/components/schemas/onYourDataAuthenticationOptions" } ], "x-ms-discriminator-value": "system_assigned_managed_identity" }, "onYourDataUserAssignedManagedIdentityAuthenticationOptions": { "required": [ "managed_identity_resource_id" ], "description": "The authentication options for Azure OpenAI On Your Data when using a user-assigned managed identity.", "allOf": [ { "$ref": "#/components/schemas/onYourDataAuthenticationOptions" }, { "properties": { "managed_identity_resource_id": { "type": "string", "description": "The resource ID of the user-assigned managed identity to use for authentication." } } } ], "x-ms-discriminator-value": "user_assigned_managed_identity" }, "onYourDataVectorizationSource": { "required": [ "type" ], "type": "object", "properties": { "type": { "$ref": "#/components/schemas/onYourDataVectorizationSourceType" } }, "description": "An abstract representation of a vectorization source for Azure OpenAI On Your Data with vector search.", "discriminator": { "propertyName": "type", "mapping": { "endpoint": "#/components/schemas/onYourDataEndpointVectorizationSource", "deployment_name": "#/components/schemas/onYourDataDeploymentNameVectorizationSource" } } }, "onYourDataVectorizationSourceType": { "type": "string", "description": "Represents the available sources Azure OpenAI On Your Data can use to configure vectorization of data for use with\nvector search.", "enum": [ "endpoint", "deployment_name" ], "x-ms-enum": { "name": "OnYourDataVectorizationSourceType", "modelAsString": true, "values": [ { "name": "endpoint", "value": "endpoint", "description": "Represents vectorization performed by public service calls to an Azure OpenAI embedding model." }, { "name": "deploymentName", "value": "deployment_name", "description": "Represents an Ada model deployment name to use. This model deployment must be in the same Azure OpenAI resource, but\nOn Your Data will use this model deployment via an internal call rather than a public one, which enables vector\nsearch even in private networks." } ] } }, "onYourDataDeploymentNameVectorizationSource": { "required": [ "deployment_name" ], "description": "The details of a a vectorization source, used by Azure OpenAI On Your Data when applying vector search, that is based\non an internal embeddings model deployment name in the same Azure OpenAI resource.", "allOf": [ { "$ref": "#/components/schemas/onYourDataVectorizationSource" }, { "properties": { "deployment_name": { "type": "string", "description": "Specifies the name of the model deployment to use for vectorization. This model deployment must be in the same Azure OpenAI resource, but On Your Data will use this model deployment via an internal call rather than a public one, which enables vector search even in private networks." } } } ], "x-ms-discriminator-value": "deployment_name" }, "onYourDataEndpointVectorizationSource": { "required": [ "authentication", "endpoint" ], "description": "The details of a a vectorization source, used by Azure OpenAI On Your Data when applying vector search, that is based\non a public Azure OpenAI endpoint call for embeddings.", "allOf": [ { "$ref": "#/components/schemas/onYourDataVectorizationSource" }, { "properties": { "authentication": { "$ref": "#/components/schemas/onYourDataApiKeyAuthenticationOptions" }, "endpoint": { "type": "string", "description": "Specifies the endpoint to use for vectorization. This endpoint must be in the same Azure OpenAI resource, but On Your Data will use this endpoint via an internal call rather than a public one, which enables vector search even in private networks.", "format": "uri" } } } ], "x-ms-discriminator-value": "endpoint" }, "azureChatExtensionsMessageContext": { "type": "object", "properties": { "citations": { "type": "array", "description": "The data source retrieval result, used to generate the assistant message in the response.", "items": { "$ref": "#/components/schemas/citation" }, "x-ms-identifiers": [] }, "intent": { "type": "string", "description": "The detected intent from the chat history, used to pass to the next turn to carry over the context." } }, "description": " A representation of the additional context information available when Azure OpenAI chat extensions are involved\n in the generation of a corresponding chat completions response. This context information is only populated when\n using an Azure OpenAI request configured to use a matching extension." }, "citation": { "required": [ "content" ], "type": "object", "properties": { "content": { "type": "string", "description": "The content of the citation." }, "title": { "type": "string", "description": "The title of the citation." }, "url": { "type": "string", "description": "The URL of the citation." }, "filepath": { "type": "string", "description": "The file path of the citation." }, "chunk_id": { "type": "string", "description": "The chunk ID of the citation." } }, "description": "citation information for a chat completions response message." }, "chatCompletionMessageToolCall": { "type": "object", "properties": { "id": { "type": "string", "description": "The ID of the tool call." }, "type": { "$ref": "#/components/schemas/toolCallType" }, "function": { "type": "object", "description": "The function that the model called.", "properties": { "name": { "type": "string", "description": "The name of the function to call." }, "arguments": { "type": "string", "description": "The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function." } }, "required": [ "name", "arguments" ] } }, "required": [ "id", "type", "function" ] }, "toolCallType": { "type": "string", "enum": [ "function" ], "description": "The type of the tool call, in this case `function`.", "x-ms-enum": { "name": "ToolCallType", "modelAsString": true, "values": [ { "value": "function", "description": "The tool call type is function." } ] } }, "chatCompletionRequestMessageTool": { "allOf": [ { "$ref": "#/components/schemas/chatCompletionRequestMessage" }, { "type": "object", "nullable": true, "properties": { "tool_call_id": { "type": "string", "description": "Tool call that this message is responding to." }, "content": { "type": "string", "description": "The contents of the message.", "nullable": true } } } ], "required": [ "tool_call_id", "content" ] }, "chatCompletionRequestMessageFunction": { "allOf": [ { "$ref": "#/components/schemas/chatCompletionRequestMessage" }, { "type": "object", "description": "Deprecated. Message that represents a function.", "nullable": true, "properties": { "role": { "type": "string", "enum": [ "function" ], "description": "The role of the messages author, in this case `function`." }, "name": { "type": "string", "description": "The contents of the message." }, "content": { "type": "string", "description": "The contents of the message.", "nullable": true } } } ], "required": [ "function_call_id", "content" ] }, "createChatCompletionResponse": { "type": "object", "allOf": [ { "$ref": "#/components/schemas/chatCompletionsResponseCommon" }, { "properties": { "prompt_filter_results": { "$ref": "#/components/schemas/promptFilterResults" }, "choices": { "type": "array", "items": { "type": "object", "allOf": [ { "$ref": "#/components/schemas/chatCompletionChoiceCommon" }, { "properties": { "message": { "$ref": "#/components/schemas/chatCompletionResponseMessage" }, "content_filter_results": { "$ref": "#/components/schemas/contentFilterChoiceResults" } } } ] } } } } ], "required": [ "id", "object", "created", "model", "choices" ] }, "chatCompletionResponseMessage": { "type": "object", "description": "A chat completion message generated by the model.", "properties": { "role": { "$ref": "#/components/schemas/chatCompletionResponseMessageRole" }, "content": { "type": "string", "description": "The contents of the message.", "nullable": true }, "tool_calls": { "type": "array", "description": "The tool calls generated by the model, such as function calls.", "items": { "$ref": "#/components/schemas/chatCompletionMessageToolCall" } }, "function_call": { "$ref": "#/components/schemas/chatCompletionFunctionCall" }, "context": { "$ref": "#/components/schemas/azureChatExtensionsMessageContext" } } }, "chatCompletionResponseMessageRole": { "type": "string", "enum": [ "assistant" ], "description": "The role of the author of the response message." }, "chatCompletionToolChoiceOption": { "description": "Controls which (if any) function is called by the model. `none` means the model will not call a function and instead generates a message. `auto` means the model can pick between generating a message or calling a function. Specifying a particular function via `{\"type\": \"function\", \"function\": {\"name\": \"my_function\"}}` forces the model to call that function.", "oneOf": [ { "type": "string", "description": "`none` means the model will not call a function and instead generates a message. `auto` means the model can pick between generating a message or calling a function.", "enum": [ "none", "auto" ] }, { "$ref": "#/components/schemas/chatCompletionNamedToolChoice" } ] }, "chatCompletionNamedToolChoice": { "type": "object", "description": "Specifies a tool the model should use. Use to force the model to call a specific function.", "properties": { "type": { "type": "string", "enum": [ "function" ], "description": "The type of the tool. Currently, only `function` is supported." }, "function": { "type": "object", "properties": { "name": { "type": "string", "description": "The name of the function to call." } }, "required": [ "name" ] } } }, "chatCompletionFunctionCall": { "type": "object", "description": "Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model.", "properties": { "name": { "type": "string", "description": "The name of the function to call." }, "arguments": { "type": "string", "description": "The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function." } }, "required": [ "name", "arguments" ] }, "chatCompletionsResponseCommon": { "type": "object", "properties": { "id": { "type": "string", "description": "A unique identifier for the chat completion." }, "object": { "$ref": "#/components/schemas/chatCompletionResponseObject" }, "created": { "type": "integer", "format": "unixtime", "description": "The Unix timestamp (in seconds) of when the chat completion was created." }, "model": { "type": "string", "description": "The model used for the chat completion." }, "usage": { "$ref": "#/components/schemas/completionUsage" }, "system_fingerprint": { "type": "string", "description": "Can be used in conjunction with the `seed` request parameter to understand when backend changes have been made that might impact determinism." } }, "required": [ "id", "object", "created", "model" ] }, "chatCompletionResponseObject": { "type": "string", "description": "The object type.", "enum": [ "chat.completion" ], "x-ms-enum": { "name": "ChatCompletionResponseObject", "modelAsString": true, "values": [ { "value": "chat.completion", "description": "The object type is chat completion." } ] } }, "completionUsage": { "type": "object", "description": "Usage statistics for the completion request.", "properties": { "prompt_tokens": { "type": "integer", "description": "Number of tokens in the prompt." }, "completion_tokens": { "type": "integer", "description": "Number of tokens in the generated completion." }, "total_tokens": { "type": "integer", "description": "Total number of tokens used in the request (prompt + completion)." } }, "required": [ "prompt_tokens", "completion_tokens", "total_tokens" ] }, "chatCompletionTool": { "type": "object", "properties": { "type": { "$ref": "#/components/schemas/chatCompletionToolType" }, "function": { "type": "object", "properties": { "description": { "type": "string", "description": "A description of what the function does, used by the model to choose when and how to call the function." }, "name": { "type": "string", "description": "The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64." }, "parameters": { "$ref": "#/components/schemas/chatCompletionFunctionParameters" } }, "required": [ "name", "parameters" ] } }, "required": [ "type", "function" ] }, "chatCompletionToolType": { "type": "string", "enum": [ "function" ], "description": "The type of the tool. Currently, only `function` is supported.", "x-ms-enum": { "name": "ChatCompletionToolType", "modelAsString": true, "values": [ { "value": "function", "description": "The tool type is function." } ] } }, "chatCompletionChoiceCommon": { "type": "object", "properties": { "index": { "type": "integer" }, "finish_reason": { "type": "string" } } }, "createTranslationRequest": { "type": "object", "description": "Translation request.", "properties": { "file": { "type": "string", "description": "The audio file to translate.", "format": "binary" }, "prompt": { "type": "string", "description": "An optional text to guide the model's style or continue a previous audio segment. The prompt should be in English." }, "response_format": { "$ref": "#/components/schemas/audioResponseFormat" }, "temperature": { "type": "number", "default": 0, "description": "The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit." } }, "required": [ "file" ] }, "audioResponse": { "description": "Translation or transcription response when response_format was json", "type": "object", "properties": { "text": { "type": "string", "description": "Translated or transcribed text." } }, "required": [ "text" ] }, "audioVerboseResponse": { "description": "Translation or transcription response when response_format was verbose_json", "type": "object", "allOf": [ { "$ref": "#/components/schemas/audioResponse" }, { "properties": { "task": { "type": "string", "description": "Type of audio task.", "enum": [ "transcribe", "translate" ], "x-ms-enum": { "modelAsString": true } }, "language": { "type": "string", "description": "Language." }, "duration": { "type": "number", "description": "Duration." }, "segments": { "type": "array", "items": { "$ref": "#/components/schemas/audioSegment" } } } } ], "required": [ "text" ] }, "audioResponseFormat": { "title": "AudioResponseFormat", "description": "Defines the format of the output.", "enum": [ "json", "text", "srt", "verbose_json", "vtt" ], "type": "string", "x-ms-enum": { "modelAsString": true } }, "createTranscriptionRequest": { "type": "object", "description": "Transcription request.", "properties": { "file": { "type": "string", "description": "The audio file object to transcribe.", "format": "binary" }, "prompt": { "type": "string", "description": "An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language." }, "response_format": { "$ref": "#/components/schemas/audioResponseFormat" }, "temperature": { "type": "number", "default": 0, "description": "The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit." }, "language": { "type": "string", "description": "The language of the input audio. Supplying the input language in ISO-639-1 format will improve accuracy and latency." } }, "required": [ "file" ] }, "audioSegment": { "type": "object", "description": "Transcription or translation segment.", "properties": { "id": { "type": "integer", "description": "Segment identifier." }, "seek": { "type": "number", "description": "Offset of the segment." }, "start": { "type": "number", "description": "Segment start offset." }, "end": { "type": "number", "description": "Segment end offset." }, "text": { "type": "string", "description": "Segment text." }, "tokens": { "type": "array", "items": { "type": "number", "nullable": false }, "description": "Tokens of the text." }, "temperature": { "type": "number", "description": "Temperature." }, "avg_logprob": { "type": "number", "description": "Average log probability." }, "compression_ratio": { "type": "number", "description": "Compression ratio." }, "no_speech_prob": { "type": "number", "description": "Probability of 'no speech'." } } }, "imageQuality": { "description": "The quality of the image that will be generated.", "type": "string", "enum": [ "standard", "hd" ], "default": "standard", "x-ms-enum": { "name": "Quality", "modelAsString": true, "values": [ { "value": "standard", "description": "Standard quality creates images with standard quality.", "name": "Standard" }, { "value": "hd", "description": "HD quality creates images with finer details and greater consistency across the image.", "name": "HD" } ] } }, "imagesResponseFormat": { "description": "The format in which the generated images are returned.", "type": "string", "enum": [ "url", "b64_json" ], "default": "url", "x-ms-enum": { "name": "ImagesResponseFormat", "modelAsString": true, "values": [ { "value": "url", "description": "The URL that provides temporary access to download the generated images.", "name": "Url" }, { "value": "b64_json", "description": "The generated images are returned as base64 encoded string.", "name": "Base64Json" } ] } }, "imageSize": { "description": "The size of the generated images.", "type": "string", "enum": [ "1792x1024", "1024x1792", "1024x1024" ], "default": "1024x1024", "x-ms-enum": { "name": "Size", "modelAsString": true, "values": [ { "value": "1792x1024", "description": "The desired size of the generated image is 1792x1024 pixels.", "name": "Size1792x1024" }, { "value": "1024x1792", "description": "The desired size of the generated image is 1024x1792 pixels.", "name": "Size1024x1792" }, { "value": "1024x1024", "description": "The desired size of the generated image is 1024x1024 pixels.", "name": "Size1024x1024" } ] } }, "imageStyle": { "description": "The style of the generated images.", "type": "string", "enum": [ "vivid", "natural" ], "default": "vivid", "x-ms-enum": { "name": "Style", "modelAsString": true, "values": [ { "value": "vivid", "description": "Vivid creates images that are hyper-realistic and dramatic.", "name": "Vivid" }, { "value": "natural", "description": "Natural creates images that are more natural and less hyper-realistic.", "name": "Natural" } ] } }, "imageGenerationsRequest": { "type": "object", "properties": { "prompt": { "description": "A text description of the desired image(s). The maximum length is 4000 characters.", "type": "string", "format": "string", "example": "a corgi in a field", "minLength": 1 }, "n": { "description": "The number of images to generate.", "type": "integer", "minimum": 1, "maximum": 1, "default": 1 }, "size": { "$ref": "#/components/schemas/imageSize" }, "response_format": { "$ref": "#/components/schemas/imagesResponseFormat" }, "user": { "description": "A unique identifier representing your end-user, which can help to monitor and detect abuse.", "type": "string", "format": "string", "example": "user123456" }, "quality": { "$ref": "#/components/schemas/imageQuality" }, "style": { "$ref": "#/components/schemas/imageStyle" } }, "required": [ "prompt" ] }, "generateImagesResponse": { "type": "object", "properties": { "created": { "type": "integer", "format": "unixtime", "description": "The unix timestamp when the operation was created.", "example": "1676540381" }, "data": { "type": "array", "description": "The result data of the operation, if successful", "items": { "$ref": "#/components/schemas/imageResult" } } }, "required": [ "created", "data" ] }, "imageResult": { "type": "object", "description": "The image url or encoded image if successful, and an error otherwise.", "properties": { "url": { "type": "string", "description": "The image url.", "example": "https://www.contoso.com" }, "b64_json": { "type": "string", "description": "The base64 encoded image" }, "content_filter_results": { "$ref": "#/components/schemas/dalleContentFilterResults" }, "revised_prompt": { "type": "string", "description": "The prompt that was used to generate the image, if there was any revision to the prompt." }, "prompt_filter_results": { "$ref": "#/components/schemas/dalleFilterResults" } } } }, "securitySchemes": { "bearer": { "type": "oauth2", "flows": { "implicit": { "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", "scopes": {} } }, "x-tokenInfoFunc": "api.middleware.auth.bearer_auth", "x-scopeValidateFunc": "api.middleware.auth.validate_scopes" }, "apiKey": { "type": "apiKey", "name": "api-key", "in": "header" } } } } ================================================ FILE: scenarios/workload-genai/terraform/modules/apim_policies/apimanagement.tf ================================================ locals { azureOpenAIAPINames = [azurerm_api_management_api.azureOpenAIApi.name] } data "azurerm_api_management" "apiManagementService" { name = var.apiManagementServiceName resource_group_name = var.resourceGroupName } data "azurerm_user_assigned_identity" "apimIdentity" { name = var.apimIdentityName resource_group_name = var.resourceGroupName } data "azurerm_eventhub_namespace" "eventHubNamespace" { name = var.eventHubNamespaceName resource_group_name = var.openaiResourceGroupName } resource "azurerm_api_management_api" "azureOpenAIApi" { name = "azure-openai-api" resource_group_name = var.resourceGroupName api_management_name = data.azurerm_api_management.apiManagementService.name revision = "1" display_name = "AzureOpenAI" path = "openai" protocols = ["https"] import { content_format = "openapi+json" content_value = file("modules/apim_policies/api-specs/openapi-spec.json") } } resource "azurerm_api_management_product" "azureOpenAIProduct" { product_id = "aoai-product" resource_group_name = var.resourceGroupName api_management_name = data.azurerm_api_management.apiManagementService.name display_name = "aoai-product" subscription_required = true published = true } resource "azurerm_api_management_product" "multiTenantProduct1" { product_id = "multi-tenant-product1" resource_group_name = var.resourceGroupName api_management_name = data.azurerm_api_management.apiManagementService.name display_name = "multi-tenant-product1" subscription_required = true published = true } resource "azurerm_api_management_product" "multiTenantProduct2" { product_id = "multi-tenant-product2" resource_group_name = var.resourceGroupName api_management_name = data.azurerm_api_management.apiManagementService.name display_name = "multi-tenant-product2" subscription_required = true published = true } resource "azurerm_api_management_product_api" "azureOpenAIProductAPI" { product_id = azurerm_api_management_product.azureOpenAIProduct.product_id api_name = azurerm_api_management_api.azureOpenAIApi.name api_management_name = data.azurerm_api_management.apiManagementService.name resource_group_name = var.resourceGroupName depends_on = [ azurerm_api_management_api.azureOpenAIApi, azurerm_api_management_policy_fragment.simpleRoundRobinPolicyFragment ] } resource "azurerm_api_management_product_api" "multiTenantProduct1API" { product_id = azurerm_api_management_product.multiTenantProduct1.product_id api_name = azurerm_api_management_api.azureOpenAIApi.name api_management_name = data.azurerm_api_management.apiManagementService.name resource_group_name = var.resourceGroupName depends_on = [ azurerm_api_management_api.azureOpenAIApi, azurerm_api_management_policy_fragment.simpleRoundRobinPolicyFragment ] } resource "azurerm_api_management_product_api" "multiTenantProduct2API" { product_id = azurerm_api_management_product.multiTenantProduct2.product_id api_name = azurerm_api_management_api.azureOpenAIApi.name api_management_name = data.azurerm_api_management.apiManagementService.name resource_group_name = var.resourceGroupName depends_on = [ azurerm_api_management_api.azureOpenAIApi, azurerm_api_management_policy_fragment.simpleRoundRobinPolicyFragment ] } resource "azurerm_api_management_backend" "ptuBackendOne" { name = "ptu-backend-1" resource_group_name = var.resourceGroupName api_management_name = data.azurerm_api_management.apiManagementService.name protocol = "http" url = var.ptuDeploymentOneBaseUrl } resource "azurerm_api_management_backend" "payAsYouGoBackendOne" { name = "payg-backend-1" resource_group_name = var.resourceGroupName api_management_name = data.azurerm_api_management.apiManagementService.name protocol = "http" url = var.payAsYouGoDeploymentOneBaseUrl } resource "azurerm_api_management_backend" "payAsYouGoBackendTwo" { name = "payg-backend-2" resource_group_name = var.resourceGroupName api_management_name = data.azurerm_api_management.apiManagementService.name protocol = "http" url = var.payAsYouGoDeploymentTwoBaseUrl } resource "azurerm_api_management_subscription" "azureOpenAIProductSubscription" { subscription_id = "aoai-product-subscription" resource_group_name = var.resourceGroupName api_management_name = data.azurerm_api_management.apiManagementService.name display_name = "aoai-product-subscription" state = "active" product_id = azurerm_api_management_product.azureOpenAIProduct.id } resource "azurerm_api_management_subscription" "multiTenantProduct1Subscription" { subscription_id = "multi-tenant-product1-subscription" resource_group_name = var.resourceGroupName api_management_name = data.azurerm_api_management.apiManagementService.name display_name = "multi-tenant-product1-subscription" state = "active" product_id = azurerm_api_management_product.multiTenantProduct1.id } resource "azurerm_api_management_subscription" "multiTenantProduct2Subscription" { subscription_id = "multi-tenant-product2-subscription" resource_group_name = var.resourceGroupName api_management_name = data.azurerm_api_management.apiManagementService.name display_name = "multi-tenant-product2-subscription" state = "active" product_id = azurerm_api_management_product.multiTenantProduct2.id } resource "azurerm_api_management_policy_fragment" "simpleRoundRobinPolicyFragment" { api_management_id = data.azurerm_api_management.apiManagementService.id name = "simple-priority-weighted" format = "rawxml" value = file("../policies/fragments/load-balancing/simple-priority-weighted.xml") depends_on = [ azurerm_api_management_backend.payAsYouGoBackendOne, azurerm_api_management_backend.payAsYouGoBackendTwo, azurerm_api_management_named_value.apimOpenaiApiUamiNamedValue, module.api_lb_pool ] } resource "azurerm_api_management_policy_fragment" "simpleRateLimitingPolicyFragment" { api_management_id = data.azurerm_api_management.apiManagementService.id name = "rate-limiting-by-tokens" format = "rawxml" value = file("../policies/fragments/rate-limiting/rate-limiting-by-tokens.xml") depends_on = [ azurerm_api_management_backend.payAsYouGoBackendOne, azurerm_api_management_backend.payAsYouGoBackendTwo ] } resource "azurerm_api_management_policy_fragment" "adaptiveRateLimitingPolicyFragment" { api_management_id = data.azurerm_api_management.apiManagementService.id name = "adaptive-rate-limiting" format = "rawxml" value = file("../policies/fragments/rate-limiting/adaptive-rate-limiting.xml") depends_on = [ azurerm_api_management_backend.payAsYouGoBackendOne, azurerm_api_management_backend.payAsYouGoBackendTwo ] } resource "azurerm_api_management_policy_fragment" "adaptiveRateLimitingWorkAroundPolicyFragment" { api_management_id = data.azurerm_api_management.apiManagementService.id name = "rate-limiting-workaround" format = "rawxml" value = file("../policies/fragments/rate-limiting/rate-limiting-workaround.xml") depends_on = [ azurerm_api_management_backend.payAsYouGoBackendOne, azurerm_api_management_backend.payAsYouGoBackendTwo ] } resource "azurerm_api_management_policy_fragment" "usageTrackingEHPolicyFragment" { api_management_id = data.azurerm_api_management.apiManagementService.id name = "usage-tracking-with-eventhub" format = "rawxml" value = file("../policies/fragments/usage-tracking/usage-tracking-with-eventhub.xml") depends_on = [ azurerm_api_management_logger.event_hub_logger ] } resource "azurerm_api_management_policy_fragment" "usageTrackingWithAppInsightsPolicyFragment" { api_management_id = data.azurerm_api_management.apiManagementService.id name = "usage-tracking-with-appinsights" format = "rawxml" value = file("../policies/fragments/usage-tracking/usage-tracking-with-appinsights.xml") depends_on = [ azurerm_api_management_logger.event_hub_logger ] } //Load-balancing with Circuit Breaker policy module "api_backend" { source = "./backends" api_management_service_name = data.azurerm_api_management.apiManagementService.name backend_uris = [ "${var.ptuDeploymentOneBaseUrl}/", "${var.payAsYouGoDeploymentOneBaseUrl}/", "${var.payAsYouGoDeploymentTwoBaseUrl}/" ] resource_group_name = var.resourceGroupName depends_on = [ data.azurerm_api_management.apiManagementService ] } module "api_lb_pool" { source = "./lb_pool" api_management_service_name = data.azurerm_api_management.apiManagementService.name backends = module.api_backend.backend_names resource_group_name = var.resourceGroupName depends_on = [ module.api_backend ] } resource "azurerm_api_management_api_policy" "azureOpenAIApiPolicy" { api_name = azurerm_api_management_api.azureOpenAIApi.name api_management_name = data.azurerm_api_management.apiManagementService.name resource_group_name = data.azurerm_api_management.apiManagementService.resource_group_name xml_content = file("../policies/genai-policy.xml") depends_on = [ azurerm_api_management_policy_fragment.simpleRoundRobinPolicyFragment, azurerm_api_management_policy_fragment.adaptiveRateLimitingPolicyFragment, azurerm_api_management_policy_fragment.usageTrackingWithAppInsightsPolicyFragment ] } resource "azurerm_api_management_product_policy" "multiTenantProduct1Policy" { product_id = azurerm_api_management_product.multiTenantProduct1.product_id api_management_name = data.azurerm_api_management.apiManagementService.name resource_group_name = data.azurerm_api_management.apiManagementService.resource_group_name xml_content = file("../policies/multi-tenancy/multi-tenant-product1-policy.xml") } resource "azurerm_api_management_product_policy" "multiTenantProduct2Policy" { product_id = azurerm_api_management_product.multiTenantProduct2.product_id api_management_name = data.azurerm_api_management.apiManagementService.name resource_group_name = data.azurerm_api_management.apiManagementService.resource_group_name xml_content = file("../policies/multi-tenancy/multi-tenant-product2-policy.xml") } resource "azurerm_api_management_named_value" "apimOpenaiApiUamiNamedValue" { name = "apim-identity" resource_group_name = var.resourceGroupName api_management_name = data.azurerm_api_management.apiManagementService.name display_name = "apim-identity" value = data.azurerm_user_assigned_identity.apimIdentity.client_id secret = true } resource "azurerm_api_management_logger" "event_hub_logger" { name = "eventhub-logger" resource_group_name = var.resourceGroupName api_management_name = data.azurerm_api_management.apiManagementService.name eventhub { name = var.eventHubName endpoint_uri = "${data.azurerm_eventhub_namespace.eventHubNamespace.name}.servicebus.windows.net" user_assigned_identity_client_id = data.azurerm_user_assigned_identity.apimIdentity.client_id } } ================================================ FILE: scenarios/workload-genai/terraform/modules/apim_policies/backends/backends.tf ================================================ variable "api_management_service_name" { type = string } variable "backend_uris" { type = list(string) } variable "resource_group_name" { type = string } data "azurerm_api_management" "apiManagementService" { name = var.api_management_service_name resource_group_name = var.resource_group_name } resource "azapi_resource" "backend" { count = length(var.backend_uris) type = "Microsoft.ApiManagement/service/backends@2023-09-01-preview" name = "aoai-${count.index}" parent_id = data.azurerm_api_management.apiManagementService.id body = jsonencode({ properties = { url = var.backend_uris[count.index] protocol = "http" circuitBreaker = { rules = [ { name = "breakerRule" failureCondition = { count = 1 interval = "PT1M" statusCodeRanges = [ { min = 429 max = 429 } ] errorReasons = ["timeout"] } tripDuration = "PT1M" acceptRetryAfter = true } ] } } }) response_export_values = ["*"] } output "backend_names" { value = [for i in range(0, length(var.backend_uris)) : azapi_resource.backend[i].name] } ================================================ FILE: scenarios/workload-genai/terraform/modules/apim_policies/backends/providers.tf ================================================ terraform { required_providers { azapi = { source = "azure/azapi" version = "~> 1.0" } } } ================================================ FILE: scenarios/workload-genai/terraform/modules/apim_policies/lb_pool/lb-pool.tf ================================================ variable "api_management_service_name" { type = string } variable "backends" { type = list(string) } # variable "backendUris" { # type = list(string) # } variable "resource_group_name" { type = string } data "azurerm_api_management" "apiManagementService" { name = var.api_management_service_name resource_group_name = var.resource_group_name } resource "azapi_resource" "aoai_lb_pool" { type = "Microsoft.ApiManagement/service/backends@2023-09-01-preview" name = "aoai-lb-pool" parent_id = data.azurerm_api_management.apiManagementService.id schema_validation_enabled = false body = jsonencode({ properties = { title = "aoai-lb-pool" type = "Pool" pool = { services = [ { id = "/backends/${var.backends[0]}" priority = 1 weight = 1 }, { id = "/backends/${var.backends[1]}" priority = 2 weight = 2 }, { id = "/backends/${var.backends[2]}" priority = 1 weight = 3 } ] } } }) } ================================================ FILE: scenarios/workload-genai/terraform/modules/apim_policies/lb_pool/providers.tf ================================================ terraform { required_providers { azapi = { source = "azure/azapi" version = "~> 1.0" } } } ================================================ FILE: scenarios/workload-genai/terraform/modules/apim_policies/outputs.tf ================================================ output "apiManagementServiceName" { description = "The name of the API Management service instance" value = var.apiManagementServiceName } # This is in bicep - but we are using AZ CLI in the deployment script # to get the key instead of exposing it in the output # output "apiManagementAzureOpenAIProductSubscriptionKey" { # value = azurerm_api_management_subscription.example.primary_key # description = "The primary key of the Azure OpenAI product subscription." # } ================================================ FILE: scenarios/workload-genai/terraform/modules/apim_policies/variables.tf ================================================ variable "resourceGroupName" { type = string description = "The name of the resource group" } variable "apiManagementServiceName" { description = "The name of the API Management service instance" type = string } variable "ptuDeploymentOneBaseUrl" { description = "The base url of the first Azure Open AI Service PTU deployment" type = string } variable "payAsYouGoDeploymentOneBaseUrl" { description = "The base url of the first Azure Open AI Service Pay-As-You-Go deployment" type = string } variable "payAsYouGoDeploymentTwoBaseUrl" { description = "The base url of the second Azure Open AI Service Pay-As-You-Go deployment" type = string } variable "eventHubNamespaceName" { description = "The name of the Event Hub Namespace to log to" type = string } variable "eventHubName" { description = "The name of the Event Hub to log utilization data to" type = string } variable "apimIdentityName" { description = "The name of the API Management Identity" type = string } variable "location" { description = "The location of the resource group" type = string } variable "openaiResourceGroupName" { description = "The name of the resource group for the OpenAI service" type = string } ================================================ FILE: scenarios/workload-genai/terraform/modules/eventhub/eventhub.tf ================================================ resource "azurerm_eventhub_namespace" "eventHubNamespace" { name = var.eventHubNamespaceName location = var.location resource_group_name = var.openaiResourceGroupName sku = var.eventHubSku capacity = 1 auto_inflate_enabled = false } resource "azurerm_eventhub" "eventHub" { name = var.eventHubName namespace_name = azurerm_eventhub_namespace.eventHubNamespace.name resource_group_name = var.openaiResourceGroupName partition_count = 1 message_retention = 7 } data "azurerm_user_assigned_identity" "apimIdentity" { name = var.apimIdentityName resource_group_name = var.apimResourceGroupName } data "azurerm_role_definition" "eventHubsDataSenderRoleDefinition" { name = "Azure Event Hubs Data Sender" } resource "azurerm_role_assignment" "assignEventHubsDataSenderToApiManagement" { scope = azurerm_eventhub_namespace.eventHubNamespace.id role_definition_name = data.azurerm_role_definition.eventHubsDataSenderRoleDefinition.name principal_id = data.azurerm_user_assigned_identity.apimIdentity.principal_id } ================================================ FILE: scenarios/workload-genai/terraform/modules/eventhub/outputs.tf ================================================ output "eventHubNamespaceName" { description = "The name of the Event Hub Namespace." value = azurerm_eventhub_namespace.eventHubNamespace.name } output "eventHubName" { description = "The name of the Event Hub." value = azurerm_eventhub.eventHub.name } ================================================ FILE: scenarios/workload-genai/terraform/modules/eventhub/variables.tf ================================================ variable "eventHubNamespaceName" { description = "The name of the Event Hub Namespace" type = string } variable "eventHubName" { description = "The name of the Event Hub" type = string } variable "eventHubSku" { description = "The messaging tier for Event Hub Namespace." type = string default = "Standard" } variable "apimIdentityName" { type = string } variable "apimResourceGroupName" { type = string } variable "openaiResourceGroupName" { type = string } variable "location" { description = "Location for all resources." type = string default = "eastus" } ================================================ FILE: scenarios/workload-genai/terraform/modules/openai/openai.tf ================================================ data "azurerm_user_assigned_identity" "apimIdentity" { name = var.apimIdentityName resource_group_name = var.apimResourceGroupName } data "azurerm_subscription" "primary" { } resource "azurerm_cognitive_account" "openai" { name = var.name location = var.location resource_group_name = var.resource_group_name kind = "OpenAI" custom_subdomain_name = var.custom_subdomain_name sku_name = var.sku_name public_network_access_enabled = var.public_network_access_enabled tags = var.tags identity { type = "SystemAssigned" } lifecycle { ignore_changes = [ tags ] } } resource "azurerm_cognitive_deployment" "deployment" { for_each = { for deployment in var.deployments : deployment.name => deployment } name = each.key cognitive_account_id = azurerm_cognitive_account.openai.id model { format = "OpenAI" name = each.value.model.name version = each.value.model.version } scale { type = "Standard" } } data "azurerm_role_definition" "cognitiveServicesOpenAIUser" { name = "Cognitive Services OpenAI User" scope = data.azurerm_subscription.primary.id } resource "azurerm_role_assignment" "roleAssignment" { scope = azurerm_cognitive_account.openai.id role_definition_id = data.azurerm_role_definition.cognitiveServicesOpenAIUser.id principal_id = data.azurerm_user_assigned_identity.apimIdentity.principal_id } ================================================ FILE: scenarios/workload-genai/terraform/modules/openai/outputs.tf ================================================ output "id" { value = azurerm_cognitive_account.openai.id description = "Specifies the resource id of the log analytics workspace" } output "location" { value = azurerm_cognitive_account.openai.location description = "Specifies the location of the log analytics workspace" } output "name" { value = azurerm_cognitive_account.openai.name description = "Specifies the name of the log analytics workspace" } output "resource_group_name" { value = azurerm_cognitive_account.openai.resource_group_name description = "Specifies the name of the resource group that contains the log analytics workspace" } output "endpoint" { value = azurerm_cognitive_account.openai.endpoint description = "Specifies the endpoint of the Azure OpenAI Service." } # This is in bicep - but we are using AZ CLI in the deployment script # to get the key instead of exposing it in the output # output "primary_access_key" { # value = azurerm_cognitive_account.openai.primary_access_key # sensitive = true # description = "Specifies the primary access key of the Azure OpenAI Service." # } # This is in bicep - but we are using AZ CLI in the deployment script # to get the key instead of exposing it in the output # output "secondary_access_key" { # value = azurerm_cognitive_account.openai.secondary_access_key # sensitive = true # description = "Specifies the secondary access key of the Azure OpenAI Service." # } ================================================ FILE: scenarios/workload-genai/terraform/modules/openai/variables.tf ================================================ variable "resource_group_name" { description = "(Required) Specifies the resource group name" type = string } variable "location" { description = "(Required) Specifies the location of the Azure OpenAI Service" type = string } variable "name" { description = "(Required) Specifies the name of the Azure OpenAI Service" type = string } variable "sku_name" { description = "(Optional) Specifies the sku name for the Azure OpenAI Service" type = string default = "S0" } variable "tags" { description = "(Optional) Specifies the tags of the Azure OpenAI Service" type = map(any) default = {} } variable "custom_subdomain_name" { description = "(Optional) Specifies the custom subdomain name of the Azure OpenAI Service" type = string } variable "public_network_access_enabled" { description = "(Optional) Specifies whether public network access is allowed for the Azure OpenAI Service" type = bool default = false } variable "deployments" { description = "(Optional) Specifies the deployments of the Azure OpenAI Service" type = list(object({ name = string model = object({ name = string version = string }) rai_policy_name = string })) default = [ { name = "gpt-35-turbo" model = { name = "gpt-35-turbo" version = "0301" } rai_policy_name = "" } ] } variable "apimIdentityName" { description = "The name of the API Management Identity" type = string } variable "apimResourceGroupName" { description = "The name of the resource group for the API Management Identity" type = string } ================================================ FILE: scenarios/workload-genai/terraform/modules/private_dns_zone/outputs.tf ================================================ output "id" { description = "Specifies the resource id of the private dns zone" value = azurerm_private_dns_zone.private_dns_zone.id } ================================================ FILE: scenarios/workload-genai/terraform/modules/private_dns_zone/privatednszone.tf ================================================ resource "azurerm_private_dns_zone" "private_dns_zone" { name = var.name resource_group_name = var.resource_group_name tags = var.tags lifecycle { ignore_changes = [ tags ] } } resource "azurerm_private_dns_zone_virtual_network_link" "link" { name = "link_to_${lower(basename(var.virtual_networks_to_link_id))}" resource_group_name = var.resource_group_name private_dns_zone_name = azurerm_private_dns_zone.private_dns_zone.name virtual_network_id = var.virtual_networks_to_link_id lifecycle { ignore_changes = [ tags ] } } ================================================ FILE: scenarios/workload-genai/terraform/modules/private_dns_zone/variables.tf ================================================ variable "name" { description = "(Required) Specifies the name of the private dns zone" type = string } variable "resource_group_name" { description = "(Required) Specifies the resource group name of the private dns zone" type = string } variable "tags" { description = "(Optional) Specifies the tags of the private dns zone" default = {} } variable "virtual_networks_to_link_id" { description = "(Optional) Specifies the virtual networks id to which create a virtual network link" type = string } ================================================ FILE: scenarios/workload-genai/terraform/modules/private_endpoint/outputs.tf ================================================ output "id" { description = "Specifies the resource id of the private endpoint." value = azurerm_private_endpoint.private_endpoint.id } output "private_dns_zone_group" { description = "Specifies the private dns zone group of the private endpoint." value = azurerm_private_endpoint.private_endpoint.private_dns_zone_group } output "private_dns_zone_configs" { description = "Specifies the private dns zone(s) configuration" value = azurerm_private_endpoint.private_endpoint.private_dns_zone_configs } ================================================ FILE: scenarios/workload-genai/terraform/modules/private_endpoint/privateendpoint.tf ================================================ resource "azurerm_private_endpoint" "private_endpoint" { name = var.name location = var.location resource_group_name = var.resource_group_name subnet_id = var.subnet_id tags = var.tags private_service_connection { name = "${var.name}Connection" private_connection_resource_id = var.private_connection_resource_id is_manual_connection = var.is_manual_connection subresource_names = try([var.subresource_name], null) request_message = try(var.request_message, null) } private_dns_zone_group { name = var.private_dns_zone_group_name private_dns_zone_ids = var.private_dns_zone_group_ids } lifecycle { ignore_changes = [ tags ] } } ================================================ FILE: scenarios/workload-genai/terraform/modules/private_endpoint/variables.tf ================================================ variable "name" { description = "(Required) Specifies the name of the private endpoint. Changing this forces a new resource to be created." type = string } variable "resource_group_name" { description = "(Required) The name of the resource group. Changing this forces a new resource to be created." type = string } variable "private_connection_resource_id" { description = "(Required) Specifies the resource id of the private link service" type = string } variable "location" { description = "(Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created." type = string } variable "subnet_id" { description = "(Required) Specifies the resource id of the subnet" type = string } variable "is_manual_connection" { description = "(Optional) Specifies whether the private endpoint connection requires manual approval from the remote resource owner." type = string default = false } variable "subresource_name" { description = "(Optional) Specifies a subresource name which the Private Endpoint is able to connect to." type = string default = null } variable "request_message" { description = "(Optional) Specifies a message passed to the owner of the remote resource when the private endpoint attempts to establish the connection to the remote resource." type = string default = null } variable "private_dns_zone_group_name" { description = "(Required) Specifies the Name of the Private DNS Zone Group. Changing this forces a new private_dns_zone_group resource to be created." type = string } variable "private_dns_zone_group_ids" { description = "(Required) Specifies the list of Private DNS Zones to include within the private_dns_zone_group." type = list(string) } variable "tags" { description = "(Optional) Specifies the tags of the network security group" default = {} } variable "private_dns" { default = {} } ================================================ FILE: scenarios/workload-genai/terraform/provider.tf ================================================ terraform { # for storage backends, see backend.tf.sample required_providers { azurerm = { source = "hashicorp/azurerm" version = "~> 3.1" } random = { source = "hashicorp/random" version = "~> 3.6.0" } azapi = { source = "azure/azapi" } } } # Configure the Microsft Azure provider provider "azurerm" { features { resource_group { prevent_deletion_if_contains_resources = false } } use_oidc = true } provider "azapi" { } ================================================ FILE: scenarios/workload-genai/terraform/variables.tf ================================================ variable "location" { type = string description = "The Azure location in which the deployment is happening" default = "eastus2" } variable "workloadName" { type = string description = "A suffix for naming" default = "apimdemo" } variable "environment" { type = string description = "Environment" default = "dev" } variable "identifier" { description = "The identifier for the resource deployments" type = string } variable "tags" { description = "(Optional) Specifies tags for all the resources" default = {} } variable "log_analytics_workspace_name" { description = "Specifies the name of the log analytics workspace" default = "Workspace" type = string } variable "vnet_name" { description = "Specifies the name of the virtual network" default = "VNet" type = string } variable "vnet_address_space" { description = "Specifies the address prefix of the virtual network" default = ["10.0.0.0/16"] type = list(string) } variable "privateEndpointAddressPrefix" { description = "Private Endpoint Address Prefix" type = string default = "10.2.5.0/24" } variable "internal_load_balancer_enabled" { description = "(Optional) specifies whether the Azure Container Apps Environment operate in Internal Load Balancing Mode? Defaults to false. Changing this forces a new resource to be created." type = bool default = false } variable "openai_name" { description = "(Required) Specifies the name of the Azure OpenAI Service" type = string default = "OpenAI" } variable "openai_sku_name" { description = "(Optional) Specifies the sku name for the Azure OpenAI Service" type = string default = "S0" } variable "openai_custom_subdomain_name" { description = "(Optional) Specifies the custom subdomain name of the Azure OpenAI Service" type = string nullable = true default = "" } variable "openai_public_network_access_enabled" { description = "(Optional) Specifies whether public network access is allowed for the Azure OpenAI Service" type = bool default = false } variable "openai_deployments" { description = "(Optional) Specifies the deployments of the Azure OpenAI Service" type = list(object({ name = string model = object({ name = string version = string }) rai_policy_name = string })) default = [ { name = "gpt-4o-mini" model = { name = "gpt-4o-mini" version = "2024-07-18" } rai_policy_name = "" }, { name = "text-embedding-ada-002" model = { name = "text-embedding-ada-002" version = "2" } rai_policy_name = "" } ] } variable "workload_managed_identity_name" { description = "(Required) Specifies the name of the workload user-defined managed identity" type = string default = "WorkloadIdentity" } variable "eventHubName" { description = "The name of the Event Hub to log utilization data to" type = string default = "apim-utilization-reporting" } variable "apimIdentityName" { description = "The name of the API Management Identity" type = string default = "apimIdentity" }