initial commit
This commit is contained in:
Родитель
83a286c1b3
Коммит
d0722d75b3
|
@ -0,0 +1,350 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.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
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# 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
|
||||
# Note: 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
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable 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
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# 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
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# 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
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# 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/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
103
README.md
103
README.md
|
@ -1,14 +1,101 @@
|
|||
# Project
|
||||
# Teams Live Event Traffic Distributor
|
||||
|
||||
> This repo has been populated by an initial template to help get you started. Please
|
||||
> make sure to update the content to build a great experience for community-building.
|
||||
## What it is
|
||||
|
||||
As the maintainer of this project, please make a few updates:
|
||||
This solution was built to load balance users between a number of Microsoft Teams Live Events, since one Live Event can only host a couple of thousand clients (current limit of 20K attendees per Live Event stream) - For Live Events that need to go beyond this limit, the recommendation as of now is to publish the produced content across multiple streams, each of them having a different URL. This situation brings some complexity in terms of communication and management of the event as it requires to split the audience in pools of 20K users.
|
||||
|
||||
- Improving this README.MD file to provide a great experience
|
||||
- Updating SUPPORT.MD with content about this project's support experience
|
||||
- Understanding the security reporting process in SECURITY.MD
|
||||
- Remove this section from the README
|
||||
More info on setting-up a Teams Live Event can be found [here](https://docs.microsoft.com/en-us/microsoftteams/teams-live-events/plan-for-teams-live-events).
|
||||
|
||||
The following solution covers this challenge by automatically distributing the Live Event URLs to the users from a unique URL. By default, this is the URL of Azure Front Door, e.g. `myliveevent.azurefd.net`, instead of a URL to the Teams Event directly. To achieve this load balancing, API Management is being used to randomly and evenly redirect client requests, using HTTP 302 status code, to one of the URL of the actual Live Event. Also, to manage the capacity and resiliency of the solution, we recommend to have a spare stream - e.g. for 100K participants, you'll need 6 streams (6 x 20K = 120K users max)
|
||||
|
||||
|
||||
Note: The unique URL can be changed by either using a URL shortener like Bitly or also by adding your own custom domain to Front Door (e.g. `myliveevent.contoso.com`) - Check the documentation to [Configure HTTPS on a Front Door custom domain](https://docs.microsoft.com/en-us/azure/frontdoor/front-door-custom-domain-https)
|
||||
|
||||
## Azure Components
|
||||
|
||||
The solution deploys the following components:
|
||||
|
||||
- Azure Front Door for global load balancing and failover
|
||||
- 2x Azure API Management in Consumption tier, in two different regions for resiliency
|
||||
- Azure Application Insights including on Azure Portal Dashboard for monitoring
|
||||
- 2x Azure Storage Accounts with one Table storage each. These are only being used (and only will incur costs) when using the table-storage mode for a high number of backend URLs
|
||||
|
||||
<p align="center">
|
||||
<br>
|
||||
<img src="./media/Azure-ARM-Template.png" alt="Azure components deployed to redirect users" width="600"/>
|
||||
<br><br>
|
||||
<img src="./media/Live-Event-Multi-streams.png" alt="Teams Live Event & multi-streams setup" width="600"/>
|
||||
<br><br>
|
||||
<img src="./media/workflow.png" alt="Accessing a multi-stream Teams Live Event" width="300"/>
|
||||
</p>
|
||||
|
||||
## Alternative use cases
|
||||
|
||||
While the solution was originally built for Teams Live Events, it can easily be repurposed for any kind of similar load balancing where the backends are hosted on the same domain.
|
||||
|
||||
## How to use
|
||||
|
||||
### Optional: Make changes and build Bicep
|
||||
This solution uses [bicep](https://github.com/Azure/bicep) templates which are then compiled into ARM templates. If you wish to make any changes, do so in the bicep templates and compile it. It is not recommended to modify the ARM template (main.json). If you didn't make any changes, you can just use the main.json ARM template file from the repo which was already generated.
|
||||
|
||||
### Deploy to Azure via CLI
|
||||
Create a resource group (change the location based on your needs)
|
||||
```
|
||||
az group create -n myresource-group -l northeurope
|
||||
```
|
||||
|
||||
There are three different deployment types available, based on how many backend URLs you need to distribute traffic to. (the reason for this is a length limitation in API Management Policy definitions).
|
||||
|
||||
#### Default Policy-based mode - **this mode should be applicable to most users.**
|
||||
Use this when the list of all your backend URLs is not longer than approx. 14,000 characters. In this case the list of URLs is injected directly into the policy of API Management.
|
||||
|
||||
Use this command to deploy the ARM template - replace the **backends** parameter with your individual, comma-separated list of Event URLs and **locationSecondary** based on your preferences.
|
||||
```
|
||||
az deployment group create -g myresource-group --template-file .\main.json -p prefix=myprefix -p locationSecondary=westeurope -p loadBalancingMode=default -p backends="https://teams.microsoft.com/l/meetup-join/1,https://teams.microsoft.com/l/meetup-join/2,https://teams.microsoft.com/l/meetup-join/3"
|
||||
```
|
||||
|
||||
#### Table-storage mode - for high number of backend URLs
|
||||
In this case the URLs must be imported after the ARM template deployment into Table storage accounts and APIM fetches them from there. See below for details on this. \
|
||||
Use this command to deploy the ARM template - replace the **locationSecondary** parameter based on your preferences. Note that we are not specifying the URLs here yet and instead setting the parameter `loadBalancingMode=largeEvent`.
|
||||
```
|
||||
az deployment group create -g myresource-group --template-file .\main.json -p prefix=myprefix -p locationSecondary=westeurope -p loadBalancingMode=largeEvent
|
||||
```
|
||||
|
||||
After the deployment finished, you need to import the list of URLs into both Table storage accounts. Use the `import-urls-to-table.ps1` PowerShell script in the `testing` folder for this purpose.
|
||||
|
||||
|
||||
#### Language-based routing mode - distribute users to different backends based on their browser language (or a query parameter)
|
||||
To use this more, your list of backends must be formatted different. See the following example.
|
||||
**Note**: This mode currently only supports a list of URLs that is not longer than approx. 14,000 characters.
|
||||
|
||||
Use this command to deploy the ARM template - replace the **backends** parameter with your individual, semicolon-separated list of Event URLs, starting with the language code. Within each language code you can have multiple URLs. In this case, those will be load-balanced as well. There always needs to be at least one URL for English (`en`) as this is used as the fallback option. Also, update **locationSecondary** based on your preferences.
|
||||
```
|
||||
az deployment group create -g myresource-group --template-file .\main.json -p prefix=myprefix -p locationSecondary=westeurope -p loadBalancingMode=userLanguage -p backends="de=https://teams.microsoft.com/l/meetup-join/19%3ameeting_GERMAN1;fr=https://teams.microsoft.com/l/meetup-join/19%3ameeting_FRENCH1;en=https://teams.microsoft.com/l/meetup-join/19%3ameeting_ENGLISH1,https://teams.microsoft.com/l/meetup-join/19%3ameeting_ENGLISH2"
|
||||
```
|
||||
|
||||
In this mode, the browser request header `Accept-Language` is being used to determine which URL to use. Alternatively the query parameter `?lang=` can be set, which will then take precedence. For example: [https://{MYPREFIX}globalfrontdoor.azurefd.net?lang=fr]() to force French as the language.
|
||||
|
||||
|
||||
#### Deploy through the Azure Portal
|
||||
As an alternative to using command line, you can also deploy through the Azure Portal directly and set the parameters accordingly.
|
||||
|
||||
[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fsebader%2Fteams-distributor%2Fmain%2Fdeployment%2Fmain.json)
|
||||
|
||||
### Test
|
||||
Your initial URL - if you did not use a custom domain - will be like [https://{MYPREFIX}globalfrontdoor.azurefd.net]()- To test your setup, simply call this URL from a web browser and you should get redirected to one of your backend URLs. Try this a couple of times and you will see different forwarding targets. Note: Based on your browser or proxy settings, it might be that the forward target gets cached. In this case, just open a second browser (or use a private browsing tab).
|
||||
|
||||
_Note: After the first deployment it can take a couple of minutes until the Front Door URL goes lives and starts to your traffic_
|
||||
|
||||
## Costs
|
||||
(only provided as an example, as of Feb-2021)
|
||||
|
||||
Overall cost for this solution is pretty minimal. The only reoccurring billing (without any incoming traffic), is for the Front Door routing configuration. All other costs are purely based on incoming traffic / usage.
|
||||
|
||||
- API Management - Consumption tier: $3.50 per 1 million calls. And the first 1 million calls per Azure subscription are free. [Pricing](https://azure.microsoft.com/en-us/pricing/details/api-management/)
|
||||
- Front Door: $0.01 per GB incoming traffic, $0.17 per GB response traffic (Zone 1), $22 per month for the two routing rules. [Pricing](https://azure.microsoft.com/en-us/pricing/details/frontdoor/)
|
||||
- Application Insights: $2.88 per GB ingested data - and the first 5 GB per billing account are included per month. [Pricing](https://azure.microsoft.com/en-us/pricing/details/monitor/)
|
||||
|
||||
The ARM template allows you to deploy the Azure services only for the preparation and duration of the Live Event. With a typical timeframe of 5 days and 100K attendees, the estimated cost of the solution is less than $5.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
@ -0,0 +1,335 @@
|
|||
@description('Prefix for all resources to create uniqueness')
|
||||
param prefix string
|
||||
|
||||
@description('Region of the second API Management instance. Needs to be different than the location of the resource group which is being used as the primary location. Must support APIM Consumption tier.')
|
||||
param locationSecondary string
|
||||
|
||||
@allowed([
|
||||
'default'
|
||||
'userLanguage'
|
||||
'largeEvent'
|
||||
])
|
||||
@description('Which Load Balancing (LB) mode to use. Default: Random LB with a list of URL that does not exceed 15,000 characters. userLanguage: LB based on user browser language. largeEvent: Random LB with a list of URL that exceedss 15,000 characters (many, long URLs).')
|
||||
param loadBalancingMode string = 'default'
|
||||
|
||||
@description('Leave blank if you set the parameter loadBalancingMode to largeEvent. Otherwise: If mode=default: Comma-separated list of backend URLs to which incoming requests will be forwarded to in a random fashion. For example like: https://teams.microsoft.com/l/meetup-join/1,https://teams.microsoft.com/l/meetup-join/2 If mode=userLanguage: List of backend URLs, split by language to which incoming requests will be forwarded based on their browser language and, if there are multiple links per language in a random fashion. Uses English as the fallback. For example like: de=https://teams.microsoft.com/l/meetup-join/19%3ameeting_GERMAN1;fr=https://teams.microsoft.com/l/meetup-join/19%3ameeting_FRENCH1;en=https://teams.microsoft.com/l/meetup-join/19%3ameeting_ENGLISH1,https://teams.microsoft.com/l/meetup-join/19%3ameeting_ENGLISH2')
|
||||
param backends string = ''
|
||||
|
||||
@description('API Management Publisher Name')
|
||||
param apimPublisherName string = 'Contoso Admin'
|
||||
|
||||
@description('API Management Publisher Email Address')
|
||||
param apimPublisherEmail string = 'noreply@contoso.com'
|
||||
|
||||
@description('No need to change. ID to be added to the deployment names, such as the run ID of a pipeline. Default to UTC-now timestamp')
|
||||
param deploymentId string = utcNow()
|
||||
|
||||
var location = resourceGroup().location
|
||||
|
||||
var frontDoorName = '${prefix}globalfrontdoor'
|
||||
var frontdoor_default_dns_name = '${frontDoorName}.azurefd.net'
|
||||
|
||||
resource appinsights 'Microsoft.Insights/components@2018-05-01-preview' = {
|
||||
name: '${prefix}appinsights'
|
||||
location: location
|
||||
kind: 'web'
|
||||
properties: {
|
||||
Application_Type: 'web'
|
||||
}
|
||||
}
|
||||
|
||||
var regions = [
|
||||
location
|
||||
locationSecondary
|
||||
]
|
||||
|
||||
module apim 'module_apim.bicep' = [for region in regions: {
|
||||
name: 'apim-${region}-${deploymentId}'
|
||||
params: {
|
||||
applicationInsightsName: appinsights.name
|
||||
location: region
|
||||
backends: backends
|
||||
prefix: prefix
|
||||
publisherEmail: apimPublisherEmail
|
||||
publisherName: apimPublisherName
|
||||
loadBalancingMode: loadBalancingMode
|
||||
}
|
||||
}]
|
||||
|
||||
resource frontdoor 'Microsoft.Network/frontDoors@2020-05-01' = {
|
||||
name: frontDoorName
|
||||
location: 'Global'
|
||||
properties: {
|
||||
backendPools: [
|
||||
{
|
||||
name: 'BackendAPIMs'
|
||||
properties: {
|
||||
backends: [for index in range(0, length(regions)): {
|
||||
address: apim[index].outputs.apimHostname
|
||||
backendHostHeader: apim[index].outputs.apimHostname
|
||||
httpPort: 80
|
||||
httpsPort: 443
|
||||
priority: 1
|
||||
weight: 50
|
||||
}]
|
||||
healthProbeSettings: {
|
||||
id: '${resourceId('Microsoft.Network/frontDoors', frontDoorName)}/healthProbeSettings/HealthProbeSetting'
|
||||
}
|
||||
loadBalancingSettings: {
|
||||
id: '${resourceId('Microsoft.Network/frontDoors', frontDoorName)}/loadBalancingSettings/LoadBalancingSettings'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
frontendEndpoints: [
|
||||
{
|
||||
name: 'DefaultFrontendEndpoint'
|
||||
properties: {
|
||||
hostName: frontdoor_default_dns_name
|
||||
sessionAffinityEnabledState: 'Disabled'
|
||||
}
|
||||
}
|
||||
/*
|
||||
// Enable this if you have a custom domain name available
|
||||
{
|
||||
name: 'CustomDomainFrontendEndpoint'
|
||||
properties: {
|
||||
hostName: customDomainName_frontdoor
|
||||
sessionAffinityEnabledState: 'Disabled'
|
||||
}
|
||||
}
|
||||
*/
|
||||
]
|
||||
routingRules: [
|
||||
{
|
||||
name: 'HTTPSRedirect'
|
||||
properties: {
|
||||
acceptedProtocols: [
|
||||
'Http'
|
||||
]
|
||||
patternsToMatch: [
|
||||
'/*'
|
||||
]
|
||||
routeConfiguration: {
|
||||
'@odata.type': '#Microsoft.Azure.FrontDoor.Models.FrontdoorRedirectConfiguration'
|
||||
redirectProtocol: 'HttpsOnly'
|
||||
redirectType: 'Moved'
|
||||
}
|
||||
frontendEndpoints: [
|
||||
{
|
||||
id: '${resourceId('Microsoft.Network/frontDoors', frontDoorName)}/frontendEndpoints/DefaultFrontendEndpoint'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
{
|
||||
name: 'DefaultBackendForwardRule'
|
||||
properties: {
|
||||
acceptedProtocols: [
|
||||
'Https'
|
||||
]
|
||||
patternsToMatch: [
|
||||
'/*'
|
||||
]
|
||||
routeConfiguration: {
|
||||
'@odata.type': '#Microsoft.Azure.FrontDoor.Models.FrontdoorForwardingConfiguration'
|
||||
backendPool: {
|
||||
id: '${resourceId('Microsoft.Network/frontDoors', frontDoorName)}/backendPools/BackendAPIMs'
|
||||
}
|
||||
forwardingProtocol: 'HttpsOnly'
|
||||
}
|
||||
frontendEndpoints: [
|
||||
{
|
||||
id: '${resourceId('Microsoft.Network/frontDoors', frontDoorName)}/frontendEndpoints/DefaultFrontendEndpoint'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
healthProbeSettings: [
|
||||
{
|
||||
name: 'HealthProbeSetting'
|
||||
properties: {
|
||||
healthProbeMethod: 'HEAD'
|
||||
path: '/healthz'
|
||||
protocol: 'Https'
|
||||
intervalInSeconds: 30
|
||||
}
|
||||
}
|
||||
]
|
||||
loadBalancingSettings: [
|
||||
{
|
||||
name: 'LoadBalancingSettings'
|
||||
properties: {
|
||||
additionalLatencyMilliseconds: 500
|
||||
sampleSize: 4
|
||||
successfulSamplesRequired: 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource dashboard 'Microsoft.Portal/dashboards@2015-08-01-preview' = {
|
||||
name: guid(resourceGroup().name, prefix, 'dashboard')
|
||||
location: location
|
||||
tags: {
|
||||
'hidden-title': 'Teams Distributor Statistics'
|
||||
}
|
||||
properties: {
|
||||
lenses: {
|
||||
'0': {
|
||||
order: 0
|
||||
parts: {
|
||||
'0': {
|
||||
position: {
|
||||
colSpan: 10
|
||||
rowSpan: 5
|
||||
x: 0
|
||||
y: 0
|
||||
}
|
||||
metadata: {
|
||||
type: 'Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart'
|
||||
inputs: [
|
||||
{
|
||||
name: 'Scope'
|
||||
value: {
|
||||
resourceIds: [
|
||||
appinsights.id
|
||||
]
|
||||
}
|
||||
}
|
||||
{
|
||||
name: 'Dimensions'
|
||||
value: {
|
||||
xAxis: {
|
||||
name: 'timestamp'
|
||||
type: 'datetime'
|
||||
}
|
||||
yAxis: [
|
||||
{
|
||||
name: 'Number of Requests'
|
||||
type: 'long'
|
||||
}
|
||||
]
|
||||
splitBy: [
|
||||
{
|
||||
name: 'Backend'
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
aggregation: 'Sum'
|
||||
}
|
||||
}
|
||||
{
|
||||
name: 'PartId'
|
||||
value: guid(resourceGroup().name, 'part0')
|
||||
}
|
||||
{
|
||||
name: 'Version'
|
||||
value: '2.0'
|
||||
}
|
||||
{
|
||||
name: 'TimeRange'
|
||||
value: 'PT30M'
|
||||
}
|
||||
{
|
||||
name: 'Query'
|
||||
value: 'set query_bin_auto_size=5m;\r\nrequests\r\n| extend Backend=tostring(customDimensions[\'Response-location\'])\r\n| where Backend != ""\r\n| summarize [\'Number of Requests\']=count() by Backend, bin_auto(timestamp)\r\n| render areachart'
|
||||
}
|
||||
{
|
||||
name: 'PartTitle'
|
||||
value: 'Forwarded Requests per Backend'
|
||||
}
|
||||
{
|
||||
name: 'PartSubTitle'
|
||||
value: 'On 5-Minute aggregation'
|
||||
}
|
||||
{
|
||||
name: 'ControlType'
|
||||
value: 'FrameControlChart'
|
||||
}
|
||||
{
|
||||
name: 'SpecificChart'
|
||||
value: 'StackedArea'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
'1': {
|
||||
position: {
|
||||
colSpan: 6
|
||||
rowSpan: 5
|
||||
x: 10
|
||||
y: 0
|
||||
}
|
||||
metadata: {
|
||||
type: 'Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart'
|
||||
inputs: [
|
||||
{
|
||||
name: 'Scope'
|
||||
value: {
|
||||
resourceIds: [
|
||||
appinsights.id
|
||||
]
|
||||
}
|
||||
}
|
||||
{
|
||||
name: 'Dimensions'
|
||||
value: {
|
||||
xAxis: {
|
||||
name: 'Region'
|
||||
type: 'string'
|
||||
}
|
||||
yAxis: [
|
||||
{
|
||||
name: 'Count'
|
||||
type: 'long'
|
||||
}
|
||||
]
|
||||
splitBy: []
|
||||
aggregation: 'Sum'
|
||||
}
|
||||
}
|
||||
{
|
||||
name: 'PartId'
|
||||
value: guid(resourceGroup().name, 'part1')
|
||||
}
|
||||
{
|
||||
name: 'Version'
|
||||
value: '2.0'
|
||||
}
|
||||
{
|
||||
name: 'TimeRange'
|
||||
value: 'PT30M'
|
||||
}
|
||||
{
|
||||
name: 'Query'
|
||||
value: 'requests\r\n| summarize Count=count() by Region=tostring(customDimensions.Region)\r\n| render piechart'
|
||||
}
|
||||
{
|
||||
name: 'PartTitle'
|
||||
value: 'Handled requests per APIM Region'
|
||||
}
|
||||
{
|
||||
name: 'PartSubTitle'
|
||||
value: 'As load-balanced by Front Door'
|
||||
}
|
||||
{
|
||||
name: 'ControlType'
|
||||
value: 'FrameControlChart'
|
||||
}
|
||||
{
|
||||
name: 'SpecificChart'
|
||||
value: 'Pie'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output frontDoorUrl string = frontdoor_default_dns_name
|
|
@ -0,0 +1,755 @@
|
|||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"prefix": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Prefix for all resources to create uniqueness"
|
||||
}
|
||||
},
|
||||
"locationSecondary": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Region of the second API Management instance. Needs to be different than the location of the resource group which is being used as the primary location. Must support APIM Consumption tier."
|
||||
}
|
||||
},
|
||||
"loadBalancingMode": {
|
||||
"type": "string",
|
||||
"defaultValue": "default",
|
||||
"metadata": {
|
||||
"description": "Which Load Balancing (LB) mode to use. Default: Random LB with a list of URL that does not exceed 15,000 characters. userLanguage: LB based on user browser language. largeEvent: Random LB with a list of URL that exceedss 15,000 characters (many, long URLs)."
|
||||
},
|
||||
"allowedValues": [
|
||||
"default",
|
||||
"userLanguage",
|
||||
"largeEvent"
|
||||
]
|
||||
},
|
||||
"backends": {
|
||||
"type": "string",
|
||||
"defaultValue": "",
|
||||
"metadata": {
|
||||
"description": "Leave blank if you set the parameter loadBalancingMode to largeEvent. Otherwise: If mode=default: Comma-separated list of backend URLs to which incoming requests will be forwarded to in a random fashion. For example like: https://teams.microsoft.com/l/meetup-join/1,https://teams.microsoft.com/l/meetup-join/2 If mode=userLanguage: List of backend URLs, split by language to which incoming requests will be forwarded based on their browser language and, if there are multiple links per language in a random fashion. Uses English as the fallback. For example like: de=https://teams.microsoft.com/l/meetup-join/19%3ameeting_GERMAN1;fr=https://teams.microsoft.com/l/meetup-join/19%3ameeting_FRENCH1;en=https://teams.microsoft.com/l/meetup-join/19%3ameeting_ENGLISH1,https://teams.microsoft.com/l/meetup-join/19%3ameeting_ENGLISH2"
|
||||
}
|
||||
},
|
||||
"apimPublisherName": {
|
||||
"type": "string",
|
||||
"defaultValue": "Contoso Admin",
|
||||
"metadata": {
|
||||
"description": "API Management Publisher Name"
|
||||
}
|
||||
},
|
||||
"apimPublisherEmail": {
|
||||
"type": "string",
|
||||
"defaultValue": "noreply@contoso.com",
|
||||
"metadata": {
|
||||
"description": "API Management Publisher Email Address"
|
||||
}
|
||||
},
|
||||
"deploymentId": {
|
||||
"type": "string",
|
||||
"defaultValue": "[utcNow()]",
|
||||
"metadata": {
|
||||
"description": "No need to change. ID to be added to the deployment names, such as the run ID of a pipeline. Default to UTC-now timestamp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions": [],
|
||||
"variables": {
|
||||
"location": "[resourceGroup().location]",
|
||||
"frontDoorName": "[format('{0}globalfrontdoor', parameters('prefix'))]",
|
||||
"frontdoor_default_dns_name": "[format('{0}.azurefd.net', variables('frontDoorName'))]",
|
||||
"regions": [
|
||||
"[variables('location')]",
|
||||
"[parameters('locationSecondary')]"
|
||||
]
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "Microsoft.Insights/components",
|
||||
"apiVersion": "2018-05-01-preview",
|
||||
"name": "[format('{0}appinsights', parameters('prefix'))]",
|
||||
"location": "[variables('location')]",
|
||||
"kind": "web",
|
||||
"properties": {
|
||||
"Application_Type": "web"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Network/frontDoors",
|
||||
"apiVersion": "2020-05-01",
|
||||
"name": "[variables('frontDoorName')]",
|
||||
"location": "Global",
|
||||
"properties": {
|
||||
"backendPools": [
|
||||
{
|
||||
"name": "BackendAPIMs",
|
||||
"properties": {
|
||||
"copy": [
|
||||
{
|
||||
"name": "backends",
|
||||
"count": "[length(range(0, length(variables('regions'))))]",
|
||||
"input": {
|
||||
"address": "[reference(resourceId('Microsoft.Resources/deployments', format('apim-{0}-{1}', variables('regions')[range(0, length(variables('regions')))[copyIndex('backends')]], parameters('deploymentId'))), '2019-10-01').outputs.apimHostname.value]",
|
||||
"backendHostHeader": "[reference(resourceId('Microsoft.Resources/deployments', format('apim-{0}-{1}', variables('regions')[range(0, length(variables('regions')))[copyIndex('backends')]], parameters('deploymentId'))), '2019-10-01').outputs.apimHostname.value]",
|
||||
"httpPort": 80,
|
||||
"httpsPort": 443,
|
||||
"priority": 1,
|
||||
"weight": 50
|
||||
}
|
||||
}
|
||||
],
|
||||
"healthProbeSettings": {
|
||||
"id": "[format('{0}/healthProbeSettings/HealthProbeSetting', resourceId('Microsoft.Network/frontDoors', variables('frontDoorName')))]"
|
||||
},
|
||||
"loadBalancingSettings": {
|
||||
"id": "[format('{0}/loadBalancingSettings/LoadBalancingSettings', resourceId('Microsoft.Network/frontDoors', variables('frontDoorName')))]"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"frontendEndpoints": [
|
||||
{
|
||||
"name": "DefaultFrontendEndpoint",
|
||||
"properties": {
|
||||
"hostName": "[variables('frontdoor_default_dns_name')]",
|
||||
"sessionAffinityEnabledState": "Disabled"
|
||||
}
|
||||
}
|
||||
],
|
||||
"routingRules": [
|
||||
{
|
||||
"name": "HTTPSRedirect",
|
||||
"properties": {
|
||||
"acceptedProtocols": [
|
||||
"Http"
|
||||
],
|
||||
"patternsToMatch": [
|
||||
"/*"
|
||||
],
|
||||
"routeConfiguration": {
|
||||
"@odata.type": "#Microsoft.Azure.FrontDoor.Models.FrontdoorRedirectConfiguration",
|
||||
"redirectProtocol": "HttpsOnly",
|
||||
"redirectType": "Moved"
|
||||
},
|
||||
"frontendEndpoints": [
|
||||
{
|
||||
"id": "[format('{0}/frontendEndpoints/DefaultFrontendEndpoint', resourceId('Microsoft.Network/frontDoors', variables('frontDoorName')))]"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "DefaultBackendForwardRule",
|
||||
"properties": {
|
||||
"acceptedProtocols": [
|
||||
"Https"
|
||||
],
|
||||
"patternsToMatch": [
|
||||
"/*"
|
||||
],
|
||||
"routeConfiguration": {
|
||||
"@odata.type": "#Microsoft.Azure.FrontDoor.Models.FrontdoorForwardingConfiguration",
|
||||
"backendPool": {
|
||||
"id": "[format('{0}/backendPools/BackendAPIMs', resourceId('Microsoft.Network/frontDoors', variables('frontDoorName')))]"
|
||||
},
|
||||
"forwardingProtocol": "HttpsOnly"
|
||||
},
|
||||
"frontendEndpoints": [
|
||||
{
|
||||
"id": "[format('{0}/frontendEndpoints/DefaultFrontendEndpoint', resourceId('Microsoft.Network/frontDoors', variables('frontDoorName')))]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"healthProbeSettings": [
|
||||
{
|
||||
"name": "HealthProbeSetting",
|
||||
"properties": {
|
||||
"healthProbeMethod": "HEAD",
|
||||
"path": "/healthz",
|
||||
"protocol": "Https",
|
||||
"intervalInSeconds": 30
|
||||
}
|
||||
}
|
||||
],
|
||||
"loadBalancingSettings": [
|
||||
{
|
||||
"name": "LoadBalancingSettings",
|
||||
"properties": {
|
||||
"additionalLatencyMilliseconds": 500,
|
||||
"sampleSize": 4,
|
||||
"successfulSamplesRequired": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependsOn": [
|
||||
"apim"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Portal/dashboards",
|
||||
"apiVersion": "2015-08-01-preview",
|
||||
"name": "[guid(resourceGroup().name, parameters('prefix'), 'dashboard')]",
|
||||
"location": "[variables('location')]",
|
||||
"tags": {
|
||||
"hidden-title": "Teams Distributor Statistics"
|
||||
},
|
||||
"properties": {
|
||||
"lenses": {
|
||||
"0": {
|
||||
"order": 0,
|
||||
"parts": {
|
||||
"0": {
|
||||
"position": {
|
||||
"colSpan": 10,
|
||||
"rowSpan": 5,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"metadata": {
|
||||
"type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "Scope",
|
||||
"value": {
|
||||
"resourceIds": [
|
||||
"[resourceId('Microsoft.Insights/components', format('{0}appinsights', parameters('prefix')))]"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Dimensions",
|
||||
"value": {
|
||||
"xAxis": {
|
||||
"name": "timestamp",
|
||||
"type": "datetime"
|
||||
},
|
||||
"yAxis": [
|
||||
{
|
||||
"name": "Number of Requests",
|
||||
"type": "long"
|
||||
}
|
||||
],
|
||||
"splitBy": [
|
||||
{
|
||||
"name": "Backend",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"aggregation": "Sum"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "PartId",
|
||||
"value": "[guid(resourceGroup().name, 'part0')]"
|
||||
},
|
||||
{
|
||||
"name": "Version",
|
||||
"value": "2.0"
|
||||
},
|
||||
{
|
||||
"name": "TimeRange",
|
||||
"value": "PT30M"
|
||||
},
|
||||
{
|
||||
"name": "Query",
|
||||
"value": "set query_bin_auto_size=5m;\r\nrequests\r\n| extend Backend=tostring(customDimensions['Response-location'])\r\n| where Backend != \"\"\r\n| summarize ['Number of Requests']=count() by Backend, bin_auto(timestamp)\r\n| render areachart"
|
||||
},
|
||||
{
|
||||
"name": "PartTitle",
|
||||
"value": "Forwarded Requests per Backend"
|
||||
},
|
||||
{
|
||||
"name": "PartSubTitle",
|
||||
"value": "On 5-Minute aggregation"
|
||||
},
|
||||
{
|
||||
"name": "ControlType",
|
||||
"value": "FrameControlChart"
|
||||
},
|
||||
{
|
||||
"name": "SpecificChart",
|
||||
"value": "StackedArea"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"1": {
|
||||
"position": {
|
||||
"colSpan": 6,
|
||||
"rowSpan": 5,
|
||||
"x": 10,
|
||||
"y": 0
|
||||
},
|
||||
"metadata": {
|
||||
"type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "Scope",
|
||||
"value": {
|
||||
"resourceIds": [
|
||||
"[resourceId('Microsoft.Insights/components', format('{0}appinsights', parameters('prefix')))]"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Dimensions",
|
||||
"value": {
|
||||
"xAxis": {
|
||||
"name": "Region",
|
||||
"type": "string"
|
||||
},
|
||||
"yAxis": [
|
||||
{
|
||||
"name": "Count",
|
||||
"type": "long"
|
||||
}
|
||||
],
|
||||
"splitBy": [],
|
||||
"aggregation": "Sum"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "PartId",
|
||||
"value": "[guid(resourceGroup().name, 'part1')]"
|
||||
},
|
||||
{
|
||||
"name": "Version",
|
||||
"value": "2.0"
|
||||
},
|
||||
{
|
||||
"name": "TimeRange",
|
||||
"value": "PT30M"
|
||||
},
|
||||
{
|
||||
"name": "Query",
|
||||
"value": "requests\r\n| summarize Count=count() by Region=tostring(customDimensions.Region)\r\n| render piechart"
|
||||
},
|
||||
{
|
||||
"name": "PartTitle",
|
||||
"value": "Handled requests per APIM Region"
|
||||
},
|
||||
{
|
||||
"name": "PartSubTitle",
|
||||
"value": "As load-balanced by Front Door"
|
||||
},
|
||||
{
|
||||
"name": "ControlType",
|
||||
"value": "FrameControlChart"
|
||||
},
|
||||
{
|
||||
"name": "SpecificChart",
|
||||
"value": "Pie"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Insights/components', format('{0}appinsights', parameters('prefix')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"copy": {
|
||||
"name": "apim",
|
||||
"count": "[length(variables('regions'))]"
|
||||
},
|
||||
"type": "Microsoft.Resources/deployments",
|
||||
"apiVersion": "2019-10-01",
|
||||
"name": "[format('apim-{0}-{1}', variables('regions')[copyIndex()], parameters('deploymentId'))]",
|
||||
"properties": {
|
||||
"expressionEvaluationOptions": {
|
||||
"scope": "inner"
|
||||
},
|
||||
"mode": "Incremental",
|
||||
"parameters": {
|
||||
"applicationInsightsName": {
|
||||
"value": "[format('{0}appinsights', parameters('prefix'))]"
|
||||
},
|
||||
"location": {
|
||||
"value": "[variables('regions')[copyIndex()]]"
|
||||
},
|
||||
"backends": {
|
||||
"value": "[parameters('backends')]"
|
||||
},
|
||||
"prefix": {
|
||||
"value": "[parameters('prefix')]"
|
||||
},
|
||||
"publisherEmail": {
|
||||
"value": "[parameters('apimPublisherEmail')]"
|
||||
},
|
||||
"publisherName": {
|
||||
"value": "[parameters('apimPublisherName')]"
|
||||
},
|
||||
"loadBalancingMode": {
|
||||
"value": "[parameters('loadBalancingMode')]"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"prefix": {
|
||||
"type": "string"
|
||||
},
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"publisherEmail": {
|
||||
"type": "string"
|
||||
},
|
||||
"publisherName": {
|
||||
"type": "string"
|
||||
},
|
||||
"backends": {
|
||||
"type": "string",
|
||||
"defaultValue": ""
|
||||
},
|
||||
"loadBalancingMode": {
|
||||
"type": "string",
|
||||
"defaultValue": "default",
|
||||
"allowedValues": [
|
||||
"default",
|
||||
"userLanguage",
|
||||
"largeEvent"
|
||||
]
|
||||
},
|
||||
"applicationInsightsName": {
|
||||
"type": "string"
|
||||
},
|
||||
"sasTokenStart": {
|
||||
"type": "string",
|
||||
"defaultValue": "[utcNow('yyyy-MM-ddTHH:mm:ssZ')]"
|
||||
},
|
||||
"sasTokenExpiry": {
|
||||
"type": "string",
|
||||
"defaultValue": "[dateTimeAdd(utcNow('u'), 'P2Y', 'yyyy-MM-ddTHH:mm:ssZ')]"
|
||||
}
|
||||
},
|
||||
"functions": [],
|
||||
"variables": {
|
||||
"useDefaultLb": "[equals(parameters('loadBalancingMode'), 'default')]",
|
||||
"useTableStorage": "[equals(parameters('loadBalancingMode'), 'largeEvent')]",
|
||||
"useLanguageRouter": "[equals(parameters('loadBalancingMode'), 'userLanguage')]",
|
||||
"tableName": "Urls",
|
||||
"accountSasProperties": {
|
||||
"signedServices": "t",
|
||||
"signedPermission": "rl",
|
||||
"signedResourceTypes": "o",
|
||||
"signedStart": "[parameters('sasTokenStart')]",
|
||||
"signedExpiry": "[parameters('sasTokenExpiry')]"
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "Microsoft.Storage/storageAccounts",
|
||||
"apiVersion": "2019-06-01",
|
||||
"name": "[format('stg{0}{1}', take(parameters('location'), 8), uniqueString(parameters('prefix'), parameters('location'), 'stg'))]",
|
||||
"location": "[parameters('location')]",
|
||||
"kind": "StorageV2",
|
||||
"sku": {
|
||||
"name": "Standard_ZRS",
|
||||
"tier": "Standard"
|
||||
},
|
||||
"properties": {
|
||||
"supportsHttpsTrafficOnly": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"condition": "[variables('useTableStorage')]",
|
||||
"type": "Microsoft.Storage/storageAccounts/tableServices/tables",
|
||||
"apiVersion": "2019-06-01",
|
||||
"name": "[format('{0}/default/{1}', format('stg{0}{1}', take(parameters('location'), 8), uniqueString(parameters('prefix'), parameters('location'), 'stg')), variables('tableName'))]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Storage/storageAccounts', format('stg{0}{1}', take(parameters('location'), 8), uniqueString(parameters('prefix'), parameters('location'), 'stg')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.ApiManagement/service",
|
||||
"apiVersion": "2020-06-01-preview",
|
||||
"name": "[format('{0}{1}apim', parameters('prefix'), parameters('location'))]",
|
||||
"location": "[parameters('location')]",
|
||||
"sku": {
|
||||
"name": "Consumption",
|
||||
"capacity": 0
|
||||
},
|
||||
"properties": {
|
||||
"publisherEmail": "[parameters('publisherEmail')]",
|
||||
"publisherName": "[parameters('publisherName')]",
|
||||
"customProperties": {
|
||||
"Microsoft.WindowsAzure.ApiManagement.Gateway.Protocols.Server.Http2": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.ApiManagement/service/apis",
|
||||
"apiVersion": "2020-06-01-preview",
|
||||
"name": "[format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location')))]",
|
||||
"properties": {
|
||||
"apiRevision": "1",
|
||||
"displayName": "GetBackend",
|
||||
"subscriptionRequired": false,
|
||||
"protocols": [
|
||||
"https"
|
||||
],
|
||||
"path": ""
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.ApiManagement/service', format('{0}{1}apim', parameters('prefix'), parameters('location')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.ApiManagement/service/apis",
|
||||
"apiVersion": "2020-06-01-preview",
|
||||
"name": "[format('{0}/apimhealthz', format('{0}{1}apim', parameters('prefix'), parameters('location')))]",
|
||||
"properties": {
|
||||
"apiRevision": "1",
|
||||
"displayName": "APIM Healthz",
|
||||
"subscriptionRequired": false,
|
||||
"protocols": [
|
||||
"https"
|
||||
],
|
||||
"path": "healthz"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.ApiManagement/service', format('{0}{1}apim', parameters('prefix'), parameters('location')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[variables('useDefaultLb')]",
|
||||
"type": "Microsoft.ApiManagement/service/apis/operations",
|
||||
"apiVersion": "2020-06-01-preview",
|
||||
"name": "[format('{0}/getbackendfrompolicy', format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location'))))]",
|
||||
"properties": {
|
||||
"displayName": "Get Backend From Policy",
|
||||
"method": "GET",
|
||||
"urlTemplate": "/"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.ApiManagement/service/apis', split(format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[0], split(format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[1])]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[variables('useTableStorage')]",
|
||||
"type": "Microsoft.ApiManagement/service/apis/operations",
|
||||
"apiVersion": "2020-06-01-preview",
|
||||
"name": "[format('{0}/getbackendfromtable', format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location'))))]",
|
||||
"properties": {
|
||||
"displayName": "Get Backend From Table Storage",
|
||||
"method": "GET",
|
||||
"urlTemplate": "/"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.ApiManagement/service/apis', split(format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[0], split(format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[1])]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[variables('useLanguageRouter')]",
|
||||
"type": "Microsoft.ApiManagement/service/apis/operations",
|
||||
"apiVersion": "2020-06-01-preview",
|
||||
"name": "[format('{0}/getbackendbylanguage', format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location'))))]",
|
||||
"properties": {
|
||||
"displayName": "Get Backend by User Language",
|
||||
"method": "GET",
|
||||
"urlTemplate": "/"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.ApiManagement/service/apis', split(format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[0], split(format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[1])]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.ApiManagement/service/apis/operations",
|
||||
"apiVersion": "2020-06-01-preview",
|
||||
"name": "[format('{0}/apimhealth', format('{0}/apimhealthz', format('{0}{1}apim', parameters('prefix'), parameters('location'))))]",
|
||||
"properties": {
|
||||
"displayName": "ApimHealth",
|
||||
"method": "HEAD",
|
||||
"urlTemplate": "/"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.ApiManagement/service/apis', split(format('{0}/apimhealthz', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[0], split(format('{0}/apimhealthz', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[1])]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[variables('useDefaultLb')]",
|
||||
"type": "Microsoft.ApiManagement/service/apis/operations/policies",
|
||||
"apiVersion": "2020-06-01-preview",
|
||||
"name": "[format('{0}/policy', format('{0}/getbackendfrompolicy', format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location')))))]",
|
||||
"properties": {
|
||||
"format": "xml",
|
||||
"value": "<policies>\r\n <inbound>\r\n <base />\r\n <return-response>\r\n <set-status code=\"302\" />\r\n <set-header name=\"X-Apim-Region\" exists-action=\"override\">\r\n <value>@(context.Deployment.Region)</value>\r\n </set-header>\r\n <set-header name=\"Location\" exists-action=\"override\">\r\n <value>@{\r\n var backends = \"{{backend-urls}}\".Split(',');\r\n var i = new Random(context.RequestId.GetHashCode()).Next(0, backends.Length);\r\n return backends[i];\r\n }</value>\r\n </set-header>\r\n </return-response>\r\n </inbound>\r\n <backend>\r\n <base />\r\n </backend>\r\n <outbound>\r\n <base />\r\n </outbound>\r\n <on-error>\r\n <base />\r\n </on-error>\r\n</policies>"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.ApiManagement/service/namedValues', split(format('{0}/backend-urls', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[0], split(format('{0}/backend-urls', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[1])]",
|
||||
"[resourceId('Microsoft.ApiManagement/service/apis/operations', split(format('{0}/getbackendfrompolicy', format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location')))), '/')[0], split(format('{0}/getbackendfrompolicy', format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location')))), '/')[1], split(format('{0}/getbackendfrompolicy', format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location')))), '/')[2])]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[variables('useLanguageRouter')]",
|
||||
"type": "Microsoft.ApiManagement/service/apis/operations/policies",
|
||||
"apiVersion": "2020-06-01-preview",
|
||||
"name": "[format('{0}/policy', format('{0}/getbackendbylanguage', format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location')))))]",
|
||||
"properties": {
|
||||
"format": "xml",
|
||||
"value": "<policies>\r\n <inbound>\r\n <base />\r\n <return-response>\r\n <set-status code=\"302\" />\r\n <set-header name=\"X-Apim-Region\" exists-action=\"override\">\r\n <value>@(context.Deployment.Region)</value>\r\n </set-header>\r\n <set-header name=\"Location\" exists-action=\"override\">\r\n <value>@{\r\n var dict = \"{{backend-urls}}\".Split(';').Select(item => item.Split('=')).ToDictionary(s => s[0], s => s[1].Split(','));\r\n\r\n string[] languageUrls;\r\n // If the query parameter \"lang\" is present and the requested language exists in the list, we use that. Otherwise we use the header Accept-Language\r\n var langQueryParam = context.Request.OriginalUrl.Query.GetValueOrDefault(\"lang\", \"\").ToLower();\r\n if(!dict.TryGetValue(langQueryParam, out languageUrls))\r\n {\r\n var userLanguage = context.Request.Headers.GetValueOrDefault(\"Accept-Language\", \"en\");\r\n // Sample values for Accept-Language: \"Accept-Language: de\" \"Accept-Language: de-CH\" \"Accept-Language: en-US,en;q=0.5\"\r\n if(userLanguage.Contains(\",\"))\r\n {\r\n userLanguage = userLanguage.Split(',')[0];\r\n }\r\n if(userLanguage.Contains(\"-\"))\r\n {\r\n userLanguage = userLanguage.Split('-')[0];\r\n }\r\n // If the language from the header exists in the list, we use that, otherwise we default to English\r\n languageUrls = dict.ContainsKey(userLanguage) ? dict[userLanguage] : dict[\"en\"];\r\n }\r\n // If the list only contains more than one entry for the selected language, we pick at random\r\n var selectedUrl = languageUrls.Length == 0 ? languageUrls[0] : languageUrls[new Random(context.RequestId.GetHashCode()).Next(0, languageUrls.Length)];\r\n return selectedUrl;\r\n }</value>\r\n </set-header>\r\n </return-response>\r\n </inbound>\r\n <backend>\r\n <base />\r\n </backend>\r\n <outbound>\r\n <base />\r\n </outbound>\r\n <on-error>\r\n <base />\r\n </on-error>\r\n</policies>"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.ApiManagement/service/namedValues', split(format('{0}/backend-urls', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[0], split(format('{0}/backend-urls', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[1])]",
|
||||
"[resourceId('Microsoft.ApiManagement/service/apis/operations', split(format('{0}/getbackendbylanguage', format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location')))), '/')[0], split(format('{0}/getbackendbylanguage', format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location')))), '/')[1], split(format('{0}/getbackendbylanguage', format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location')))), '/')[2])]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[variables('useTableStorage')]",
|
||||
"type": "Microsoft.ApiManagement/service/apis/operations/policies",
|
||||
"apiVersion": "2020-06-01-preview",
|
||||
"name": "[format('{0}/policy', format('{0}/getbackendfromtable', format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location')))))]",
|
||||
"properties": {
|
||||
"format": "xml",
|
||||
"value": "<policies>\r\n <inbound>\r\n <base />\r\n <!-- Make outbound request to the table API which holds the list of backend URLs -->\r\n <send-request mode=\"new\" response-variable-name=\"tableApiResponse\" timeout=\"20\" ignore-error=\"true\">\r\n <set-url>@(\"{{table-url}}?{{table-sas-token}}&$select=url\")</set-url>\r\n <set-method>GET</set-method>\r\n <set-header name=\"Accept\" exists-action=\"override\">\r\n <value>application/json;odata=nometadata</value>\r\n </set-header>\r\n </send-request>\r\n <set-method>GET</set-method>\r\n <return-response>\r\n <set-status code=\"302\" />\r\n <set-header name=\"X-Apim-Region\" exists-action=\"override\">\r\n <value>@(context.Deployment.Region)</value>\r\n </set-header>\r\n <set-header name=\"Location\" exists-action=\"override\">\r\n <value>@{\r\n try\r\n {\r\n var urls = ((IResponse) context.Variables[\"tableApiResponse\"]).Body.As<JObject>()[\"value\"];\r\n // Generate random rowKey\r\n var rowKey = new Random(context.RequestId.GetHashCode()).Next(0, urls.Count());\r\n return (string)urls[rowKey][\"url\"];\r\n }\r\n catch (Exception e)\r\n {\r\n // If something failed, it is usually because of an transient error. Then we just send the user to the same URL again to retry.\r\n return \"/\";\r\n }\r\n }</value>\r\n </set-header>\r\n </return-response>\r\n </inbound>\r\n <backend>\r\n <base />\r\n </backend>\r\n <outbound>\r\n <base />\r\n </outbound>\r\n <on-error>\r\n <base />\r\n </on-error>\r\n</policies>"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.ApiManagement/service/namedValues', split(format('{0}/table-sas-token', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[0], split(format('{0}/table-sas-token', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[1])]",
|
||||
"[resourceId('Microsoft.ApiManagement/service/namedValues', split(format('{0}/table-url', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[0], split(format('{0}/table-url', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[1])]",
|
||||
"[resourceId('Microsoft.ApiManagement/service/apis/operations', split(format('{0}/getbackendfromtable', format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location')))), '/')[0], split(format('{0}/getbackendfromtable', format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location')))), '/')[1], split(format('{0}/getbackendfromtable', format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location')))), '/')[2])]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.ApiManagement/service/apis/operations/policies",
|
||||
"apiVersion": "2020-06-01-preview",
|
||||
"name": "[format('{0}/policy', format('{0}/apimhealth', format('{0}/apimhealthz', format('{0}{1}apim', parameters('prefix'), parameters('location')))))]",
|
||||
"properties": {
|
||||
"format": "xml",
|
||||
"value": "<policies>\r\n <inbound>\r\n <base />\r\n <return-response>\r\n <set-status code=\"200\" />\r\n </return-response>\r\n </inbound>\r\n <backend>\r\n <base />\r\n </backend>\r\n <outbound>\r\n <base />\r\n </outbound>\r\n <on-error>\r\n <base />\r\n </on-error>\r\n</policies>"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.ApiManagement/service/apis/operations', split(format('{0}/apimhealth', format('{0}/apimhealthz', format('{0}{1}apim', parameters('prefix'), parameters('location')))), '/')[0], split(format('{0}/apimhealth', format('{0}/apimhealthz', format('{0}{1}apim', parameters('prefix'), parameters('location')))), '/')[1], split(format('{0}/apimhealth', format('{0}/apimhealthz', format('{0}{1}apim', parameters('prefix'), parameters('location')))), '/')[2])]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[or(variables('useDefaultLb'), variables('useLanguageRouter'))]",
|
||||
"type": "Microsoft.ApiManagement/service/namedValues",
|
||||
"apiVersion": "2020-06-01-preview",
|
||||
"name": "[format('{0}/backend-urls', format('{0}{1}apim', parameters('prefix'), parameters('location')))]",
|
||||
"properties": {
|
||||
"displayName": "backend-urls",
|
||||
"value": "[parameters('backends')]"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.ApiManagement/service', format('{0}{1}apim', parameters('prefix'), parameters('location')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[variables('useTableStorage')]",
|
||||
"type": "Microsoft.ApiManagement/service/namedValues",
|
||||
"apiVersion": "2020-06-01-preview",
|
||||
"name": "[format('{0}/table-url', format('{0}{1}apim', parameters('prefix'), parameters('location')))]",
|
||||
"properties": {
|
||||
"displayName": "table-url",
|
||||
"value": "[format('{0}{1}', reference(resourceId('Microsoft.Storage/storageAccounts', format('stg{0}{1}', take(parameters('location'), 8), uniqueString(parameters('prefix'), parameters('location'), 'stg')))).primaryEndpoints.table, variables('tableName'))]"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.ApiManagement/service', format('{0}{1}apim', parameters('prefix'), parameters('location')))]",
|
||||
"[resourceId('Microsoft.Storage/storageAccounts', format('stg{0}{1}', take(parameters('location'), 8), uniqueString(parameters('prefix'), parameters('location'), 'stg')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[variables('useTableStorage')]",
|
||||
"type": "Microsoft.ApiManagement/service/namedValues",
|
||||
"apiVersion": "2020-06-01-preview",
|
||||
"name": "[format('{0}/table-sas-token', format('{0}{1}apim', parameters('prefix'), parameters('location')))]",
|
||||
"properties": {
|
||||
"displayName": "table-sas-token",
|
||||
"value": "[listAccountSas(format('stg{0}{1}', take(parameters('location'), 8), uniqueString(parameters('prefix'), parameters('location'), 'stg')), '2019-06-01', variables('accountSasProperties')).accountSasToken]",
|
||||
"secret": true
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.ApiManagement/service', format('{0}{1}apim', parameters('prefix'), parameters('location')))]",
|
||||
"[resourceId('Microsoft.Storage/storageAccounts', format('stg{0}{1}', take(parameters('location'), 8), uniqueString(parameters('prefix'), parameters('location'), 'stg')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.ApiManagement/service/namedValues",
|
||||
"apiVersion": "2020-06-01-preview",
|
||||
"name": "[format('{0}/logger-credentials', format('{0}{1}apim', parameters('prefix'), parameters('location')))]",
|
||||
"properties": {
|
||||
"displayName": "logger-credentials",
|
||||
"value": "[reference(resourceId('Microsoft.Insights/components', parameters('applicationInsightsName')), '2018-05-01-preview').InstrumentationKey]",
|
||||
"secret": true
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.ApiManagement/service', format('{0}{1}apim', parameters('prefix'), parameters('location')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.ApiManagement/service/loggers",
|
||||
"apiVersion": "2020-06-01-preview",
|
||||
"name": "[format('{0}/{1}', format('{0}{1}apim', parameters('prefix'), parameters('location')), parameters('applicationInsightsName'))]",
|
||||
"properties": {
|
||||
"loggerType": "applicationInsights",
|
||||
"credentials": {
|
||||
"instrumentationKey": "{{logger-credentials}}"
|
||||
},
|
||||
"isBuffered": true,
|
||||
"resourceId": "[resourceId('Microsoft.Insights/components', parameters('applicationInsightsName'))]"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.ApiManagement/service', format('{0}{1}apim', parameters('prefix'), parameters('location')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.ApiManagement/service/apis/diagnostics",
|
||||
"apiVersion": "2020-06-01-preview",
|
||||
"name": "[format('{0}/applicationinsights', format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location'))))]",
|
||||
"properties": {
|
||||
"alwaysLog": "allErrors",
|
||||
"httpCorrelationProtocol": "W3C",
|
||||
"verbosity": "information",
|
||||
"loggerId": "[resourceId('Microsoft.ApiManagement/service/loggers', split(format('{0}/{1}', format('{0}{1}apim', parameters('prefix'), parameters('location')), parameters('applicationInsightsName')), '/')[0], split(format('{0}/{1}', format('{0}{1}apim', parameters('prefix'), parameters('location')), parameters('applicationInsightsName')), '/')[1])]",
|
||||
"frontend": {
|
||||
"response": {
|
||||
"headers": [
|
||||
"location"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.ApiManagement/service/apis', split(format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[0], split(format('{0}/getbackend', format('{0}{1}apim', parameters('prefix'), parameters('location'))), '/')[1])]",
|
||||
"[resourceId('Microsoft.ApiManagement/service/loggers', split(format('{0}/{1}', format('{0}{1}apim', parameters('prefix'), parameters('location')), parameters('applicationInsightsName')), '/')[0], split(format('{0}/{1}', format('{0}{1}apim', parameters('prefix'), parameters('location')), parameters('applicationInsightsName')), '/')[1])]"
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": {
|
||||
"apimHostname": {
|
||||
"type": "string",
|
||||
"value": "[replace(reference(resourceId('Microsoft.ApiManagement/service', format('{0}{1}apim', parameters('prefix'), parameters('location')))).gatewayUrl, 'https://', '')]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Insights/components', format('{0}appinsights', parameters('prefix')))]"
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": {
|
||||
"frontDoorUrl": {
|
||||
"type": "string",
|
||||
"value": "[variables('frontdoor_default_dns_name')]"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"_generator": {
|
||||
"name": "bicep",
|
||||
"version": "0.3.104.52030",
|
||||
"templateHash": "5962523844245559369"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,388 @@
|
|||
param prefix string
|
||||
param location string
|
||||
param publisherEmail string
|
||||
param publisherName string
|
||||
|
||||
param backends string = ''
|
||||
|
||||
@allowed([
|
||||
'default'
|
||||
'userLanguage'
|
||||
'largeEvent'
|
||||
])
|
||||
param loadBalancingMode string = 'default'
|
||||
|
||||
param applicationInsightsName string
|
||||
|
||||
param sasTokenStart string = utcNow('yyyy-MM-ddTHH:mm:ssZ')
|
||||
param sasTokenExpiry string = dateTimeAdd(utcNow('u'), 'P2Y', 'yyyy-MM-ddTHH:mm:ssZ') // Expries in 2 years from deployment time
|
||||
|
||||
var useDefaultLb = loadBalancingMode == 'default'
|
||||
var useTableStorage = loadBalancingMode == 'largeEvent'
|
||||
var useLanguageRouter = loadBalancingMode == 'userLanguage'
|
||||
|
||||
// Because of some issue in bicep/ARM using "if (useTableStorage)" on this resource breaks other dependencies. Thus, we always provision the storage account, even if it is not being used when not running in Table-storage mode.
|
||||
resource storageAccount 'Microsoft.Storage/storageAccounts@2019-06-01' = {
|
||||
name: 'stg${take(location, 8)}${uniqueString(prefix, location, 'stg')}'
|
||||
location: location
|
||||
kind: 'StorageV2'
|
||||
sku: {
|
||||
name: 'Standard_ZRS'
|
||||
tier: 'Standard'
|
||||
}
|
||||
properties: {
|
||||
supportsHttpsTrafficOnly: true
|
||||
}
|
||||
}
|
||||
|
||||
var tableName = 'Urls'
|
||||
|
||||
resource table 'Microsoft.Storage/storageAccounts/tableServices/tables@2019-06-01' = if (useTableStorage) {
|
||||
name: '${storageAccount.name}/default/${tableName}'
|
||||
}
|
||||
|
||||
resource apim 'Microsoft.ApiManagement/service@2020-06-01-preview' = {
|
||||
name: '${prefix}${location}apim'
|
||||
location: location
|
||||
sku: {
|
||||
name: 'Consumption'
|
||||
capacity: 0
|
||||
}
|
||||
properties: {
|
||||
publisherEmail: publisherEmail
|
||||
publisherName: publisherName
|
||||
|
||||
customProperties: {
|
||||
'Microsoft.WindowsAzure.ApiManagement.Gateway.Protocols.Server.Http2': 'true'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource apiGetbackend 'Microsoft.ApiManagement/service/apis@2020-06-01-preview' = {
|
||||
name: '${apim.name}/getbackend'
|
||||
properties: {
|
||||
apiRevision: '1'
|
||||
displayName: 'GetBackend'
|
||||
subscriptionRequired: false
|
||||
protocols: [
|
||||
'https'
|
||||
]
|
||||
path: ''
|
||||
}
|
||||
}
|
||||
|
||||
resource apiHealthz 'Microsoft.ApiManagement/service/apis@2020-06-01-preview' = {
|
||||
name: '${apim.name}/apimhealthz'
|
||||
properties: {
|
||||
apiRevision: '1'
|
||||
displayName: 'APIM Healthz'
|
||||
subscriptionRequired: false
|
||||
protocols: [
|
||||
'https'
|
||||
]
|
||||
path: 'healthz'
|
||||
}
|
||||
}
|
||||
|
||||
// Based on the parameter useTableStorage either this operation is being created...
|
||||
resource operationGetbackend 'Microsoft.ApiManagement/service/apis/operations@2020-06-01-preview' = if (useDefaultLb) {
|
||||
name: '${apiGetbackend.name}/getbackendfrompolicy'
|
||||
properties: {
|
||||
displayName: 'Get Backend From Policy'
|
||||
method: 'GET'
|
||||
urlTemplate: '/'
|
||||
}
|
||||
}
|
||||
|
||||
// ... or this operation is being used. This one will retrieve the backend URLs from the Table storage.
|
||||
resource operationGetbackendFromTable 'Microsoft.ApiManagement/service/apis/operations@2020-06-01-preview' = if (useTableStorage) {
|
||||
name: '${apiGetbackend.name}/getbackendfromtable'
|
||||
properties: {
|
||||
displayName: 'Get Backend From Table Storage'
|
||||
method: 'GET'
|
||||
urlTemplate: '/'
|
||||
}
|
||||
}
|
||||
|
||||
// ... or this operation is being used. This one will retrieve the backend URLs from the Table storage.
|
||||
resource operationGetbackendByLanguage 'Microsoft.ApiManagement/service/apis/operations@2020-06-01-preview' = if (useLanguageRouter) {
|
||||
name: '${apiGetbackend.name}/getbackendbylanguage'
|
||||
properties: {
|
||||
displayName: 'Get Backend by User Language'
|
||||
method: 'GET'
|
||||
urlTemplate: '/'
|
||||
}
|
||||
}
|
||||
|
||||
resource operationHealthz 'Microsoft.ApiManagement/service/apis/operations@2020-06-01-preview' = {
|
||||
name: '${apiHealthz.name}/apimhealth'
|
||||
properties: {
|
||||
displayName: 'ApimHealth'
|
||||
method: 'HEAD'
|
||||
urlTemplate: '/'
|
||||
}
|
||||
}
|
||||
|
||||
resource getbackendPolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2020-06-01-preview' = if (useDefaultLb) {
|
||||
name: '${operationGetbackend.name}/policy'
|
||||
dependsOn: [
|
||||
namedValueBackends
|
||||
]
|
||||
properties: {
|
||||
format: 'xml'
|
||||
// The 'backends' parameter gets injected into the policy via the named value
|
||||
value: '''
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<return-response>
|
||||
<set-status code="302" />
|
||||
<set-header name="X-Apim-Region" exists-action="override">
|
||||
<value>@(context.Deployment.Region)</value>
|
||||
</set-header>
|
||||
<set-header name="Location" exists-action="override">
|
||||
<value>@{
|
||||
var backends = "{{backend-urls}}".Split(',');
|
||||
var i = new Random(context.RequestId.GetHashCode()).Next(0, backends.Length);
|
||||
return backends[i];
|
||||
}</value>
|
||||
</set-header>
|
||||
</return-response>
|
||||
</inbound>
|
||||
<backend>
|
||||
<base />
|
||||
</backend>
|
||||
<outbound>
|
||||
<base />
|
||||
</outbound>
|
||||
<on-error>
|
||||
<base />
|
||||
</on-error>
|
||||
</policies>'''
|
||||
}
|
||||
}
|
||||
|
||||
resource getbackendPolicyByLanguage 'Microsoft.ApiManagement/service/apis/operations/policies@2020-06-01-preview' = if (useLanguageRouter) {
|
||||
name: '${operationGetbackendByLanguage.name}/policy'
|
||||
dependsOn: [
|
||||
namedValueBackends
|
||||
]
|
||||
properties: {
|
||||
format: 'xml'
|
||||
// The 'backends' parameter gets injected into the policy via the named value
|
||||
value: '''
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<return-response>
|
||||
<set-status code="302" />
|
||||
<set-header name="X-Apim-Region" exists-action="override">
|
||||
<value>@(context.Deployment.Region)</value>
|
||||
</set-header>
|
||||
<set-header name="Location" exists-action="override">
|
||||
<value>@{
|
||||
var dict = "{{backend-urls}}".Split(';').Select(item => item.Split('=')).ToDictionary(s => s[0], s => s[1].Split(','));
|
||||
|
||||
string[] languageUrls;
|
||||
// If the query parameter "lang" is present and the requested language exists in the list, we use that. Otherwise we use the header Accept-Language
|
||||
var langQueryParam = context.Request.OriginalUrl.Query.GetValueOrDefault("lang", "").ToLower();
|
||||
if(!dict.TryGetValue(langQueryParam, out languageUrls))
|
||||
{
|
||||
var userLanguage = context.Request.Headers.GetValueOrDefault("Accept-Language", "en");
|
||||
// Sample values for Accept-Language: "Accept-Language: de" "Accept-Language: de-CH" "Accept-Language: en-US,en;q=0.5"
|
||||
if(userLanguage.Contains(","))
|
||||
{
|
||||
userLanguage = userLanguage.Split(',')[0];
|
||||
}
|
||||
if(userLanguage.Contains("-"))
|
||||
{
|
||||
userLanguage = userLanguage.Split('-')[0];
|
||||
}
|
||||
// If the language from the header exists in the list, we use that, otherwise we default to English
|
||||
languageUrls = dict.ContainsKey(userLanguage) ? dict[userLanguage] : dict["en"];
|
||||
}
|
||||
// If the list only contains more than one entry for the selected language, we pick at random
|
||||
var selectedUrl = languageUrls.Length == 0 ? languageUrls[0] : languageUrls[new Random(context.RequestId.GetHashCode()).Next(0, languageUrls.Length)];
|
||||
return selectedUrl;
|
||||
}</value>
|
||||
</set-header>
|
||||
</return-response>
|
||||
</inbound>
|
||||
<backend>
|
||||
<base />
|
||||
</backend>
|
||||
<outbound>
|
||||
<base />
|
||||
</outbound>
|
||||
<on-error>
|
||||
<base />
|
||||
</on-error>
|
||||
</policies>'''
|
||||
}
|
||||
}
|
||||
|
||||
resource getbackendFromTablePolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2020-06-01-preview' = if (useTableStorage) {
|
||||
name: '${operationGetbackendFromTable.name}/policy'
|
||||
dependsOn: [
|
||||
namedValueTableUrl
|
||||
namedValueTableSasToken
|
||||
]
|
||||
properties: {
|
||||
format: 'xml'
|
||||
value: '''
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Make outbound request to the table API which holds the list of backend URLs -->
|
||||
<send-request mode="new" response-variable-name="tableApiResponse" timeout="20" ignore-error="true">
|
||||
<set-url>@("{{table-url}}?{{table-sas-token}}&$select=url")</set-url>
|
||||
<set-method>GET</set-method>
|
||||
<set-header name="Accept" exists-action="override">
|
||||
<value>application/json;odata=nometadata</value>
|
||||
</set-header>
|
||||
</send-request>
|
||||
<set-method>GET</set-method>
|
||||
<return-response>
|
||||
<set-status code="302" />
|
||||
<set-header name="X-Apim-Region" exists-action="override">
|
||||
<value>@(context.Deployment.Region)</value>
|
||||
</set-header>
|
||||
<set-header name="Location" exists-action="override">
|
||||
<value>@{
|
||||
try
|
||||
{
|
||||
var urls = ((IResponse) context.Variables["tableApiResponse"]).Body.As<JObject>()["value"];
|
||||
// Generate random rowKey
|
||||
var rowKey = new Random(context.RequestId.GetHashCode()).Next(0, urls.Count());
|
||||
return (string)urls[rowKey]["url"];
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// If something failed, it is usually because of an transient error. Then we just send the user to the same URL again to retry.
|
||||
return "/";
|
||||
}
|
||||
}</value>
|
||||
</set-header>
|
||||
</return-response>
|
||||
</inbound>
|
||||
<backend>
|
||||
<base />
|
||||
</backend>
|
||||
<outbound>
|
||||
<base />
|
||||
</outbound>
|
||||
<on-error>
|
||||
<base />
|
||||
</on-error>
|
||||
</policies>'''
|
||||
}
|
||||
}
|
||||
|
||||
resource healthzPolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2020-06-01-preview' = {
|
||||
name: '${operationHealthz.name}/policy'
|
||||
properties: {
|
||||
format: 'xml'
|
||||
value: '''
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<return-response>
|
||||
<set-status code="200" />
|
||||
</return-response>
|
||||
</inbound>
|
||||
<backend>
|
||||
<base />
|
||||
</backend>
|
||||
<outbound>
|
||||
<base />
|
||||
</outbound>
|
||||
<on-error>
|
||||
<base />
|
||||
</on-error>
|
||||
</policies>'''
|
||||
}
|
||||
}
|
||||
|
||||
// Section: named values for backend urls
|
||||
resource namedValueBackends 'Microsoft.ApiManagement/service/namedValues@2020-06-01-preview' = if (useDefaultLb || useLanguageRouter) {
|
||||
name: '${apim.name}/backend-urls'
|
||||
properties: {
|
||||
displayName: 'backend-urls'
|
||||
value: backends
|
||||
}
|
||||
}
|
||||
|
||||
// Section: named values for Table storage backend
|
||||
resource namedValueTableUrl 'Microsoft.ApiManagement/service/namedValues@2020-06-01-preview' = if (useTableStorage) {
|
||||
name: '${apim.name}/table-url'
|
||||
properties: {
|
||||
displayName: 'table-url'
|
||||
value: '${storageAccount.properties.primaryEndpoints.table}${tableName}'
|
||||
}
|
||||
}
|
||||
|
||||
// Table storage sas token properties
|
||||
var accountSasProperties = {
|
||||
signedServices: 't' // only valid on Table endpoint
|
||||
signedPermission: 'rl' // Permissions: read and list
|
||||
signedResourceTypes: 'o' // object-level
|
||||
signedStart: sasTokenStart
|
||||
signedExpiry: sasTokenExpiry
|
||||
}
|
||||
|
||||
resource namedValueTableSasToken 'Microsoft.ApiManagement/service/namedValues@2020-06-01-preview' = if (useTableStorage) {
|
||||
name: '${apim.name}/table-sas-token'
|
||||
properties: {
|
||||
displayName: 'table-sas-token'
|
||||
value: listAccountSas(storageAccount.name, storageAccount.apiVersion, accountSasProperties).accountSasToken
|
||||
secret: true
|
||||
}
|
||||
}
|
||||
|
||||
// Section: Logging
|
||||
|
||||
resource namedValueAppInsightsKey 'Microsoft.ApiManagement/service/namedValues@2020-06-01-preview' = {
|
||||
name: '${apim.name}/logger-credentials'
|
||||
properties: {
|
||||
displayName: 'logger-credentials'
|
||||
value: appInsights.properties.InstrumentationKey
|
||||
secret: true
|
||||
}
|
||||
}
|
||||
|
||||
resource apimLogger 'Microsoft.ApiManagement/service/loggers@2020-06-01-preview' = {
|
||||
name: '${apim.name}/${appInsights.name}'
|
||||
properties: {
|
||||
loggerType: 'applicationInsights'
|
||||
credentials: {
|
||||
instrumentationKey: '{{logger-credentials}}'
|
||||
}
|
||||
isBuffered: true
|
||||
resourceId: appInsights.id
|
||||
}
|
||||
}
|
||||
|
||||
resource apiGetbackendDiagnostics 'Microsoft.ApiManagement/service/apis/diagnostics@2020-06-01-preview' = {
|
||||
name: '${apiGetbackend.name}/applicationinsights'
|
||||
properties: {
|
||||
alwaysLog: 'allErrors'
|
||||
httpCorrelationProtocol: 'W3C'
|
||||
verbosity: 'information'
|
||||
loggerId: apimLogger.id
|
||||
frontend: {
|
||||
response: {
|
||||
headers: [
|
||||
'location'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use AppInsights which was created outside of this module
|
||||
resource appInsights 'Microsoft.Insights/components@2018-05-01-preview' existing = {
|
||||
name: applicationInsightsName
|
||||
}
|
||||
|
||||
// We only need the hostname, without the protocol
|
||||
output apimHostname string = replace(apim.properties.gatewayUrl, 'https://', '')
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 53 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 60 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 178 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 37 KiB |
|
@ -0,0 +1,30 @@
|
|||
param
|
||||
(
|
||||
[Parameter(Mandatory)]
|
||||
[string]
|
||||
${Please provide the URL to test}
|
||||
)
|
||||
|
||||
|
||||
$URL = ${Please provide the URL to test}
|
||||
$count = 10
|
||||
|
||||
function Get-UrlStatusCode([string] $Url)
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = Invoke-WebRequest -Uri $Url -UseBasicParsing -DisableKeepAlive -MaximumRedirection 0 -SkipHttpErrorCheck -ErrorAction Ignore
|
||||
if($response.StatusCode -ne 302)
|
||||
{
|
||||
Write-Error -Message "Unexpected status code ($($response.StatusCode))" -ErrorAction Stop
|
||||
}
|
||||
Write-Host "Success! Status code: $($response.StatusCode) -- Backend: $($response.Headers['Location'])"
|
||||
}
|
||||
catch [Exception]
|
||||
{
|
||||
$_.Exception.Message
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for($i = 0; $i -lt $count; $i++){ Get-UrlStatusCode $URL }
|
|
@ -0,0 +1,48 @@
|
|||
# Script to load the URL into all Azure Storage accounts deployed as part of the Teams load-balancing solution
|
||||
|
||||
# Install Azure PowerShell
|
||||
# https://docs.microsoft.com/en-us/powershell/azure/install-az-ps?view=azps-5.5.0
|
||||
# This is the only Az module required for this script (you don't have to install all Az modules)
|
||||
Install-Module AzTable
|
||||
# Login to Azure with a browser sign-in token
|
||||
Connect-AzAccount
|
||||
|
||||
# Set to the subscription ID in which the solution got deployed in
|
||||
Set-AzContext -Subscription "xxxx-xxxx-xxxx-xxxx"
|
||||
|
||||
# url-list.txt needs to contain the list of backend URLs, one per line
|
||||
$urls = Get-Content sample-url-list.txt
|
||||
|
||||
Write-Host "Fount $($urls.Count) URLs to import"
|
||||
|
||||
$resourceGroup = "teamsdistributor" # <-- Change me!
|
||||
|
||||
$tableName = "Urls" # does not need to be changed unless it was changed in the ARM template
|
||||
$partitionKey = "event1" # does not need to be changed
|
||||
|
||||
# Get all storage accounts in the resource group
|
||||
$storageAccounts = Get-AzStorageAccount -ResourceGroupName $resourceGroup
|
||||
|
||||
foreach ($storageAccount in $storageAccounts) {
|
||||
Write-Host "Importing data into storage account $($storageAccount.StorageAccountName)"
|
||||
$ctx = $storageAccount.Context
|
||||
|
||||
$cloudTable = (Get-AzStorageTable –Name $tableName –Context $ctx).CloudTable
|
||||
|
||||
# First, delete all current entries in the table
|
||||
$currentRows = Get-AzTableRow -Table $cloudTable
|
||||
Write-Host "Deleting $($currentRows.Count) old rows from the table"
|
||||
$currentRows | Remove-AzTableRow -Table $cloudTable
|
||||
|
||||
$i = 0
|
||||
foreach ($url in $urls) {
|
||||
Add-AzTableRow `
|
||||
-Table $cloudTable `
|
||||
-PartitionKey $partitionKey `
|
||||
-RowKey ($i) `
|
||||
-Property @{"url" = "$url" } `
|
||||
-UpdateExisting
|
||||
|
||||
$i++
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче