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 |