|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +set -eu # use set -eux for debugging |
| 4 | + |
| 5 | +function load_env_variables() { |
| 6 | + set -a |
| 7 | + source .env |
| 8 | + set +a |
| 9 | +} |
| 10 | + |
| 11 | +function checkRequiredParams () { |
| 12 | + requiredParams=( |
| 13 | + LOCATION |
| 14 | + RESOURCE_GROUP |
| 15 | + SUBSCRIPTION_ID |
| 16 | + AAD_CLIENT_ID |
| 17 | + AAD_OBJECT_ID |
| 18 | + AAD_TENANT_ID |
| 19 | + ) |
| 20 | + local paramsFile=$1 |
| 21 | + for param in "${requiredParams[@]}"; do |
| 22 | + local paramValue=$(jq -r .$param < $paramsFile) |
| 23 | + if [ "null" == "$paramValue" ] || [ -z "$paramValue" ]; then |
| 24 | + echo "Parameter $param is required, exiting..." |
| 25 | + exit 1 |
| 26 | + fi |
| 27 | + done |
| 28 | +} |
| 29 | + |
| 30 | +function populateRequiredParams () { |
| 31 | + local paramsFile=$1 |
| 32 | + printf "Checking required parameters... " |
| 33 | + checkRequiredParams $paramsFile |
| 34 | + # The jq command below sets environment variables based on the key-value pairs in a JSON-formatted file |
| 35 | + eval $(jq -r 'to_entries | .[] | "export \(.key)=\(.value)"' $paramsFile) |
| 36 | + printf "Done.\n" |
| 37 | +} |
| 38 | + |
| 39 | +function set_variables() { |
| 40 | + printf "Setting environment variables...\n" |
| 41 | + SUBSCRIPTION_ID=${SUBSCRIPTION_ID:-""} |
| 42 | + RESOURCE_GROUP=${RESOURCE_GROUP:-""} |
| 43 | + LOCATION=${LOCATION:-""} |
| 44 | + AAD_CLIENT_ID=${AAD_CLIENT_ID:-""} |
| 45 | + AAD_OBJECT_ID=${AAD_OBJECT_ID:-""} |
| 46 | + AAD_TENANT_ID=${AAD_TENANT_ID:-""} |
| 47 | + AAD_TOKEN_ISSUER_URL=${AAD_TOKEN_ISSUER_URL:-"https://login.microsoftonline.com/$AAD_TENANT_ID/v2.0"} |
| 48 | + IMAGE_NAME=${IMAGE_NAME:-"graphrag:frontend"} |
| 49 | + REGISTRY_NAME=${REGISTRY_NAME:-"${RESOURCE_GROUP}reg"} |
| 50 | + APP_SERVICE_PLAN=${APP_SERVICE_PLAN:-"${RESOURCE_GROUP}-asp"} |
| 51 | + WEB_APP=${WEB_APP:-"${RESOURCE_GROUP}-playground"} |
| 52 | + WEB_APP_IDENTITY=${WEB_APP_IDENTITY:-"${WEB_APP}-identity"} |
| 53 | + #BACKEND_RESOURCE_GROUP=${BACKEND_RESOURCE_GROUP:-""} # needed for backend outbound vnet integration |
| 54 | + printf "Done setting environment variables.\n" |
| 55 | +} |
| 56 | + |
| 57 | +function create_resource_group { |
| 58 | + printf "Setting subsctiption to $SUBSCRIPTION_ID and Creating resource group...\n" |
| 59 | + az account set --subscription $SUBSCRIPTION_ID > /dev/null |
| 60 | + az group create --name $RESOURCE_GROUP --location $LOCATION > /dev/null |
| 61 | + printf "Resource group created.\n" |
| 62 | +} |
| 63 | + |
| 64 | +function create_acr() { |
| 65 | + printf "Creating Azure Container Registry...\n" |
| 66 | + az acr create --resource-group $RESOURCE_GROUP \ |
| 67 | + --name $REGISTRY_NAME \ |
| 68 | + --sku Basic \ |
| 69 | + --admin-enabled true > /dev/null |
| 70 | + printf "Azure Container Registry created.\n" |
| 71 | +} |
| 72 | + |
| 73 | +function build_and_push_image() { |
| 74 | + printf "Building and pushing image...\n" |
| 75 | + local SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )"; |
| 76 | + az acr build --registry $REGISTRY_NAME -f $SCRIPT_DIR/../docker/Dockerfile-frontend --image $IMAGE_NAME $SCRIPT_DIR/../ |
| 77 | + printf "Image built and pushed.\n" |
| 78 | +} |
| 79 | + |
| 80 | +function create_app_service_plan() { |
| 81 | + printf "Creating app service plan...\n" |
| 82 | + az appservice plan create --name $APP_SERVICE_PLAN \ |
| 83 | + --resource-group $RESOURCE_GROUP \ |
| 84 | + --sku B3 \ |
| 85 | + --is-linux > /dev/null |
| 86 | + printf "App service plan created.\n" |
| 87 | +} |
| 88 | + |
| 89 | +function create_web_app() { |
| 90 | + printf "Creating web app...\n" |
| 91 | + az webapp create --resource-group $RESOURCE_GROUP \ |
| 92 | + --plan $APP_SERVICE_PLAN \ |
| 93 | + --name $WEB_APP \ |
| 94 | + --deployment-container-image-name $REGISTRY_NAME.azurecr.io/$IMAGE_NAME > /dev/null |
| 95 | + printf "Web app created.\n" |
| 96 | +} |
| 97 | + |
| 98 | +function create_web_app_identity() { |
| 99 | + printf "Creating web app identity...\n" |
| 100 | + IDENTITY_RESULT=$(az identity create --resource-group $RESOURCE_GROUP --name $WEB_APP_IDENTITY --output json) |
| 101 | + WEBAPP_IDENTITY_ID=$(jq -r .id <<< $IDENTITY_RESULT) |
| 102 | + WEBAPP_IDENTITY_OBJECT_ID=$(jq -r .principalId <<< $IDENTITY_RESULT) |
| 103 | + WEBAPP_IDENTITY_CLIENT_ID=$(jq -r .clientId <<< $IDENTITY_RESULT) |
| 104 | + az webapp identity assign --name $WEB_APP \ |
| 105 | + --resource-group $RESOURCE_GROUP \ |
| 106 | + --identities $WEBAPP_IDENTITY_ID > /dev/null |
| 107 | + printf "Web app identity created.\n" |
| 108 | +} |
| 109 | +function configure_registry_credentials() { |
| 110 | + printf "Configuring registry credentials...\n" |
| 111 | + ACR_ID=$(az acr show --name $REGISTRY_NAME --resource-group $RESOURCE_GROUP --query id --output tsv) |
| 112 | + az role assignment create --assignee $WEBAPP_IDENTITY_CLIENT_ID \ |
| 113 | + --role AcrPull \ |
| 114 | + --scope $ACR_ID > /dev/null |
| 115 | + az webapp config container set --name $WEB_APP \ |
| 116 | + --resource-group $RESOURCE_GROUP \ |
| 117 | + --container-image-name $REGISTRY_NAME.azurecr.io/$IMAGE_NAME > /dev/null |
| 118 | + printf "Registry credentials configured.\n" |
| 119 | +} |
| 120 | + |
| 121 | +function configure_app_settings() { |
| 122 | + printf "Configuring app settings...\n" |
| 123 | + APP_SETTINGS="" |
| 124 | + while IFS='=' read -r name value |
| 125 | + do |
| 126 | + value="${value%\"}" # Remove opening quote |
| 127 | + value="${value#\"}" # Remove closing quote |
| 128 | + APP_SETTINGS="$APP_SETTINGS $name=$value" |
| 129 | + done < .env |
| 130 | + # echo $APP_SETTINGS |
| 131 | + az webapp config appsettings set --name $WEB_APP \ |
| 132 | + --resource-group $RESOURCE_GROUP \ |
| 133 | + --settings $APP_SETTINGS > /dev/null |
| 134 | + printf "App settings configured.\n" |
| 135 | +} |
| 136 | + |
| 137 | +function create_federated_identity_credentials() { |
| 138 | + printf "Creating federated identity credentials...\n" |
| 139 | + EXISTING_CREDENTIAL_SUBJECTS=$(az rest --method GET --uri "https://graph.microsoft.com/beta/applications/$AAD_OBJECT_ID/federatedIdentityCredentials" -o json | jq -r '.value[].subject') |
| 140 | + if [[ "$EXISTING_CREDENTIAL_SUBJECTS" == *"$WEBAPP_IDENTITY_OBJECT_ID"* ]]; then |
| 141 | + echo "Federated identity credential already exists for the subject: $WEBAPP_IDENTITY_OBJECT_ID" |
| 142 | + else |
| 143 | + az webapp auth update \ |
| 144 | + --name $WEB_APP \ |
| 145 | + --resource-group $RESOURCE_GROUP \ |
| 146 | + --enabled true \ |
| 147 | + --action LoginWithAzureActiveDirectory \ |
| 148 | + --aad-client-id $AAD_CLIENT_ID \ |
| 149 | + --aad-token-issuer-url $AAD_TOKEN_ISSUER_URL > /dev/null |
| 150 | + az rest --method POST \ |
| 151 | + --uri "https://graph.microsoft.com/beta/applications/$AAD_OBJECT_ID/federatedIdentityCredentials" \ |
| 152 | + --body "{'name': '$WEB_APP', 'issuer': '$AAD_TOKEN_ISSUER_URL', 'subject': '$WEBAPP_IDENTITY_OBJECT_ID', 'audiences': [ 'api://AzureADTokenExchange' ]}" > /dev/null |
| 153 | + fi |
| 154 | + printf "Federated identity credentials created.\n" |
| 155 | +} |
| 156 | + |
| 157 | +function configure_auth_settings() { |
| 158 | + printf "Configuring auth settings...\n" |
| 159 | + az webapp config appsettings set --resource-group $RESOURCE_GROUP \ |
| 160 | + --name $WEB_APP \ |
| 161 | + --slot-settings OVERRIDE_USE_MI_FIC_ASSERTION_CLIENTID=$WEBAPP_IDENTITY_CLIENT_ID \ |
| 162 | + --verbose > /dev/null |
| 163 | + az webapp config appsettings list --resource-group $RESOURCE_GROUP \ |
| 164 | + --name $WEB_APP > /dev/null |
| 165 | + |
| 166 | + authSettings=$(az rest --method GET --url "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Web/sites/$WEB_APP/config/authsettingsV2/list?api-version=2020-12-01" --output json) |
| 167 | + echo $authSettings > auth.json |
| 168 | + jq '.properties.identityProviders.azureActiveDirectory.registration.clientSecretSettingName = "OVERRIDE_USE_MI_FIC_ASSERTION_CLIENTID"' auth.json > tmp.json && mv tmp.json auth.json |
| 169 | + az rest --method PUT \ |
| 170 | + --url "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Web/sites/$WEB_APP/config/authsettingsV2?api-version=2020-12-01" \ |
| 171 | + --body @auth.json \ |
| 172 | + --headers "Content-Type=application/json" > /dev/null |
| 173 | + rm auth.json |
| 174 | + printf "Auth settings configured.\n" |
| 175 | +} |
| 176 | + |
| 177 | +function enable_https() { |
| 178 | + printf "Enabling HTTPS...\n" |
| 179 | + az webapp update --name $WEB_APP \ |
| 180 | + --resource-group $RESOURCE_GROUP \ |
| 181 | + --set httpsOnly=true > /dev/null |
| 182 | + printf "HTTPS enabled.\n" |
| 183 | +} |
| 184 | + |
| 185 | +function update_appreg_redirect_uris() { |
| 186 | + printf "Updating app registration redirect URIs...\n" |
| 187 | + WEB_APP_URL=$(az webapp show --name $WEB_APP --resource-group $RESOURCE_GROUP --query defaultHostName --output tsv) |
| 188 | + NEW_REDIRECT_URI=https://$WEB_APP_URL/.auth/login/aad/callback |
| 189 | + # Fetch the current list of web redirect URIs |
| 190 | + CURRENT_URIS=$(az ad app show --id $AAD_CLIENT_ID --query "web.redirectUris" --output tsv) |
| 191 | + if ! echo "${CURRENT_URIS}" | grep -q "${NEW_REDIRECT_URI}"; then |
| 192 | + az ad app update --id $AAD_CLIENT_ID --web-redirect-uris ${CURRENT_URIS[@]} "$NEW_REDIRECT_URI" > /dev/null |
| 193 | + fi |
| 194 | + printf "App registration redirect URIs updated.\n" |
| 195 | +} |
| 196 | + |
| 197 | +function restart_web_app() { |
| 198 | + printf "Restarting web app...\n" |
| 199 | + az webapp restart --name $WEB_APP --resource-group $RESOURCE_GROUP > /dev/null |
| 200 | + printf "Waiting for webapp to restart, webapp might take a few minutes to load.....\n" |
| 201 | + sleep 180 |
| 202 | + printf "Web app restarted. \n" |
| 203 | +} |
| 204 | + |
| 205 | +## The following function adds outbound vnet integration on the webapp so that the frontend container can access resources in the AKS cluster directly. |
| 206 | +## This will create a new subnet named "frontend" in the backend resource group's vnet if it does not exist. |
| 207 | +## This may not be needed in simplified backend architecture but useful for folks using a prior version of the accelerator that had a different network architecture. |
| 208 | +# function add_vnet_integration() { |
| 209 | +# VNET_NAME=$(az network vnet list --resource-group $BACKEND_RESOURCE_GROUP --query "[0].name" --output tsv) |
| 210 | +# VNET_ID=$(az network vnet list --resource-group $BACKEND_RESOURCE_GROUP --query "[0].id" --output tsv) |
| 211 | +# SUBNET_NAMES=$(az network vnet subnet list --resource-group $BACKEND_RESOURCE_GROUP --vnet-name $VNET_NAME --query "[].name" --output tsv) |
| 212 | +# SUBNET_NAME="frontend" |
| 213 | +# if [[ $SUBNET_NAMES == *$SUBNET_NAME* ]]; then |
| 214 | +# echo "Subnet with name $SUBNET_NAME already exists" |
| 215 | +# else |
| 216 | +# echo "Subnet with name $SUBNET_NAME does not exist, creating one now." |
| 217 | +# az network vnet subnet create --resource-group $BACKEND_RESOURCE_GROUP --vnet-name $VNET_NAME --name $SUBNET_NAME --address-prefixes 10.0.10.0/24 |
| 218 | +# fi |
| 219 | +# az webapp vnet-integration add --name $WEB_APP --resource-group $RESOURCE_GROUP --vnet $VNET_ID --subnet $SUBNET_NAME |
| 220 | +# } |
| 221 | + |
| 222 | +function usage() { |
| 223 | + echo |
| 224 | + echo "Usage: bash $0 [-h] -p <frontend_deploy.parameters.json>" |
| 225 | + echo "Description: Deployment script for the Frontend App for GraphRAG Solution Accelerator." |
| 226 | + echo "options:" |
| 227 | + echo " -h Print this help menu." |
| 228 | + echo " -p A JSON file containing the deployment parameters (frontend_deploy.parameters.json)." |
| 229 | + echo |
| 230 | +} |
| 231 | + |
| 232 | +function main() { |
| 233 | + load_env_variables |
| 234 | + populateRequiredParams $PARAMS_FILE |
| 235 | + set_variables |
| 236 | + create_resource_group |
| 237 | + create_acr |
| 238 | + build_and_push_image |
| 239 | + create_app_service_plan |
| 240 | + create_web_app |
| 241 | + create_web_app_identity |
| 242 | + configure_registry_credentials |
| 243 | + configure_app_settings |
| 244 | + create_federated_identity_credentials |
| 245 | + configure_auth_settings |
| 246 | + enable_https |
| 247 | + # add_vnet_integration |
| 248 | + update_appreg_redirect_uris |
| 249 | + restart_web_app |
| 250 | + echo "**********Graphrag Frontend Web app deployment successful!**********" |
| 251 | + echo "Please visit the webapp at https://$WEB_APP_URL" |
| 252 | + echo "*******************************************************************" |
| 253 | +} |
| 254 | + |
| 255 | +# print usage if no arguments are supplied |
| 256 | +[ $# -eq 0 ] && usage && exit 0 |
| 257 | +PARAMS_FILE="" |
| 258 | +while getopts ":p:h" option; do |
| 259 | + case "${option}" in |
| 260 | + p) |
| 261 | + PARAMS_FILE=${OPTARG} |
| 262 | + ;; |
| 263 | + h | *) |
| 264 | + usage |
| 265 | + exit 0 |
| 266 | + ;; |
| 267 | + esac |
| 268 | +done |
| 269 | +shift $((OPTIND-1)) |
| 270 | +# check if required arguments are supplied |
| 271 | +if [ ! -f $PARAMS_FILE ]; then |
| 272 | + echo "Error: invalid required argument." |
| 273 | + usage |
| 274 | + exit 1 |
| 275 | +fi |
| 276 | + |
| 277 | +main |
0 commit comments