run . ./create-azure-environment.sh e.g.:
$ . ./create-azure-environment.sh uat| _oauth-permissions.json | |
| _disable-oauth-permissions.json |
| SUBSCRIPTION="MyAzureSubscription" | |
| CLIENT_NAME="ProjectOrClientName" | |
| CLIENT_SHORTNAME="sys" | |
| DATABASE_NAME="MyApplicationName" | |
| DB_COLLECTION_NAME="Default" | |
| LOCATION="euwest" | |
| LOCATION_NAME="westeurope" | |
| LOCATION_SHORTNAME="euw" | |
| TAGS="Development" | |
| APP_SERVICE_PLAN_SKU="S1" | |
| # With these TLAs we know how to name our resources, e.g. a webapp = "$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$ENVIRONMENT_NAME-$WEBAPP_SHORTNAME" | |
| APP_INSIGHTS_SHORTNAME="insights" | |
| APP_SERVICE_PLAN_SHORTNAME="asp" | |
| SERVICE_BUS_NAMESPACE_SHORTNAME="sbn" | |
| COSMOSDB_SHORTNAME="cdb" | |
| ELASTIC_POOL_SHORTNAME="pool" | |
| FUNCTIONAPP_SHORTNAME="fn" | |
| KEYVAULT_SHORTNAME="kv" | |
| RESOURCE_GROUP_SHORTNAME="rg" | |
| SIGNALR_SHORTNAME="signalr" | |
| SQL_SHORTNAME="sql" | |
| WEBAPP_SHORTNAME="as" | |
| # create-apps.sh | |
| SPA_APP_SHORTNAME="spa" | |
| API_APP_SHORTNAME="api" | |
| APP_REGISTRATION_SUFFIX="app-registration" |
| [ | |
| { | |
| "resourceAppId": "00000003-0000-0000-c000-000000000000", | |
| "resourceAccess": [ | |
| { | |
| "id": "465a38f9-76ea-45b9-9f34-9e8b0d4b0b42", | |
| "type": "Scope" | |
| }, | |
| { | |
| "id": "a4b8392a-d8d1-4954-a029-8e668a39a170", | |
| "type": "Scope" | |
| }, | |
| { | |
| "id": "f45671fb-e0fe-4b4b-be20-3d3ce43f1bcb", | |
| "type": "Scope" | |
| }, | |
| { | |
| "id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d", | |
| "type": "Scope" | |
| } | |
| ] | |
| } | |
| ] |
| [ | |
| { | |
| "resourceAppId": "00000003-0000-0000-c000-000000000000", | |
| "resourceAccess": [ | |
| { | |
| "id": "14dad69e-099b-42c9-810b-d002981feec1", | |
| "type": "Scope" | |
| } | |
| ] | |
| } | |
| ] |
| #!/bin/bash | |
| # ensure bash exits on first error | |
| set -e | |
| if [ $# -lt 2 ]; then | |
| echo " usage: $0 <environment> <appname>" >&2 | |
| echo " example: $0 uat MyApplicationName" >&2 | |
| exit 1 | |
| fi | |
| # Grab our "naming conventions" - | |
| # moved to separate file so it can be shared by scripts | |
| set -a | |
| . ./.naming-rules.txt | |
| set +a | |
| . ./ensure-logged-in.sh $SUBSCRIPTION $LOCATION_NAME | |
| environmentName=$1 | |
| applicationName=$2 | |
| shift | |
| shift | |
| create_ad_app() { | |
| local resourceName=$1 | |
| local available_to_other_tenants=$2 | |
| local oauth2_allow_implicit_flow=$3 | |
| local resource_manifest=$4 | |
| shift | |
| shift | |
| shift | |
| shift | |
| local reply_urls=$@ | |
| if [[ -n $reply_urls ]]; then | |
| az ad app create \ | |
| --display-name "$resourceName" \ | |
| --available-to-other-tenants $available_to_other_tenants \ | |
| --oauth2-allow-implicit-flow $oauth2_allow_implicit_flow \ | |
| --required-resource-accesses @$resource_manifest \ | |
| --reply-urls $reply_urls \ | |
| --query [].appId \ | |
| -o tsv | |
| else | |
| az ad app create \ | |
| --display-name "$resourceName" \ | |
| --available-to-other-tenants $available_to_other_tenants \ | |
| --oauth2-allow-implicit-flow $oauth2_allow_implicit_flow \ | |
| --required-resource-accesses @$resource_manifest \ | |
| -o json | |
| fi | |
| } | |
| create_ad_apps() { | |
| # take first argument as resource name and then remove it | |
| local resourceName=$1 | |
| shift | |
| local apiApplicationName="$resourceName-$API_APP_SHORTNAME" | |
| echo " Checking if Azure AD API App '$apiApplicationName' already exists." | |
| local apiApplicationJson=$(az ad app list --display-name "$apiApplicationName" -o json --query [0]) >/dev/null | |
| if [[ -n $apiApplicationJson ]]; then | |
| echo " AAD App '$apiApplicationName' already exists - skipping." | |
| else | |
| echo " Creating API app $apiApplicationName" | |
| # app-resource-manifest-api.json specifies the MS Graph resources the API requires. At writing: | |
| # Calendars.Read; Mail.ReadBasic; Tasks.Read; and User.Read. | |
| apiApplicationJson=$(create_ad_app $apiApplicationName true false app-resource-manifest-api.json) | |
| fi | |
| local apiApplicationId=$(az ad app list --display-name "$apiApplicationName" -o tsv --query [].appId) >/dev/null | |
| echo " App has ID '$apiApplicationId'" | |
| echo " Add to $keyvaultName secret $apiApplicationName-AppId=$apiApplicationId" | |
| az keyvault secret set --vault-name $keyvaultName \ | |
| --name "$apiApplicationName-AppId" \ | |
| --value $apiApplicationId >/dev/null | |
| # You can't add a new scope without first either disabling then deleting the existng `user_impersonation` one, | |
| # OR by fetching the user_impersonation scope and appending a new one. I chose the latter as it's slightly | |
| # easier. | |
| # So: | |
| # 1) disable any existing 'execute' scope from Oauth2Permissions: | |
| echo " 1.1 Updating JSON to disable existing 'execute' scope in oauth2Permissions." | |
| local changes=$(./disable-existing-oauth-execute.js "$apiApplicationJson") | |
| if [[ -z $changes ]]; then | |
| echo " 1.2 No changes required." | |
| else | |
| echo $changes > _disable-oauth-permissions.json | |
| echo " 1.2 Updating ${apiApplicationName}'s OAUTH2 permissions." | |
| az ad app update --id $apiApplicationId \ | |
| --identifier-uris "api://$apiApplicationId" \ | |
| --set oauth2Permissions=@_disable-oauth-permissions.json | |
| fi | |
| # 2) add the `execute` scope to our $apiApplicationJson (via a node script: node can "do" JSON; bash not so much!) | |
| local scopeGuid=$(uuidgen) | |
| echo " 2. Creating new 'execute' oauth2Permission JSON as $scopeGuid." | |
| ./modify-and-extract-oauth-json.js $scopeGuid $apiApplicationName "$apiApplicationJson" > _oauth-permissions.json | |
| # 3) add the new oauth2Permission | |
| echo " 3. Updating ${apiApplicationName}'s OAUTH2 permissions." | |
| az ad app update --id $apiApplicationId \ | |
| --identifier-uris "api://$apiApplicationId" \ | |
| --set oauth2Permissions=@_oauth-permissions.json | |
| # NB only the API needs a secret - we don't need to do this for SPA. | |
| echo " Create secret for appID" | |
| local applicationPassword=$(az ad app credential reset --id $apiApplicationId \ | |
| --credential-description "scripted" \ | |
| -o tsv \ | |
| --query password) | |
| if [[ -z $applicationPassword ]]; then | |
| echo "COULD NOT CREATE PASSWORD" | |
| echo az ad app credential reset --id $apiApplicationId \ | |
| --credential-description "scripted" \ | |
| -o tsv \ | |
| --query password | |
| read -p "hit a key to continue or Ctrl+c to exit." | |
| else | |
| echo " Store secrets/config in KV" | |
| # Consider using "$resourceName-AppClientSecret" instead of "MyAzureSubscriptionAppRegistrationClientSecret" in code. | |
| echo " Add to $keyvaultName secret $apiApplicationName-AppClientSecret" | |
| az keyvault secret set --vault-name $keyvaultName \ | |
| --name "$apiApplicationName-AppClientSecret" \ | |
| --value $applicationPassword >/dev/null | |
| echo " Add to $keyvaultName secret MyAzureSubscriptionAppRegistrationClientSecret" | |
| az keyvault secret set --vault-name $keyvaultName \ | |
| --name "TokenValidationSettings--MyAzureSubscriptionAppRegistrationClientSecret" \ | |
| --value $applicationPassword >/dev/null | |
| fi | |
| echo " Created API App '${apiApplicationName}'." | |
| local spaApplicationName="$resourceName-$SPA_APP_SHORTNAME" | |
| echo " Checking if Azure AD SPA App '$spaApplicationName' already exists." | |
| local spaApplicationId=$(az ad app list --display-name "$spaApplicationName" -o tsv --query [].appId) >/dev/null | |
| if [[ -n $spaApplicationId ]]; then | |
| echo " AAD SPA App '$spaApplicationName' already exists as $spaApplicationId; skipping." | |
| else | |
| local webApplicationName="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$environmentName-$applicationName-$WEBAPP_SHORTNAME" | |
| local replyUrl="https://$webApplicationName.azurewebsites.net/auth/signinend" | |
| echo " Creating SPA app $spaApplicationName with replyUrl $replyUrl." | |
| # app-resource-manifest-spa.json specifies the MS Graph resources the SPA requires. At writing: | |
| # 'profile' = View users' basic profile (name, picture, user name) | |
| # NB we may authorize other (non-MS-Graph) later in script, e.g. API's 'execute'. | |
| create_ad_app $spaApplicationName true true app-resource-manifest-spa.json \ | |
| "http://localhost:8080/auth/signinend" $replyUrl | |
| spaApplicationId=$(az ad app list --display-name "$spaApplicationName" -o tsv --query [].appId) >/dev/null | |
| echo " Created $spaApplicationName with ID $spaApplicationId." | |
| fi | |
| echo " Storing SPA APP ID in KV" | |
| az keyvault secret set --vault-name $keyvaultName --name "$spaApplicationName-AppId" --value $spaApplicationId >/dev/null | |
| local spCreated=$(az ad sp show --id $spaApplicationId -o tsv --query "appId") >/dev/null | |
| if [[ -n $spCreated ]]; then | |
| echo " Service Principal for SPA app '${spaApplicationName}' ($spaApplicationId) already exists." | |
| else | |
| echo " Creating Service Principal for SPA app '${spaApplicationName}' ($spaApplicationId)." | |
| az ad sp create --id $spaApplicationId >/dev/null | |
| fi | |
| echo " Getting ID of API app's 'execute' permission." | |
| local executePermissionId=$(az ad app show --id $apiApplicationId -o tsv --query "oauth2Permissions[?value=='execute'].id") | |
| echo " Getting ObjectID of API app $apiApplicationId to patch preAuthorizedApplications via REST API" | |
| local objectId=$(az ad app show --id $apiApplicationId --query objectId -o tsv) | |
| echo " Authorising SPA app $spaApplicationId to API $objectId's 'execute' scope ($executePermissionId)" | |
| az rest -m patch \ | |
| --headers "{\"Content-Type\": \"application/json\"}" \ | |
| -u "https://graph.microsoft.com/v1.0/applications/$objectId" \ | |
| --body \ | |
| "{ \ | |
| \"api\": { \ | |
| \"knownClientApplications\": [ \ | |
| \"$spaApplicationId\" \ | |
| ], \ | |
| \"preAuthorizedApplications\": [ \ | |
| { \ | |
| \"appId\": \"$spaApplicationId\", \ | |
| \"delegatedPermissionIds\": [ \ | |
| \"$executePermissionId\" \ | |
| ], \ | |
| }, \ | |
| ], \ | |
| }, \ | |
| }" | |
| } | |
| resourceGroupName="$CLIENT_SHORTNAME-$environmentName-$RESOURCE_GROUP_SHORTNAME" | |
| keyvaultName="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$environmentName-$KEYVAULT_SHORTNAME" | |
| # Will print device code and login URL to console (stdout) if not logged in. | |
| . ./ensure-logged-in.sh $SUBSCRIPTION $LOCATION_NAME | |
| az configure --defaults group=$resourceGroupName | |
| # Create an app registration for SPA & API | |
| applicationNamePrefix="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$environmentName-$APP_REGISTRATION_SUFFIX" | |
| create_ad_apps $applicationNamePrefix |
| #!/bin/bash | |
| # ensure bash exits on first error | |
| set -e | |
| if [ $# -lt 2 ]; then | |
| echo " usage: $0 <environment> <appname>" >&2 | |
| echo " example: $0 uat MyApplicationName" >&2 | |
| exit 1 | |
| fi | |
| # Grab our "naming conventions" - | |
| # moved to separate file so it can be shared by scripts | |
| set -a | |
| . ./.naming-rules.txt | |
| set +a | |
| . ./ensure-logged-in.sh $SUBSCRIPTION $LOCATION_NAME | |
| environmentName=$1 | |
| applicationName=$2 | |
| shift | |
| shift | |
| resourceGroupName="$CLIENT_SHORTNAME-$environmentName-$RESOURCE_GROUP_SHORTNAME" | |
| keyvaultName="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$environmentName-$KEYVAULT_SHORTNAME" | |
| appServicePlanName="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$environmentName-$APP_SERVICE_PLAN_SHORTNAME" | |
| storageAccountName="$CLIENT_SHORTNAME$LOCATION_SHORTNAME$environmentName" | |
| appInsightsName="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$environmentName-$APP_INSIGHTS_SHORTNAME" | |
| appInsightsKey=$(az resource show \ | |
| --namespace Microsoft.Insights \ | |
| --resource-type components \ | |
| -g $resourceGroupName \ | |
| -n $appInsightsName \ | |
| --query properties.InstrumentationKey -o tsv) | |
| create_app() { | |
| # take first two arguments as resource type and name | |
| local applicationType=$1 | |
| local applicationName=$2 | |
| local keyVaultName=$3 | |
| # remove expected arguments | |
| shift | |
| shift | |
| shift | |
| # take the rest of the arguments as extra arguments for create command | |
| local extraArgs=$@ | |
| ./upsert-resource.sh $applicationType $applicationName -p $appServicePlanName $extraArgs --tags $environmentName | |
| echo " Configuring app setting KeyVault__Url=https://$keyvaultName.vault.azure.net/" | |
| az $applicationType config appsettings set -g $resourceGroupName -n $applicationName --settings KeyVault__Url=https://$keyvaultName.vault.azure.net/ >/dev/null | |
| echo " Configuring app setting APPINSIGHTS_INSTRUMENTATIONKEY=$appInsightsKey" | |
| az $applicationType config appsettings set -g $resourceGroupName -n $applicationName --settings APPINSIGHTS_INSTRUMENTATIONKEY=$appInsightsKey >/dev/null | |
| echo " Configuring app setting ASPNETCORE_ENVIRONMENT=$environmentName" | |
| # [[ $environmentName = "live" ]] && ASPNETCORE_ENVIRONMENT="Production" || ASPNETCORE_ENVIRONMENT=$environmentName | |
| az $applicationType config appsettings set -g $resourceGroupName -n $applicationName --settings ASPNETCORE_ENVIRONMENT=$environmentName >/dev/null | |
| echo " Creating managed ID for web/fn app to access KV" | |
| principalId=$(az $applicationType identity assign --name $applicationName -g $resourceGroupName -o tsv --query 'principalId') | |
| #echo Getting the just-created PrincipalId... | |
| #principalId=$(az $applicationType identity show -n $applicationName --resource-group $resourceGroupName -o tsv --query 'principalId') | |
| echo " Allowing application service principal $principalId to get/list KeyVault values from $keyvaultName" | |
| az keyvault set-policy --name $keyvaultName -g $resourceGroupName --object-id $principalId --secret-permissions get list >/dev/null | |
| echo " $applicationName app created." | |
| } | |
| create_webapp() { | |
| local applicationName=$1 | |
| shift | |
| local extraArgs=$@ | |
| webApplicationName="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$environmentName-$applicationName-$WEBAPP_SHORTNAME" | |
| create_app "webapp" $webApplicationName $keyvaultName $extraArgs | |
| az webapp config set -g $resourceGroupName -n $webApplicationName >/dev/null | |
| # Add boostrap settings to webapp config. Can't do this after create_ad_apps bcos web app doesn't exist yet | |
| echo " Configuring app settings for bootstrapping the SPA" | |
| applicationNamePrefix="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$environmentName-$APP_REGISTRATION_SUFFIX" | |
| apiApplicationName="$applicationNamePrefix-$API_APP_SHORTNAME" | |
| spaApplicationName="$applicationNamePrefix-$SPA_APP_SHORTNAME" | |
| apiApplicationId=$(az ad app list --display-name "$apiApplicationName" -o tsv --query [].appId) | |
| spaApplicationId=$(az ad app list --display-name "$spaApplicationName" -o tsv --query [].appId) | |
| echo " Configuring app setting SpaAppSettings__SpaAuthSettings__ApiApplicationResourceId=api://$apiApplicationId" | |
| az webapp config appsettings set -g $resourceGroupName -n $webApplicationName \ | |
| --settings "SpaAppSettings__SpaAuthSettings__ApiApplicationResourceId"=api://$apiApplicationId >/dev/null | |
| echo " Configuring app setting SpaAppSettings__SpaAuthSettings__SpaApplicationResourceId=$spaApplicationId" | |
| az webapp config appsettings set -g $resourceGroupName -n $webApplicationName \ | |
| --settings "SpaAppSettings__SpaAuthSettings__SpaApplicationResourceId"=$spaApplicationId >/dev/null | |
| } | |
| create_functionapp() { | |
| echo " create_functionapp($@)" | |
| local applicationName=$1 | |
| local getUpdatedDataCronSpec=$2 | |
| shift | |
| shift | |
| local extraArgs=$@ | |
| functionAppName="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$environmentName-$applicationName-$FUNCTIONAPP_SHORTNAME" | |
| webApplicationName="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$environmentName-$applicationName-$WEBAPP_SHORTNAME" | |
| create_app "functionapp" $functionAppName $keyvaultName \ | |
| -g $resourceGroupName \ | |
| -p $appServicePlanName \ | |
| -s $storageAccountName \ | |
| --runtime dotnet \ | |
| --app-insights $appInsightsName \ | |
| --app-insights-key $appInsightsKey $extraArgs | |
| echo " Configuring functions app \"https://$functionAppName.azurewebsites.net\" in $keyvaultName" | |
| echo " Configuring app setting FunctionsAppEndpoint=https://$functionAppName.azurewebsites.net" | |
| az keyvault secret set --vault-name $keyvaultName --name "FunctionsAppEndpoint" \ | |
| --value "https://$functionAppName.azurewebsites.net" >/dev/null | |
| echo " Configuring app setting WcConfig--MyApplicationNameTokenApiUrl=https://$webApplicationName.azurewebsites.net/api/health/me" | |
| az keyvault secret set --vault-name $keyvaultName --name "WcConfig--MyApplicationNameTokenApiUrl" \ | |
| --value "https://$webApplicationName.azurewebsites.net/api/health/me" >/dev/null | |
| echo " Configuring app setting GraphApiChangeNotifications--NotificationSubscriptionEndpoint = \ | |
| https://$functionAppName.azurewebsites.net/api/GraphApiChangeNotificationsTrigger" | |
| az keyvault secret set --vault-name $keyvaultName \ | |
| --name "GraphApiChangeNotifications--NotificationSubscriptionEndpoint" \ | |
| --value "https://$functionAppName.azurewebsites.net/api/GraphApiChangeNotificationsTrigger" >/dev/null | |
| # TEMPORARILY disable shell expansion otherwise the asterisks in cron spec will mean "all files"! | |
| set -f | |
| echo " Configuring app setting TimerTrigger:GetUpdatedDataCron=$getUpdatedDataCronSpec" | |
| # Function App bindings won't read from KV, only config reading in code will do that. | |
| # We could work around that by explicitly adding the setting in function appsettings to point to KV using the | |
| # (e.g.) @Microsoft.KeyVault(VaultName=myvault;SecretName=mysecret;SecretVersion=ec96f02080254f109c51a1f14cdb1931) | |
| # syntax. But until we have >1 thing wanting to read this timertrigger it's probably YAGNI. It's not a secret. | |
| # az keyvault secret set --vault-name $keyvaultName --name "TimerTrigger--GetUpdatedDataCron" --value "$getUpdatedDataCronSpec" >/dev/null | |
| az functionapp config appsettings set --name $functionAppName \ | |
| --settings "TimerTrigger:GetUpdatedDataCron=$getUpdatedDataCronSpec" >/dev/null | |
| # per TEMP above | |
| set +f | |
| echo " Configuring CORS, allowing https://$webApplicationName.azurewebsites.net to access $functionAppName" | |
| az functionapp cors add -n $functionAppName -g $resourceGroupName --allowed-origins https://$webApplicationName.azurewebsites.net >/dev/null | |
| # Write to stderr so we can collect actions separate to stdout logging | |
| echo "" >&2 | |
| echo "==================================================================================================" >&2 | |
| echo "You may need to **manually** change the version on $functionAppName to '~3' because" >&2 | |
| echo az functionapp config appsettings set --name $functionAppName \ | |
| --resource-group $resourceGroupName \ | |
| --settings FUNCTIONS_EXTENSION_VERSION=~3 >&2 | |
| echo "is not working." >&2 | |
| echo "==================================================================================================" >&2 | |
| echo "" >&2 | |
| } | |
| echo "Creating Web App for '$applicationName'" | |
| create_webapp $applicationName | |
| # TEMPORARILY disable file globbing otherwise the asterisks in cron spec will mean "all files". | |
| set -f | |
| # Creates a fn app in the same app service plan as the web app | |
| echo "Creating Functions App for '$applicationName'" | |
| create_functionapp $applicationName "0 */1 * * * *" # i.e. "data updated" fn runs every minute | |
| set +f |
| #!/bin/bash | |
| # Keep these up-to-date or you'll break settings! | |
| declare -A appIdForEnvironment=( | |
| ["local"]="4fdf3bfa-bab5-48ec-9b45-f9a51174c780" | |
| ["uat"]="9aa37d00-6264-44c5-aa66-cde415225caa" | |
| ["demo"]="B44A5F88-5FFC-4680-A5CA-DB11438F7C9E" | |
| ["live"]="" | |
| ) | |
| # ensure bash exits on first error | |
| set -e | |
| ENVIRONMENT_NAME=$1 | |
| if [[ -z $ENVIRONMENT_NAME ]]; then | |
| echo " " >&2 | |
| echo " ERROR: Missing argument." >&2 | |
| echo " " >&2 | |
| echo " $0" >&2 | |
| echo " " >&2 | |
| echo " A shell script to creates project resources on Azure. It will create resource group, appserviceplan, webapp(s)" >&2 | |
| echo " functionapp(s), SQL Server, etc. for given 'ENVIRONMENT' (UAT, Live, etc.) and skip them if they already exist." >&2 | |
| echo " " >&2 | |
| echo " USAGE: " >&2 | |
| echo " " >&2 | |
| echo " $ $0.sh <environment> [applicationName='MyApplicationName']" >&2 | |
| echo " " >&2 | |
| echo " EXAMPLES:" >&2 | |
| echo " " >&2 | |
| echo " '$0 live'" >&2 | |
| echo " '$0 uat MyApplicationName MyAzureSubscription'" >&2 | |
| echo " " >&2 | |
| exit 1 | |
| fi | |
| applicationName=${2:-MyApplicationName} | |
| # Grab our "naming conventions" - | |
| # moved to separate file so it can be shared by scripts | |
| set -a | |
| . ./.naming-rules.txt | |
| set +a | |
| TEAMS_MANIFEST_APP_ID=${appIdForEnvironment[$ENVIRONMENT_NAME]} | |
| if [[ -z $TEAMS_MANIFEST_APP_ID ]]; then | |
| echo " " >&2 | |
| echo " ERROR: No App Manifest ID found for '$ENVIRONMENT_NAME' environment." >&2 | |
| echo " " >&2 | |
| echo " Please check this script and update mapping (associative array) for 'appIdForEnvironment'." >&2 | |
| echo " " >&2 | |
| exit | |
| fi | |
| # Will print device code and login URL to console (stdout) if not logged in. | |
| . ./ensure-logged-in.sh $SUBSCRIPTION $LOCATION_NAME | |
| # Create resource group. | |
| RESOURCE_GROUP_NAME="$CLIENT_SHORTNAME-$ENVIRONMENT_NAME-$RESOURCE_GROUP_SHORTNAME" | |
| echo "Upserting '$RESOURCE_GROUP_NAME' resource group" | |
| . ./upsert-resource.sh "group" $RESOURCE_GROUP_NAME | |
| # NewOrbit Guidelines: Provide contact and environment at RG level. | |
| echo "Tagging '$RESOURCE_GROUP_NAME' with Environment=$ENVIRONMENT_NAME" | |
| az group update --name $RESOURCE_GROUP_NAME --tags Owner=petevb Environment=$ENVIRONMENT_NAME >/dev/null | |
| # Set the default resource group | |
| echo "Setting default resource group to $RESOURCE_GROUP_NAME" | |
| az configure --defaults group=$RESOURCE_GROUP_NAME >/dev/null | |
| # Create App Insights and get the key as we need it for webapps and functions | |
| APP_INSIGHTS_NAME="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$ENVIRONMENT_NAME-$APP_INSIGHTS_SHORTNAME" | |
| echo "Creating AppInsights resource $APP_INSIGHTS_NAME" | |
| . ./upsert-resource.sh "resource" $APP_INSIGHTS_NAME --namespace Microsoft.Insights \ | |
| --resource-type components --properties '{"Application_Type":"web"}' | |
| APP_INSIGHTS_KEY=$(az resource show --namespace Microsoft.Insights \ | |
| --resource-type components -n $APP_INSIGHTS_NAME --query properties.InstrumentationKey -o tsv) | |
| # Create KeyVault | |
| KEYVAULT_NAME="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$ENVIRONMENT_NAME-$KEYVAULT_SHORTNAME" | |
| echo "Creating KeyVault $KEYVAULT_NAME" | |
| . ./upsert-resource.sh "keyvault" $KEYVAULT_NAME | |
| # Create an app registration for SPA & API | |
| echo "Creating Azure AD Apps for $ENVIRONMENT_NAME" | |
| . ./create-ad-apps.sh $ENVIRONMENT_NAME $applicationName | |
| # Create Storage account and save Storage ConnectionString to KeyVault | |
| STORAGE_ACCOUNT_NAME="$CLIENT_SHORTNAME$LOCATION_SHORTNAME$ENVIRONMENT_NAME" | |
| echo "Creating Storage Account Apps for $STORAGE_ACCOUNT_NAME" | |
| . ./upsert-resource.sh "storage account" $STORAGE_ACCOUNT_NAME -g $RESOURCE_GROUP_NAME -l $LOCATION_NAME --sku Standard_LRS | |
| storageConnectionString=$(az storage account show-connection-string -g $RESOURCE_GROUP_NAME \ | |
| -n $STORAGE_ACCOUNT_NAME -o tsv --query "connectionString") | |
| # NB: KV set will fail with MFA error if you didn't use a device code | |
| echo "Adding AzureStorage--ConnectionString to $KEYVAULT_NAME" | |
| az keyvault secret set --vault-name $KEYVAULT_NAME --name "AzureStorage--ConnectionString" --value $storageConnectionString >/dev/null | |
| az keyvault secret set --vault-name $KEYVAULT_NAME --name "AzureWebJobsStorage" --value $storageConnectionString >/dev/null | |
| # Create an app service plan | |
| APP_SERVICE_PLAN_NAME="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$ENVIRONMENT_NAME-$APP_SERVICE_PLAN_SHORTNAME" | |
| echo "Creating App Service Plan $APP_SERVICE_PLAN_NAME" | |
| . ./upsert-resource.sh "appservice plan" $APP_SERVICE_PLAN_NAME --sku $APP_SERVICE_PLAN_SKU # --is-linux | |
| # Create MyApplicationName web & function apps | |
| . ./create-apps.sh $ENVIRONMENT_NAME $applicationName | |
| # Creates an eventBus storage bus namespace, topic(s) and subscription(s) | |
| echo "Creating Azure Service Bus for '$applicationName'" | |
| . ./create-event-bus.sh $applicationName | |
| # Creates azure signalR service | |
| SIGNALR_NAME="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$ENVIRONMENT_NAME-$SIGNALR_SHORTNAME" | |
| webApplicationName="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$ENVIRONMENT_NAME-$applicationName-$WEBAPP_SHORTNAME" | |
| functionAppName="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$ENVIRONMENT_NAME-$applicationName-$FUNCTIONAPP_SHORTNAME" | |
| echo "Creating Azure SignalR Service for $SIGNALR_NAME for '$webApplicationName' and '$functionAppName'" | |
| . ./create-signalr.sh $SIGNALR_NAME $webApplicationName $functionAppName | |
| # Create Cosmos DB account | |
| COSMOSDB_NAME="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$ENVIRONMENT_NAME-$COSMOSDB_SHORTNAME" | |
| echo "Creating Cosmos DB Account and Database $COSMOSDB_NAME" | |
| . ./create-cosmos-db.sh $COSMOSDB_NAME $DATABASE_NAME | |
| # Add a "secret" to help ID the KeyVault | |
| echo "Adding $ENVIRONMENT_NAME to $KEYVAULT_NAME for HealthCheck to test KV connection" | |
| az keyvault secret set --vault-name $KEYVAULT_NAME --name "HealthCheckTest" --value $ENVIRONMENT_NAME | |
| # Add the appId from the Teams manifest to KV for this environment | |
| echo "Adding Team Manifest ID ($TEAMS_MANIFEST_APP_ID) to $KEYVAULT_NAME" | |
| az keyvault secret set --vault-name $KEYVAULT_NAME --name "ManifestAppId" --value $TEAMS_MANIFEST_APP_ID | |
| # Endpoint for auth used to check user. | |
| webApplicationName="$CLIENT_SHORTNAME-$LOCATION_SHORTNAME-$ENVIRONMENT_NAME-$applicationName-$WEBAPP_SHORTNAME" | |
| echo "Adding WebApp name ($webApplicationName) to $KEYVAULT_NAME" | |
| az keyvault secret set --vault-name $KEYVAULT_NAME \ | |
| --name "WcConfig--MyApplicationNameTokenApiUrl" \ | |
| --value "https://$webApplicationName.azurewebsites.net/api/health/me" | |
| # Prompt dev to set these at end of script. If in KV we can set in one place. | |
| # Write to stderr so we can collect actions separate to stdout logging | |
| echo >&2 | |
| echo ============================================================================================================================================ >&2 | |
| echo You may now need to set the TC API in config too: >&2 | |
| echo az keyvault secret set --vault-name $KEYVAULT_NAME --name "TcConfig--TimeCapchaUrl" --value https://timecapchaapi-no.azurewebsites.net/ >&2 | |
| echo az keyvault secret set --vault-name $KEYVAULT_NAME --name "TcConfig--TimeCapchaUser" --value WC >&2 | |
| echo az keyvault secret set --vault-name $KEYVAULT_NAME --name "TcConfig--TimeCapchaSecret" --value [secret] >&2 | |
| echo ============================================================================================================================================ >&2 | |
| echo >&2 | |
| echo You may ALSO need to configure the Hybrid Connector in KeyVault too: >&2 | |
| echo az keyvault secret set --vault-name $KEYVAULT_NAME --name "TenantConfiguration:Tenants:0:TenantId" --value [Tenant GUID] >&2 | |
| echo az keyvault secret set --vault-name $KEYVAULT_NAME --name "TenantConfiguration:Tenants:0:ConnectionString" --value "Data Source=..." >&2 | |
| echo ============================================================================================================================================ >&2 | |
| echo >&2 | |
| # eof |
| #!/bin/bash | |
| # ensure bash exits on first error | |
| set -e | |
| if [ $# -lt 2 ]; then | |
| echo " usage: $0 <cosmosDbAccount> <databaseName>" >&2 | |
| exit 1 | |
| fi | |
| # Grab our "naming conventions" - | |
| # moved to separate file so it can be shared by scripts | |
| set -a | |
| . ./.naming-rules.txt | |
| set +a | |
| # take arguments and then remove | |
| cosmosDbAccount=$1 | |
| databaseName=$2 | |
| shift | |
| shift | |
| . ./upsert-resource.sh "cosmosdb" $cosmosDbAccount | |
| echo " Getting key for cosmos db $cosmosDbAccount" | |
| # need "raw" tsv bcos json will wrap response in quotes. '"AuthKey"' in KeyVault won't unlock a DB :) | |
| KEY=$(az cosmosdb keys list -n $cosmosDbAccount \ | |
| -g $RESOURCE_GROUP_NAME \ | |
| --query "primaryMasterKey" -o tsv) | |
| echo " Writing 'CosmosDb--AuthKey' to KeyVault $KEYVAULT_NAME" | |
| az keyvault secret set --vault-name $KEYVAULT_NAME \ | |
| --name "CosmosDb--AuthKey" \ | |
| --value $KEY >/dev/null | |
| ENDPOINT="https://$cosmosDbAccount.documents.azure.com:443/" | |
| echo " Writing 'CosmosDb--CosmosDbEndpoint' $ENDPOINT to KeyVault $KEYVAULT_NAME" | |
| az keyvault secret set --vault-name $KEYVAULT_NAME \ | |
| --name "CosmosDb--CosmosDbEndpoint" \ | |
| --value $ENDPOINT >/dev/null | |
| echo " Getting connection string for cosmos db" | |
| CS=$(az cosmosdb keys list -n $cosmosDbAccount \ | |
| -g $RESOURCE_GROUP_NAME \ | |
| --type connection-strings \ | |
| -o tsv \ | |
| --query "connectionStrings[?description == 'Primary SQL Connection String'].connectionString | [0]") | |
| echo " Writing 'CosmosDbConnectionString' to KeyVault $KEYVAULT_NAME" | |
| az keyvault secret set --vault-name $KEYVAULT_NAME --name "CosmosDbConnectionString" --value $CS >/dev/null | |
| echo " Checking for Cosmos database '$cosmosDbAccount/$databaseName':" | |
| DB_EXISTS=$(az cosmosdb database exists --db-name $databaseName --key $KEY --name $cosmosDbAccount-o json) | |
| if [ "true" == "$DB_EXISTS" ]; then | |
| echo " Cosmos Database '$cosmosDbAccount/$databaseName' already exists; skipping." | |
| else | |
| echo " Creating Cosmos Database '$cosmosDbAccount/$databaseName':" | |
| az cosmosdb database create --db-name $databaseName \ | |
| --key $KEY \ | |
| --name $cosmosDbAccount \ | |
| --throughput 400 >/dev/null | |
| echo " Created '$cosmosDbAccount/$databaseName'." | |
| fi |
| #!/bin/bash | |
| # ensure bash exits on first error | |
| set -e | |
| if [ "$#" -ne 1 ]; then | |
| echo " usage: $0 <serviceBusName>" >&2 | |
| exit 1 | |
| fi | |
| # Grab our "naming conventions" - | |
| # moved to separate file so it can be shared by scripts | |
| set -a | |
| . ./.naming-rules.txt | |
| set +a | |
| # take arguments and then remove | |
| serviceBusName=$1 | |
| shift | |
| NAMESPACE_NAME="${CLIENT_SHORTNAME}-${LOCATION_SHORTNAME}-${ENVIRONMENT_NAME}-${serviceBusName}-${SERVICE_BUS_NAMESPACE_SHORTNAME}" | |
| . ./upsert-resource.sh "servicebus namespace" $NAMESPACE_NAME \ | |
| -g $RESOURCE_GROUP_NAME \ | |
| -l $LOCATION_NAME \ | |
| --sku Standard | |
| echo " Getting connection string for azure servicebus namespace" | |
| connectionString=$(az servicebus namespace authorization-rule keys list \ | |
| --resource-group $RESOURCE_GROUP_NAME \ | |
| --namespace-name $NAMESPACE_NAME \ | |
| --name RootManageSharedAccessKey \ | |
| --query primaryConnectionString \ | |
| --output tsv) | |
| # NOTE: if we use input binding for servicebus then we\'ll need to add to functionapp settings too :( | |
| echo " writing MyApplicationNameEventBusConnectionString to KeyVault $KEYVAULT_NAME" | |
| az keyvault secret set --vault-name $KEYVAULT_NAME \ | |
| --name "MyApplicationNameEventBusConnectionString" \ | |
| --value $connectionString >/dev/null | |
| # create topic(s) and subscription(s) here | |
| . ./upsert-resource.sh "servicebus topic" "TimeEntryUpserted" --namespace-name $NAMESPACE_NAME -g $RESOURCE_GROUP_NAME | |
| . ./upsert-resource.sh "servicebus topic subscription" "MyApplicationNameTimeEntries" \ | |
| --namespace-name $NAMESPACE_NAME \ | |
| -g $RESOURCE_GROUP_NAME \ | |
| --topic-name "TimeEntryUpserted" |
| #!/bin/bash | |
| # ensure bash exits on first error | |
| set -e | |
| if [ $# -lt 3 ]; then | |
| echo " usage: $0 <signalRName> <webAppName> <functionAppName> [options]" >&2 | |
| exit 1 | |
| fi | |
| # Grab our "naming conventions" - | |
| # moved to separate file so it can be shared by scripts | |
| set -a | |
| . ./.naming-rules.txt | |
| set +a | |
| # take arguments and then remove | |
| signalRName=$1 | |
| webAppName=$2 | |
| functionAppName=$3 | |
| shift | |
| shift | |
| shift | |
| # "DEBUG" | |
| # echo "signalRName=$signalRName" | |
| # echo "webAppName=$webAppName" | |
| # echo "functionAppName=$functionAppName" | |
| # echo "RESOURCE_GROUP_NAME=$RESOURCE_GROUP_NAME" | |
| # echo "KEYVAULT_NAME=$KEYVAULT_NAME" | |
| # read -p "any key for signalr" | |
| # take the rest of the arguments as extra arguments for create command | |
| extraArgs=$@ | |
| . ./upsert-resource.sh "signalr" $signalRName -g $RESOURCE_GROUP_NAME --sku Free_F1 --unit-count 1 \ | |
| --service-mode Serverless $extraArgs | |
| echo " Getting connection string for azure signalR" | |
| azureSignalRConnectionString=$(az signalr key list --name $signalRName --resource-group $RESOURCE_GROUP_NAME --query primaryConnectionString -o tsv) | |
| echo " Adding $AzureSignalRConnectionString to $KEYVAULT_NAME" | |
| az keyvault secret set --vault-name $KEYVAULT_NAME --name "AzureSignalRConnectionString" --value $azureSignalRConnectionString >/dev/null | |
| # Per comments in create_functionapp it seems we cannot use KV for input binding, i.e. can't get the URL to the KV | |
| # setting from script. | |
| echo " Writing SignalR connection string to FunctionApp $functionAppName" | |
| az functionapp config appsettings set --name $functionAppName --settings "AzureSignalRConnectionString=$azureSignalRConnectionString" >/dev/null | |
| echo " Allowing CORS https://$webAppName.azurewebsites.net from $signalRName" | |
| az signalr cors add -n $signalRName --allowed-origins https://$webAppName.azurewebsites.net |
| #! /usr/bin/env node | |
| // Expect the existing app JSON as an arg | |
| const json = process.argv[2]; | |
| const azAdAppData = JSON.parse(json); | |
| let changes = false; | |
| // There can't be a duplicate 'execute', need to disable any existing permission | |
| const disableExistingPermission = (permission) => { | |
| if (permission.value === "execute") { | |
| permission.isEnabled = false; | |
| changes = true; | |
| } | |
| return permission; | |
| }; | |
| azAdAppData.oauth2Permissions.map(disableExistingPermission); | |
| if (changes) { | |
| console.log(JSON.stringify(azAdAppData.oauth2Permissions)); | |
| } |
| #!/bin/bash | |
| # ensure bash exits on first error | |
| set -e | |
| SUBSCRIPTION=${1:-Playground} | |
| LOCATION_NAME=${2:-westeurope} | |
| if ! az account get-access-token --subscription $SUBSCRIPTION -o tsv --query "expiresOn" >/dev/null 2>&1; then | |
| # My Windows machine was playing up when signing into `az` in Ubuntu/WSL2 | |
| # login may work better with `--use-device-code`, YMMV. | |
| # https://github.com/Azure/azure-cli/issues/6962 | |
| # Logging in interactively (the normal way) may cause MFA errors in this script. | |
| echo " You need to login, e.g. az login --use-device-code" >&2 | |
| exit 1 | |
| else | |
| echo " Already logged in; continuing" | |
| fi | |
| echo " az configure --defaults location=$LOCATION_NAME" | |
| az configure --defaults location=$LOCATION_NAME | |
| echo " az account set -s $SUBSCRIPTION" | |
| az account set -s $SUBSCRIPTION |
| { | |
| "adminConsentDescription": "Allows the app to execute methods on the API", | |
| "adminConsentDisplayName": "Execute methods on the API", | |
| "isEnabled": true, | |
| "type": "User", | |
| "userConsentDescription": "Allows the app to execute methods on the API", | |
| "userConsentDisplayName": "Execute methods on the API", | |
| "value": "execute" | |
| } |
| #! /usr/bin/env node | |
| // Expect the existing app JSON as an arg | |
| const uuid = process.argv[2]; | |
| const name = process.argv[3]; | |
| const json = process.argv[4]; | |
| const azAdAppData = JSON.parse(json); | |
| const newExecuteRoleScopeJson = require("./execute-role.json"); | |
| const displayName = `Execute methods on the ${name} API`; | |
| newExecuteRoleScopeJson.adminConsentDisplayName = displayName; | |
| newExecuteRoleScopeJson.userConsentDisplayName = displayName; | |
| newExecuteRoleScopeJson.id = uuid; | |
| const oauth2Permissions = azAdAppData.oauth2Permissions.filter( | |
| (permission) => permission.value != "execute" | |
| ); | |
| oauth2Permissions.push(newExecuteRoleScopeJson); | |
| console.log(JSON.stringify(oauth2Permissions)); |
| #!/bin/bash | |
| # ensure bash exits on first error | |
| set -e | |
| if [ $# -lt 2 ]; then | |
| echo " usage: $0 <resourceType> <resourceName> [options]" >&2 | |
| exit 1 | |
| fi | |
| # Grab our "naming conventions" - | |
| # moved to separate file so it can be shared by scripts | |
| set -a | |
| . ./.naming-rules.txt | |
| set +a | |
| # take first two arguments as resource type and name | |
| resourceType=$1 | |
| resourceName=$2 | |
| # remove first two arguments | |
| shift | |
| shift | |
| # take the rest of the arguments as extra arguments for create command | |
| extraArgs=$@ | |
| echo " Checking Azure $resourceType '$resourceName':" | |
| if az $resourceType list -o table | grep -q $resourceName; then | |
| echo " $resourceType '$resourceName' already exists; skipping." | |
| else | |
| echo " Creating ${resourceType} '${resourceName}':" | |
| echo " az $resourceType create --name $resourceName $extraArgs" | |
| az $resourceType create --name $resourceName $extraArgs >/dev/null | |
| echo " Created ${resourceType} '${resourceName}'." | |
| fi |