Skip to content

Instantly share code, notes, and snippets.

@petevb
Last active July 29, 2020 12:55
Show Gist options
  • Save petevb/11e39d7b55e87322cda7c361a2669d1e to your computer and use it in GitHub Desktop.
Save petevb/11e39d7b55e87322cda7c361a2669d1e to your computer and use it in GitHub Desktop.
Create Azure Environment in bash with az CLI
_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"

instructions

run . ./create-azure-environment.sh e.g.:

$ . ./create-azure-environment.sh uat
[
{
"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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment