2025-04-02 17:15:26 -04:00
#!/usr/bin/env bash
2024-06-26 15:45:06 -04:00
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
2024-08-07 09:51:56 -04:00
# set -ux # uncomment this line to debug
2025-04-02 17:15:26 -04:00
# TODO: use https://www.shellcheck.net to lint this script and make recommended updates
2024-06-26 15:45:06 -04:00
aksNamespace = "graphrag"
2025-04-02 17:15:26 -04:00
# Optional parameters with default values
AI_SEARCH_AUDIENCE = "https://search.azure.com"
AISEARCH_ENDPOINT_SUFFIX = "search.windows.net"
2024-06-26 15:45:06 -04:00
APIM_NAME = ""
2025-04-02 17:15:26 -04:00
APIM_TIER = "Developer"
CLOUD_NAME = "AzurePublicCloud"
GRAPHRAG_IMAGE = "graphrag:backend"
PUBLISHER_EMAIL = "publisher@microsoft.com"
PUBLISHER_NAME = "publisher"
2024-06-26 15:45:06 -04:00
RESOURCE_BASE_NAME = ""
2025-04-02 17:15:26 -04:00
COGNITIVE_SERVICES_AUDIENCE = "https://cognitiveservices.azure.com/.default"
CONTAINER_REGISTRY_LOGIN_SERVER = ""
GRAPHRAG_API_BASE = ""
GRAPHRAG_API_VERSION = "2023-03-15-preview"
GRAPHRAG_LLM_MODEL = "gpt-4"
GRAPHRAG_LLM_MODEL_VERSION = "turbo-2024-04-09"
GRAPHRAG_LLM_DEPLOYMENT_NAME = "gpt-4"
GRAPHRAG_LLM_MODEL_QUOTA = "80"
GRAPHRAG_EMBEDDING_MODEL = "text-embedding-ada-002"
GRAPHRAG_EMBEDDING_MODEL_VERSION = "2"
GRAPHRAG_EMBEDDING_DEPLOYMENT_NAME = "text-embedding-ada-002"
GRAPHRAG_EMBEDDING_MODEL_QUOTA = "300"
2025-04-04 15:52:21 +00:00
GRAPHRAG_LLM_MODEL_CONCURRENT_REQUEST = "15"
GRAPHRAG_EMBEDDING_MODEL_CONCURRENT_REQUEST = "15"
2024-06-26 15:45:06 -04:00
requiredParams = (
LOCATION
2025-04-02 17:15:26 -04:00
RESOURCE_GROUP
)
optionalParams = (
AI_SEARCH_AUDIENCE
AISEARCH_ENDPOINT_SUFFIX
APIM_NAME
APIM_TIER
CLOUD_NAME
GRAPHRAG_IMAGE
PUBLISHER_EMAIL
PUBLISHER_NAME
RESOURCE_BASE_NAME
COGNITIVE_SERVICES_AUDIENCE
CONTAINER_REGISTRY_LOGIN_SERVER
2024-06-26 15:45:06 -04:00
GRAPHRAG_API_BASE
GRAPHRAG_API_VERSION
GRAPHRAG_LLM_MODEL
2025-04-02 17:15:26 -04:00
GRAPHRAG_LLM_MODEL_QUOTA
GRAPHRAG_LLM_MODEL_VERSION
2024-06-26 15:45:06 -04:00
GRAPHRAG_LLM_DEPLOYMENT_NAME
GRAPHRAG_EMBEDDING_MODEL
2025-04-02 17:15:26 -04:00
GRAPHRAG_EMBEDDING_MODEL_QUOTA
GRAPHRAG_EMBEDDING_MODEL_VERSION
2024-06-26 15:45:06 -04:00
GRAPHRAG_EMBEDDING_DEPLOYMENT_NAME
2025-04-04 15:52:21 +00:00
GRAPHRAG_LLM_MODEL_CONCURRENT_REQUEST
GRAPHRAG_EMBEDDING_MODEL_CONCURRENT_REQUEST
2024-06-26 15:45:06 -04:00
)
2024-08-07 09:51:56 -04:00
errorBanner ( ) {
# https://cowsay-svelte.vercel.app
cat << "EOF"
________________________________
/ Uh oh, an error has occurred. \
\ Please see message below. /
‾‾‾‾‾‾‾‾‾‾/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
/
__ /
/ \
~ ~
/ \ /_\
\o / \o /
| |
|| | /
|| ||
|| ||
| \_ / |
\ /
\_ __/
EOF
printf "\n"
}
successBanner ( ) {
# https://patorjk.com/software/taag
cat << "EOF"
2024-09-12 21:41:46 -04:00
_____ __ _
/ ____| / _| | |
| ( ___ _ _ ___ ___ ___ ___ ___| | _ _ _| |
\_ __ \| | | | / __/ __/ _ / __/ __| _| | | | |
____) | | _| | ( _| ( _| __\_ _ \_ _ | | | | _| | |
| _____/ \_ _,_| \_ __\_ __\_ __| ___| ___| _| \_ _,_| _| _ _
2024-08-07 09:51:56 -04:00
| | | | | | | |
__| | ___ _ __ | | ___ _ _ _ __ ___ ___ _ __ | | _| |
/ _` | / _ | '_ \| |/ _ \| | | | ' _ ` _ \ / _ | ' _ \| __| |
| ( _| | __| | _) | | ( _) | | _| | | | | | | __| | | | | _| _|
\_ _,_| \_ __| .__/| _| \_ __/ \_ _, | _| | _| | _| \_ __| _| | _| \_ _( _)
2024-09-12 21:41:46 -04:00
| | __/ |
| _| | ___/
2024-08-07 09:51:56 -04:00
EOF
printf "\n\n"
}
startBanner ( ) {
# https://patorjk.com/software/taag
cat << "EOF"
2024-09-12 21:41:46 -04:00
_____ _ _____ _____
/ ____| | | | __ \ /\ / ____|
| | __ _ __ __ _ _ __ | | __ | | __) | / \ | | __
| | | _ | '__/ _` | ' _ \| ' _ \| _ / / /\ \| | | _ |
| | __| | | | ( _| | | _) | | | | | \ \ / ____ | | __| |
\_ ____| _| \_ _,_| .__/| _| | _| _| \_ /_/_ \_ \_ ____|
/\ | | | | | |
/ \ ___ ___| _| _| | ___ _ __ __ _| | _ ___ _ __
2024-08-07 09:51:56 -04:00
/ /\ \ / __/ __/ _ | | / _ | '__/ _` | __/ _ \| ' __|
2024-09-12 21:41:46 -04:00
/ ____ | ( _| ( _| __| | __| | | ( _| | || ( _) | |
/_/ \_ \_ __\_ __\_ __| _| \_ __| _| \_ _,_| \_ _\_ __/| _|
2024-08-07 09:51:56 -04:00
EOF
printf "\n\n"
}
2024-06-26 15:45:06 -04:00
exitIfCommandFailed ( ) {
local res = $1
local msg = $2
if [ 0 -ne $res ] ; then
2024-08-07 09:51:56 -04:00
errorBanner
2024-06-26 15:45:06 -04:00
printf " $msg \n "
exit 1
fi
}
exitIfValueEmpty ( ) {
local value = $1
local msg = $2
2024-08-07 09:51:56 -04:00
# check if the value is empty or "null" (jq returns "null" when a value is not found)
if [ -z " $value " ] || [ [ " $value " = = "null" ] ] ; then
errorBanner
2024-06-26 15:45:06 -04:00
printf " $msg \n "
exit 1
fi
}
2024-10-02 13:37:29 -04:00
exitIfThresholdExceeded ( ) {
local value = $1
local threshold = $2
local msg = $3
# throw an error if input value exceeds threshold
if [ $value -ge $threshold ] ; then
errorBanner
printf " $msg \n "
exit 1
fi
}
2024-06-26 15:45:06 -04:00
versionCheck ( ) {
# assume the version is in the format major.minor.patch
local TOOL = $1
local VERSION = $2
local MINIMUM_VERSION_REQUIREMENT = $3
for i in 1 2 3; do
part1 = $( echo $VERSION | cut -d "." -f $i )
part2 = $( echo $MINIMUM_VERSION_REQUIREMENT | cut -d "." -f $i )
if [ $part1 -gt $part2 ] ; then
return 0
fi
if [ $part1 -lt $part2 ] ; then
echo " $TOOL version requirement >= $MINIMUM_VERSION_REQUIREMENT , but you have version $VERSION "
exit 1
fi
done
}
checkRequiredTools ( ) {
2025-04-02 17:15:26 -04:00
local JQ_VERSION
local major minor patch
local YQ_VERSION
local AZ_VERSION
2024-06-26 15:45:06 -04:00
printf "Checking for required tools... "
which sed > /dev/null
exitIfCommandFailed $? "sed is required, exiting..."
which kubectl > /dev/null
exitIfCommandFailed $? "kubectl is required, exiting..."
2025-04-02 17:15:26 -04:00
which kubelogin > /dev/null
exitIfCommandFailed $? "kubelogin is required, exiting..."
2024-06-26 15:45:06 -04:00
which helm > /dev/null
exitIfCommandFailed $? "helm is required, exiting..."
which jq > /dev/null
exitIfCommandFailed $? "jq is required, exiting..."
which yq > /dev/null
exitIfCommandFailed $? "yq is required, exiting..."
which az > /dev/null
exitIfCommandFailed $? "azcli is required, exiting..."
which curl > /dev/null
exitIfCommandFailed $? "curl is required, exiting..."
2024-07-07 03:29:26 -04:00
# minimum version check for jq, yq, and az cli
2025-04-02 17:15:26 -04:00
JQ_VERSION = $( jq --version | cut -d'-' -f2)
2024-07-07 03:29:26 -04:00
IFS = '.' read -r major minor patch <<< " $JQ_VERSION "
if [ -z $patch ] ; then
2024-09-12 21:41:46 -04:00
# NOTE: older acceptable versions of jq report a version
2024-08-07 09:51:56 -04:00
# number without the patch number. if patch version is
# not present, set it to 0
2024-07-07 03:29:26 -04:00
patch = 0
JQ_VERSION = " $major . $minor . $patch "
fi
2025-04-02 17:15:26 -04:00
YQ_VERSION = $( yq --version | awk '{print substr($4,2)}' )
AZ_VERSION = $( az version -o json | jq -r '.["azure-cli"]' )
2024-07-07 03:29:26 -04:00
versionCheck "jq" $JQ_VERSION "1.6.0"
2024-06-26 15:45:06 -04:00
versionCheck "yq" $YQ_VERSION "4.40.7"
versionCheck "az cli" $AZ_VERSION "2.55.0"
printf "Done.\n"
}
checkRequiredParams ( ) {
local paramsFile = $1
2025-04-02 17:15:26 -04:00
local paramValue
2024-06-26 15:45:06 -04:00
for param in " ${ requiredParams [@] } " ; do
2025-04-02 17:15:26 -04:00
paramValue = $( jq -r .$param < $paramsFile )
2024-08-07 09:51:56 -04:00
if [ -z " $paramValue " ] || [ " $paramValue " = = "null" ] ; then
2024-06-26 15:45:06 -04:00
echo " Parameter $param is required, exiting... "
exit 1
fi
done
}
2025-04-02 17:15:26 -04:00
populateParams ( ) {
2024-06-26 15:45:06 -04:00
local paramsFile = $1
printf "Checking required parameters... "
checkRequiredParams $paramsFile
printf "Done.\n"
2025-04-02 17:15:26 -04:00
# The jq command below sets env variables based on the key-value pairs defined in a JSON-formatted parameters file.
# This will override default values of previously defined env variables.
eval $( jq -r 'to_entries | .[] | "export \(.key)=\(.value)"' $paramsFile )
2024-06-26 15:45:06 -04:00
2025-04-02 17:15:26 -04:00
# print environment variables for end user
echo "Setting environment variables..."
for param in " ${ requiredParams [@] } " ; do
# skip empty variables
if [ -z " ${ !param } " ] ; then
continue
fi
printf " \t $param = ${ !param } \n "
done
for param in " ${ optionalParams [@] } " ; do
# skip empty variables
if [ -z " ${ !param } " ] ; then
continue
fi
printf " \t $param = ${ !param } \n "
done
2024-06-26 15:45:06 -04:00
}
createResourceGroupIfNotExists ( ) {
local location = $1
local rg = $2
printf " Checking if resource group $rg exists... "
2025-01-30 13:59:51 -05:00
az group show -n $rg -o json > /dev/null 2>& 1
2024-06-26 15:45:06 -04:00
if [ $? -ne 0 ] ; then
printf "No.\n"
printf "Creating resource group... "
2025-01-30 13:59:51 -05:00
az group create -l $location -n $rg > /dev/null 2>& 1
2024-06-26 15:45:06 -04:00
printf "Done.\n"
else
printf "Yes.\n"
fi
}
2024-08-07 09:51:56 -04:00
getAksCredentials ( ) {
2024-06-26 15:45:06 -04:00
local rg = $1
2025-04-02 17:15:26 -04:00
local aks_name
local principalId
local scope
2024-06-26 15:45:06 -04:00
printf "Getting AKS credentials... "
2025-04-02 17:15:26 -04:00
aks_name = $( jq -r .azure_aks_name.value <<< $AZURE_DEPLOY_OUTPUTS )
az aks get-credentials -g $rg -n $aks_name --overwrite-existing > /dev/null 2>& 1
2024-08-07 09:51:56 -04:00
exitIfCommandFailed $? "Error getting AKS credentials, exiting..."
2024-10-30 15:51:02 -04:00
kubelogin convert-kubeconfig -l azurecli
exitIfCommandFailed $? "Error logging into AKS, exiting..."
# get principal/object id of the signed in user
2025-04-02 17:15:26 -04:00
principalId = $( az ad signed-in-user show --output json | jq -r .id)
2024-10-30 15:51:02 -04:00
exitIfValueEmpty $principalId "Principal ID of deployer not found"
# assign "Azure Kubernetes Service RBAC Admin" role to deployer
2025-04-02 17:15:26 -04:00
scope = $( az aks show --resource-group $rg --name $aks_name --query "id" -o tsv)
2024-10-30 15:51:02 -04:00
exitIfValueEmpty " $scope " "Unable to get AKS scope, exiting..."
az role assignment create --role "Azure Kubernetes Service RBAC Cluster Admin" --assignee-object-id $principalId --scope $scope
exitIfCommandFailed $? "Error assigning 'Azure Kubernetes Service RBAC Cluster Admin' role to deployer, exiting..."
2025-04-02 17:15:26 -04:00
kubectl config set-context $aks_name --namespace= $aksNamespace
2024-06-26 15:45:06 -04:00
printf "Done\n"
}
2024-08-09 22:22:49 -04:00
checkForApimSoftDelete ( ) {
2025-04-02 17:15:26 -04:00
local apimName
local location
local deleted_service_list_results
2024-08-09 22:22:49 -04:00
printf "Checking if APIM was soft-deleted... "
# This is an optional step to check if an APIM instance previously existed in the
# resource group and is in a soft-deleted state. If so, purge it before deploying
# a new APIM instance to prevent conflicts with the new deployment.
2025-04-02 17:15:26 -04:00
deleted_service_list_results = $( az apim deletedservice list -o json --query " [?contains(serviceId, 'resourceGroups/ $RESOURCE_GROUP /')].{name:name, location:location} " )
2024-08-09 22:22:49 -04:00
exitIfCommandFailed $? "Error checking for soft-deleted APIM instances, exiting..."
2025-04-02 17:15:26 -04:00
apimName = $( jq -r .[ 0] .name <<< $deleted_service_list_results )
location = $( jq -r .[ 0] .location <<< $deleted_service_list_results )
2024-08-09 22:22:49 -04:00
# jq returns "null" if a value is not found
if [ -z " $apimName " ] || [ [ " $apimName " = = "null" ] ] || [ -z " $location " ] || [ [ " $location " = = "null" ] ] ; then
printf "Done.\n"
return 0
fi
if [ ! -z " $apimName " ] && [ ! -z " $location " ] ; then
printf "\nAPIM instance found in soft-deleted state. Purging...\n"
az apim deletedservice purge -n $apimName --location " $location " > /dev/null
fi
printf "Done.\n"
}
2024-06-26 15:45:06 -04:00
deployAzureResources ( ) {
2025-04-02 17:15:26 -04:00
local deployAoai
local existingAoaiId = ""
local deployAcr
local graphragImageName
local graphragImageVersion
2024-06-26 15:45:06 -04:00
echo "Deploying Azure resources..."
2025-04-02 17:15:26 -04:00
# deploy AOAI if the user did not provide links to an existing AOAI service
deployAoai = "true"
if [ -n " $GRAPHRAG_API_BASE " ] ; then
deployAoai = "false"
existingAoaiId = $( az cognitiveservices account list --query " [?contains(properties.endpoint, ' $GRAPHRAG_API_BASE ')].id " -o tsv)
exitIfValueEmpty " $existingAoaiId " "Unable to get AOAI resource id from GRAPHRAG_API_BASE, exiting..."
fi
deployAcr = "true"
if [ -n " $CONTAINER_REGISTRY_LOGIN_SERVER " ] ; then
deployAcr = "false"
fi
graphragImageName = $( sed -rn "s/([^:]+).*/\1/p" <<< " $GRAPHRAG_IMAGE " )
graphragImageVersion = $( sed -rn "s/[^:]+:(.*)/\1/p" <<< " $GRAPHRAG_IMAGE " )
exitIfValueEmpty " $graphragImageName " "Unable to parse graphrag docker image name, exiting..."
exitIfValueEmpty " $graphragImageVersion " "Unable to parse graphrag docker image version, exiting..."
local datetime deployName AZURE_DEPLOY_RESULTS
datetime = "`date +%Y-%m-%d_%H-%M-%S`"
deployName = " graphrag-deploy- $datetime "
2024-08-07 09:51:56 -04:00
echo " Deployment name: $deployName "
2025-04-02 17:15:26 -04:00
AZURE_DEPLOY_RESULTS = $( az deployment group create --name " $deployName " \
2024-08-07 09:51:56 -04:00
--no-prompt \
--resource-group $RESOURCE_GROUP \
--template-file ./main.bicep \
2024-06-26 15:45:06 -04:00
--parameters " resourceBaseName= $RESOURCE_BASE_NAME " \
--parameters " apimName= $APIM_NAME " \
2024-08-07 09:51:56 -04:00
--parameters " apimTier= $APIM_TIER " \
2025-01-30 13:59:51 -05:00
--parameters " apiPublisherEmail= $PUBLISHER_EMAIL " \
2025-04-02 17:15:26 -04:00
--parameters " apiPublisherName= $PUBLISHER_NAME " \
2024-08-07 09:51:56 -04:00
--parameters " enablePrivateEndpoints= $ENABLE_PRIVATE_ENDPOINTS " \
2025-04-02 17:15:26 -04:00
--parameters " deployAcr= $deployAcr " \
--parameters " existingAcrLoginServer= $CONTAINER_REGISTRY_LOGIN_SERVER " \
--parameters " graphragImageName= $graphragImageName " \
--parameters " graphragImageVersion= $graphragImageVersion " \
--parameters " deployAoai= $deployAoai " \
--parameters " existingAoaiId= $existingAoaiId " \
--parameters " llmModelName= $GRAPHRAG_LLM_MODEL " \
--parameters " llmModelDeploymentName= $GRAPHRAG_LLM_DEPLOYMENT_NAME " \
--parameters " llmModelVersion= $GRAPHRAG_LLM_MODEL_VERSION " \
--parameters " llmModelQuota= $GRAPHRAG_LLM_MODEL_QUOTA " \
--parameters " embeddingModelName= $GRAPHRAG_EMBEDDING_MODEL " \
--parameters " embeddingModelDeploymentName= $GRAPHRAG_EMBEDDING_DEPLOYMENT_NAME " \
--parameters " embeddingModelVersion= $GRAPHRAG_EMBEDDING_MODEL_VERSION " \
--parameters " embeddingModelQuota= $GRAPHRAG_EMBEDDING_MODEL_QUOTA " \
2024-08-07 09:51:56 -04:00
--output json)
2024-10-26 01:14:53 -04:00
# errors in deployment may not be caught by exitIfCommandFailed function so we also check the output for errors
2024-06-26 15:45:06 -04:00
exitIfCommandFailed $? "Error deploying Azure resources..."
2024-10-26 01:14:53 -04:00
exitIfValueEmpty " $AZURE_DEPLOY_RESULTS " "Error deploying Azure resources..."
2025-04-02 17:15:26 -04:00
AZURE_DEPLOY_OUTPUTS = $( jq -r .properties.outputs <<< $AZURE_DEPLOY_RESULTS )
2024-10-26 01:14:53 -04:00
exitIfCommandFailed $? "Error parsing outputs from Azure deployment..."
2025-04-02 17:15:26 -04:00
exitIfValueEmpty " $AZURE_DEPLOY_OUTPUTS " "Error parsing outputs from Azure deployment..."
# Must assign ACRPull role to aks if ACR was not part of the deployment (i.e. user chose to utilize an ACR resource external to this deployment)
if [ -n " $CONTAINER_REGISTRY_LOGIN_SERVER " ] ; then
assignACRPullRoleToAKS $RESOURCE_GROUP $CONTAINER_REGISTRY_LOGIN_SERVER
fi
}
assignACRPullRoleToAKS( ) {
local rg = $1
local registry = $2
local aks_name kubelet_id acr_id
echo "Assigning 'ACRPull' role to AKS..."
aks_name = $( jq -r .azure_aks_name.value <<< $AZURE_DEPLOY_OUTPUTS )
exitIfValueEmpty " $aks_name " "Unable to parse aks name from azure outputs, exiting..."
kubelet_id = $( az aks show --resource-group $rg --name $aks_name --query identityProfile.kubeletidentity.objectId --output tsv)
exitIfValueEmpty " $kubelet_id " "Unable to retrieve AKS kubelet id, exiting..."
acr_id = $( az acr show --name $registry --query id -o tsv)
exitIfValueEmpty " $acr_id " "Unable to retrieve ACR id, exiting..."
az role assignment create --role "AcrPull" --assignee $kubelet_id --scope $acr_id
exitIfCommandFailed $? "Error assigning ACRPull role to AKS, exiting..."
2024-06-26 15:45:06 -04:00
}
2024-10-02 13:37:29 -04:00
validateSKUs( ) {
# Run SKU validation functions unless skip flag is set
2024-10-26 01:14:53 -04:00
local location = $1
local validate_skus = $2
if [ $validate_skus = true ] ; then
checkSKUAvailability $location
checkSKUQuotas $location
2024-10-02 13:37:29 -04:00
fi
}
checkSKUAvailability( ) {
# Function to validate that the required SKUs are not restricted for the given region
local location = $1
2025-04-02 17:15:26 -04:00
local sku_checklist
local sku_check_result
local sku_validation_listing
sku_checklist = ( "standard_d4s_v5" "standard_d8s_v5" "standard_e8s_v5" )
printf "Checking cloud region for VM sku availability... "
2024-10-02 13:37:29 -04:00
for sku in ${ sku_checklist [@] } ; do
2025-04-02 17:15:26 -04:00
sku_check_result = $(
2024-10-02 13:37:29 -04:00
az vm list-skus --location $location --size $sku --output json
)
2025-04-02 17:15:26 -04:00
sku_validation_listing = $( jq -r .[ 0] .name <<< $sku_check_result )
2024-10-02 13:37:29 -04:00
exitIfValueEmpty $sku_validation_listing " SKU $sku is restricted for location $location under the current subscription. "
done
printf "Done.\n"
}
checkSKUQuotas( ) {
2025-04-02 17:15:26 -04:00
local location = $1
local vm_usage_report
2024-10-02 13:37:29 -04:00
# Function to validation that the SKU quotas would not be exceeded during deployment
printf "Checking Location for SKU Quota Usage... "
2025-04-02 17:15:26 -04:00
vm_usage_report = $(
2024-10-25 23:49:49 -04:00
az vm list-usage --location $location -o json
2024-10-02 13:37:29 -04:00
)
# Check quota for Standard DSv5 Family vCPUs
2025-04-02 17:15:26 -04:00
local dsv5_usage_report dsv5_limit dsv5_currVal dsv5_reqVal
dsv5_usage_report = $( jq -c '.[] | select(.localName | contains("Standard DSv5 Family vCPUs"))' <<< $vm_usage_report )
dsv5_limit = $( jq -r .limit <<< $dsv5_usage_report )
dsv5_currVal = $( jq -r .currentValue <<< $dsv5_usage_report )
dsv5_reqVal = $( expr $dsv5_currVal + 12)
2024-10-30 15:51:02 -04:00
exitIfThresholdExceeded $dsv5_reqVal $dsv5_limit "Not enough Standard DSv5 Family vCPU quota for deployment. At least 12 vCPU is required."
2024-10-25 23:49:49 -04:00
2024-10-02 13:37:29 -04:00
# Check quota for Standard ESv5 Family vCPUs
2025-04-02 17:15:26 -04:00
local esv5_usage_report esv5_limit esv5_currVal esv5_reqVal
esv5_usage_report = $( jq -c '.[] | select(.localName | contains("Standard ESv5 Family vCPUs"))' <<< $vm_usage_report )
esv5_limit = $( jq -r .limit <<< $esv5_usage_report )
esv5_currVal = $( jq -r .currentValue <<< $esv5_usage_report )
esv5_reqVal = $( expr $esv5_currVal + 8)
2024-10-30 15:51:02 -04:00
exitIfThresholdExceeded $esv5_reqVal $esv5_limit "Not enough Standard ESv5 Family vCPU quota for deployment. At least 8 vCPU is required."
2024-10-02 13:37:29 -04:00
printf "Done.\n"
}
2024-06-26 15:45:06 -04:00
installGraphRAGHelmChart ( ) {
2025-04-02 17:15:26 -04:00
local containerRegistryServer = ""
local graphragImageName graphragImageVersion
local workloadId serviceAccountName appInsightsConnectionString aiSearchName cosmosEndpoint appHostname storageAccountBlobUrl
local graphragApiBase graphragApiVersion graphragLlmModel graphragLlmModelDeployment graphragEmbeddingModel graphragEmbeddingModelDeployment
2024-08-07 09:51:56 -04:00
echo "Deploying graphrag helm chart... "
2025-04-02 17:15:26 -04:00
workloadId = $( jq -r .azure_workload_identity_client_id.value <<< $AZURE_DEPLOY_OUTPUTS )
2024-06-26 15:45:06 -04:00
exitIfValueEmpty " $workloadId " "Unable to parse workload id from Azure outputs, exiting..."
2025-04-02 17:15:26 -04:00
serviceAccountName = $( jq -r .azure_aks_service_account_name.value <<< $AZURE_DEPLOY_OUTPUTS )
2024-06-26 15:45:06 -04:00
exitIfValueEmpty " $serviceAccountName " "Unable to parse service account name from Azure outputs, exiting..."
2025-04-02 17:15:26 -04:00
appInsightsConnectionString = $( jq -r .azure_app_insights_connection_string.value <<< $AZURE_DEPLOY_OUTPUTS )
2024-07-15 16:42:22 -07:00
exitIfValueEmpty " $appInsightsConnectionString " "Unable to parse app insights connection string from Azure outputs, exiting..."
2024-06-26 15:45:06 -04:00
2025-04-02 17:15:26 -04:00
aiSearchName = $( jq -r .azure_ai_search_name.value <<< $AZURE_DEPLOY_OUTPUTS )
2024-06-26 15:45:06 -04:00
exitIfValueEmpty " $aiSearchName " "Unable to parse AI search name from Azure outputs, exiting..."
2025-04-02 17:15:26 -04:00
cosmosEndpoint = $( jq -r .azure_cosmosdb_endpoint.value <<< $AZURE_DEPLOY_OUTPUTS )
2024-06-26 15:45:06 -04:00
exitIfValueEmpty " $cosmosEndpoint " "Unable to parse CosmosDB endpoint from Azure outputs, exiting..."
2025-04-02 17:15:26 -04:00
appHostname = $( jq -r .azure_app_hostname.value <<< $AZURE_DEPLOY_OUTPUTS )
exitIfValueEmpty " $appHostname " "Unable to parse graphrag hostname from deployment outputs, exiting..."
2024-06-26 15:45:06 -04:00
2025-04-02 17:15:26 -04:00
storageAccountBlobUrl = $( jq -r .azure_storage_account_blob_url.value <<< $AZURE_DEPLOY_OUTPUTS )
2024-06-26 15:45:06 -04:00
exitIfValueEmpty " $storageAccountBlobUrl " "Unable to parse storage account blob url from deployment outputs, exiting..."
2024-08-07 09:51:56 -04:00
2025-04-02 17:15:26 -04:00
# retrieve container registry info either from the deployment or from user provided input
if [ -n " $CONTAINER_REGISTRY_LOGIN_SERVER " ] ; then
containerRegistryServer = " $CONTAINER_REGISTRY_LOGIN_SERVER "
else
containerRegistryServer = $( jq -r .azure_acr_login_server.value <<< $AZURE_DEPLOY_OUTPUTS )
fi
exitIfValueEmpty " $containerRegistryServer " "Unable to parse container registry url from deployment outputs, exiting..."
graphragImageName = $( sed -rn "s/([^:]+).*/\1/p" <<< " $GRAPHRAG_IMAGE " )
graphragImageVersion = $( sed -rn "s/[^:]+:(.*)/\1/p" <<< " $GRAPHRAG_IMAGE " )
exitIfValueEmpty " $graphragImageName " "Unable to parse graphrag docker image name, exiting..."
exitIfValueEmpty " $graphragImageVersion " "Unable to parse graphrag docker image version, exiting..."
# retrieve AOAOI values either from the deployment or from user provided input
if [ -n " $GRAPHRAG_API_BASE " ] ; then
graphragApiBase = " $GRAPHRAG_API_BASE "
graphragApiVersion = " $GRAPHRAG_API_VERSION "
graphragLlmModel = " $GRAPHRAG_LLM_MODEL "
graphragLlmModelDeployment = " $GRAPHRAG_LLM_DEPLOYMENT_NAME "
graphragEmbeddingModel = " $GRAPHRAG_EMBEDDING_MODEL "
graphragEmbeddingModelDeployment = " $GRAPHRAG_EMBEDDING_DEPLOYMENT_NAME "
else
graphragApiBase = $( jq -r .azure_aoai_endpoint.value <<< $AZURE_DEPLOY_OUTPUTS )
exitIfValueEmpty " $graphragApiBase " "Unable to parse AOAI endpoint from deployment outputs, exiting..."
graphragApiVersion = $( jq -r .azure_aoai_llm_model_api_version.value <<< $AZURE_DEPLOY_OUTPUTS )
exitIfValueEmpty " $graphragApiVersion " "Unable to parse AOAI model api version from deployment outputs, exiting..."
graphragLlmModel = $( jq -r .azure_aoai_llm_model.value <<< $AZURE_DEPLOY_OUTPUTS )
exitIfValueEmpty " $graphragLlmModel " "Unable to parse LLM model name from deployment outputs, exiting..."
graphragLlmModelDeployment = $( jq -r .azure_aoai_llm_model_deployment_name.value <<< $AZURE_DEPLOY_OUTPUTS )
exitIfValueEmpty " $graphragLlmModelDeployment " "Unable to parse LLM model deployment name from deployment outputs, exiting..."
graphragEmbeddingModel = $( jq -r .azure_aoai_embedding_model.value <<< $AZURE_DEPLOY_OUTPUTS )
exitIfValueEmpty " $graphragEmbeddingModel " "Unable to parse embedding model name from deployment outputs, exiting..."
graphragEmbeddingModelDeployment = $( jq -r .azure_aoai_embedding_model_deployment_name.value <<< $AZURE_DEPLOY_OUTPUTS )
exitIfValueEmpty " $graphragEmbeddingModelDeployment " "Unable to parse embedding model deployment name from deployment outputs, exiting..."
fi
2024-06-26 15:45:06 -04:00
2025-04-04 15:52:21 +00:00
graphragLlmModelConcurrentRequest = " $GRAPHRAG_LLM_MODEL_CONCURRENT_REQUEST "
graphragEmbeddingModelConcurrentRequest = " $GRAPHRAG_EMBEDDING_MODEL_CONCURRENT_REQUEST "
2024-06-26 15:45:06 -04:00
reset_x = true
if ! [ -o xtrace ] ; then
set -x
else
reset_x = false
fi
2024-08-07 09:51:56 -04:00
helm upgrade -i graphrag ./helm/graphrag -f ./helm/graphrag/values.yaml \
--namespace $aksNamespace --create-namespace \
2024-06-26 15:45:06 -04:00
--set " serviceAccount.name= $serviceAccountName " \
--set " serviceAccount.annotations.azure\.workload\.identity/client-id= $workloadId " \
2025-04-02 17:15:26 -04:00
--set " master.image.repository= $containerRegistryServer / $graphragImageName " \
2024-08-07 09:51:56 -04:00
--set " master.image.tag= $graphragImageVersion " \
2025-04-02 17:15:26 -04:00
--set " ingress.host= $appHostname " \
2024-06-26 15:45:06 -04:00
--set " graphragConfig.AI_SEARCH_URL=https:// $aiSearchName . $AISEARCH_ENDPOINT_SUFFIX " \
2025-04-02 17:15:26 -04:00
--set " graphragConfig.AI_SEARCH_AUDIENCE= $AI_SEARCH_AUDIENCE " \
--set " graphragConfig.APPLICATIONINSIGHTS_CONNECTION_STRING= $appInsightsConnectionString " \
2025-01-30 13:59:51 -05:00
--set " graphragConfig.COGNITIVE_SERVICES_AUDIENCE= $COGNITIVE_SERVICES_AUDIENCE " \
2025-04-02 17:15:26 -04:00
--set " graphragConfig.COSMOS_URI_ENDPOINT= $cosmosEndpoint " \
--set " graphragConfig.GRAPHRAG_API_BASE= $graphragApiBase " \
--set " graphragConfig.GRAPHRAG_API_VERSION= $graphragApiVersion " \
--set " graphragConfig.GRAPHRAG_LLM_MODEL= $graphragLlmModel " \
--set " graphragConfig.GRAPHRAG_LLM_DEPLOYMENT_NAME= $graphragLlmModelDeployment " \
--set " graphragConfig.GRAPHRAG_EMBEDDING_MODEL= $graphragEmbeddingModel " \
--set " graphragConfig.GRAPHRAG_EMBEDDING_DEPLOYMENT_NAME= $graphragEmbeddingModelDeployment " \
2025-04-04 15:52:21 +00:00
--set " graphragConfig.STORAGE_ACCOUNT_BLOB_URL= $storageAccountBlobUrl " \
--set " graphragConfig.GRAPHRAG_LLM_MODEL_CONCURRENT_REQUEST= $GRAPHRAG_LLM_MODEL_CONCURRENT_REQUEST " \
--set " graphragConfig.GRAPHRAG_EMBEDDING_MODEL_CONCURRENT_REQUEST= $GRAPHRAG_EMBEDDING_MODEL_CONCURRENT_REQUEST "
2024-06-26 15:45:06 -04:00
2025-04-02 17:15:26 -04:00
local helmResult
helmResult = $?
2024-06-26 15:45:06 -04:00
" $reset_x " && set +x
exitIfCommandFailed $helmResult "Error deploying helm chart, exiting..."
}
2024-08-07 09:51:56 -04:00
waitForExternalIp ( ) {
2024-06-26 15:45:06 -04:00
local -i maxTries = 14
local available = "false"
2025-04-02 17:15:26 -04:00
local TMP_GRAPHRAG_SERVICE_IP
2024-06-26 15:45:06 -04:00
printf "Checking for GraphRAG external IP"
for ( ( i = 0; i < $maxTries ; i++) ) ; do
2024-08-07 09:51:56 -04:00
TMP_GRAPHRAG_SERVICE_IP = $( kubectl get ingress --namespace graphrag graphrag -o json | jq -r .status.loadBalancer.ingress[ 0] .ip)
# jq returns "null" if a value is not found
if [ [ " $TMP_GRAPHRAG_SERVICE_IP " != "null" ] ] ; then
2024-06-26 15:45:06 -04:00
available = "true"
GRAPHRAG_SERVICE_IP = $TMP_GRAPHRAG_SERVICE_IP
break
fi
sleep 10
printf "."
done
if [ $available = = "true" ] ; then
printf " Available.\n"
else
printf " Failed.\n"
fi
}
2024-08-07 09:51:56 -04:00
waitForGraphragBackend ( ) {
2024-06-26 15:45:06 -04:00
local backendSwaggerUrl = $1
local -i maxTries = 20
local available = "false"
2025-04-02 17:15:26 -04:00
2024-08-07 09:51:56 -04:00
printf "Checking for GraphRAG API availability..."
2024-06-26 15:45:06 -04:00
for ( ( i = 0; i < $maxTries ; i++) ) ; do
az rest --method get --url $backendSwaggerUrl > /dev/null 2>& 1
if [ $? -eq 0 ] ; then
available = "true"
break
fi
sleep 20
printf "."
done
if [ $available = = "true" ] ; then
printf " Available.\n"
else
printf " Failed.\n"
2024-08-07 09:51:56 -04:00
exitIfValueEmpty "" "GraphRAG API unavailable, exiting..."
2024-06-26 15:45:06 -04:00
fi
}
2024-08-07 09:51:56 -04:00
deployDnsRecord ( ) {
waitForExternalIp
2024-06-26 15:45:06 -04:00
exitIfValueEmpty " $GRAPHRAG_SERVICE_IP " "Unable to get GraphRAG external IP."
2025-04-02 17:15:26 -04:00
local dnsZoneName
dnsZoneName = $( jq -r .azure_dns_zone_name.value <<< $AZURE_DEPLOY_OUTPUTS )
2024-06-26 15:45:06 -04:00
exitIfValueEmpty " $dnsZoneName " "Error parsing DNS zone name from azure outputs, exiting..."
2024-08-07 09:51:56 -04:00
az deployment group create --only-show-errors --no-prompt \
2025-04-02 17:15:26 -04:00
--name graphrag-dns-deployment \
2024-08-07 09:51:56 -04:00
--resource-group $RESOURCE_GROUP \
--template-file core/vnet/private-dns-zone-a-record.bicep \
2024-06-26 15:45:06 -04:00
--parameters "name=graphrag" \
--parameters " dnsZoneName= $dnsZoneName " \
2024-08-07 09:51:56 -04:00
--parameters " ipv4Address= $GRAPHRAG_SERVICE_IP " > /dev/null
2024-06-26 15:45:06 -04:00
exitIfCommandFailed $? "Error creating GraphRAG DNS record, exiting..."
}
deployGraphragAPI ( ) {
2025-04-02 17:15:26 -04:00
local apimGatewayUrl apimName backendSwaggerUrl graphragUrl
2024-06-26 15:45:06 -04:00
echo "Registering GraphRAG API with APIM..."
2025-04-02 17:15:26 -04:00
apimGatewayUrl = $( jq -r .azure_apim_gateway_url.value <<< $AZURE_DEPLOY_OUTPUTS )
2024-06-26 15:45:06 -04:00
exitIfValueEmpty " $apimGatewayUrl " "Unable to parse APIM gateway url from Azure outputs, exiting..."
2025-04-02 17:15:26 -04:00
apimName = $( jq -r .azure_apim_name.value <<< $AZURE_DEPLOY_OUTPUTS )
2024-06-26 15:45:06 -04:00
exitIfValueEmpty " $apimName " "Error parsing apim name from azure outputs, exiting..."
2025-04-02 17:15:26 -04:00
backendSwaggerUrl = " $apimGatewayUrl /manpage/openapi.json "
graphragUrl = $( jq -r .azure_app_url.value <<< $AZURE_DEPLOY_OUTPUTS )
2024-06-26 15:45:06 -04:00
exitIfValueEmpty " $graphragUrl " "Error parsing GraphRAG URL from azure outputs, exiting..."
2024-08-07 09:51:56 -04:00
waitForGraphragBackend $backendSwaggerUrl
2024-06-26 15:45:06 -04:00
2024-08-07 09:51:56 -04:00
# download the openapi spec from the backend and load it into APIM
2025-04-02 17:15:26 -04:00
az rest --only-show-errors --method get --url $backendSwaggerUrl -o json > core/apim/openapi.json
2024-08-07 09:51:56 -04:00
exitIfCommandFailed $? "Error downloading graphrag openapi spec, exiting..."
az deployment group create --only-show-errors --no-prompt \
2025-04-02 17:15:26 -04:00
--name upload-graphrag-api-to-apim \
2024-08-07 09:51:56 -04:00
--resource-group $RESOURCE_GROUP \
2025-04-02 17:15:26 -04:00
--template-file core/apim/apim.graphrag-api.bicep \
2024-06-26 15:45:06 -04:00
--parameters " backendUrl= $graphragUrl " \
--parameters "name=GraphRAG" \
2025-04-02 17:15:26 -04:00
--parameters " apiManagementName= $apimName " > /dev/null
2024-06-26 15:45:06 -04:00
exitIfCommandFailed $? "Error registering graphrag API, exiting..."
# cleanup
2025-04-02 17:15:26 -04:00
#rm core/apim/openapi.json
2024-06-26 15:45:06 -04:00
}
grantDevAccessToAzureResources( ) {
2024-08-07 09:51:56 -04:00
# This function is used to grant the deployer of this script "developer" access
# to GraphRAG Azure resources by assigning the necessary RBAC roles for
2025-04-02 17:15:26 -04:00
# Azure Storage, AI Search, and CosmosDB to the signed-in user. This grants
2024-08-07 09:51:56 -04:00
# the deployer access to data in the storage account, cosmos db, and AI search services
# from the Azure portal.
2024-06-26 15:45:06 -04:00
echo "Granting deployer developer access to Azure resources..."
# get subscription id of the active subscription
2025-04-02 17:15:26 -04:00
local subscriptionId
subscriptionId = $( az account show --output json | jq -r .id)
2024-06-26 15:45:06 -04:00
exitIfValueEmpty $subscriptionId "Subscription ID not found"
# get principal/object id of the signed in user
2025-04-02 17:15:26 -04:00
local principalId
principalId = $( az ad signed-in-user show --output json | jq -r .id)
2024-10-30 15:51:02 -04:00
exitIfValueEmpty $principalId "Principal ID of deployer not found"
2024-06-26 15:45:06 -04:00
# assign storage account roles
2025-04-02 17:15:26 -04:00
local storageAccountName
storageAccountName = $( az storage account list --resource-group $RESOURCE_GROUP --output json | jq -r .[ 0] .name)
2024-06-26 15:45:06 -04:00
exitIfValueEmpty $storageAccountName "Storage account not found"
2024-08-07 09:51:56 -04:00
az role assignment create \
--role "Storage Blob Data Contributor" \
--assignee $principalId \
--scope " /subscriptions/ $subscriptionId /resourceGroups/ $RESOURCE_GROUP /providers/Microsoft.Storage/storageAccounts/ $storageAccountName " > /dev/null
2024-06-26 15:45:06 -04:00
# assign cosmos db role
2025-04-02 17:15:26 -04:00
local cosmosDbName
cosmosDbName = $( az cosmosdb list --resource-group $RESOURCE_GROUP -o json | jq -r .[ 0] .name)
2024-06-26 15:45:06 -04:00
exitIfValueEmpty $cosmosDbName "CosmosDB account not found"
2024-08-07 09:51:56 -04:00
az cosmosdb sql role assignment create \
--account-name $cosmosDbName \
--resource-group $RESOURCE_GROUP \
--scope "/" \
--principal-id $principalId \
--role-definition-id /subscriptions/$subscriptionId /resourceGroups/$RESOURCE_GROUP /providers/Microsoft.DocumentDB/databaseAccounts/graphrag/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002 > /dev/null
2024-06-26 15:45:06 -04:00
# assign AI search roles
2025-04-02 17:15:26 -04:00
local searchServiceName
searchServiceName = $( az search service list --resource-group $RESOURCE_GROUP -o json | jq -r .[ 0] .name)
2024-06-26 15:45:06 -04:00
exitIfValueEmpty $searchServiceName "AI Search service not found"
2024-08-07 09:51:56 -04:00
az role assignment create \
--role "Contributor" \
--assignee $principalId \
--scope " /subscriptions/ $subscriptionId /resourceGroups/ $RESOURCE_GROUP /providers/Microsoft.Search/searchServices/ $searchServiceName " > /dev/null
az role assignment create \
--role "Search Index Data Contributor" \
--assignee $principalId \
--scope " /subscriptions/ $subscriptionId /resourceGroups/ $RESOURCE_GROUP /providers/Microsoft.Search/searchServices/ $searchServiceName " > /dev/null
az role assignment create \
--role "Search Index Data Reader" \
--assignee $principalId \
--scope " /subscriptions/ $subscriptionId /resourceGroups/ $RESOURCE_GROUP /providers/Microsoft.Search/searchServices/ $searchServiceName " > /dev/null
2024-06-26 15:45:06 -04:00
}
2025-04-02 17:15:26 -04:00
deployDockerImageToInternalACR( ) {
local containerRegistry
containerRegistry = $( jq -r .azure_acr_login_server.value <<< $AZURE_DEPLOY_OUTPUTS )
2024-08-07 09:51:56 -04:00
exitIfValueEmpty " $containerRegistry " "Unable to parse container registry from azure deployment outputs, exiting..."
echo " Deploying docker image ' ${ GRAPHRAG_IMAGE } ' to container registry ' ${ containerRegistry } '... "
2025-04-02 17:15:26 -04:00
local scriptDir
scriptDir = " $( cd -- " $( dirname -- " ${ BASH_SOURCE [0] :- $0 } " ; ) " & > /dev/null && pwd 2> /dev/null; ) " ;
2024-08-07 09:51:56 -04:00
az acr build --only-show-errors \
--registry $containerRegistry \
--file $scriptDir /../docker/Dockerfile-backend \
--image $GRAPHRAG_IMAGE \
$scriptDir /../
2024-06-26 15:45:06 -04:00
exitIfCommandFailed $? "Error deploying docker image, exiting..."
}
################################################################################
# Help menu #
################################################################################
usage( ) {
echo
2024-10-02 13:37:29 -04:00
echo " Usage: bash $0 [-h|d|g|s] -p <deploy.parameters.json> "
2024-06-26 15:45:06 -04:00
echo "Description: Deployment script for the GraphRAG Solution Accelerator."
echo "options:"
echo " -h Print this help menu."
echo " -d Disable private endpoint usage."
2024-08-07 09:51:56 -04:00
echo " -g Developer mode. Grants deployer of this script access to Azure Storage, AI Search, and CosmosDB. Will disable private endpoints (-d) and enable debug mode."
2024-10-02 13:37:29 -04:00
echo " -s Skip validation of SKU availability and quota for a faster deployment"
2024-06-26 15:45:06 -04:00
echo " -p A JSON file containing the deployment parameters (deploy.parameters.json)."
echo
}
# print usage if no arguments are supplied
[ $# -eq 0 ] && usage && exit 0
# parse arguments
ENABLE_PRIVATE_ENDPOINTS = true
2024-10-02 13:37:29 -04:00
VALIDATE_SKUS_FLAG = true
2024-06-26 15:45:06 -04:00
GRANT_DEV_ACCESS = 0 # false
PARAMS_FILE = ""
2024-10-02 13:37:29 -04:00
while getopts ":dgsp:h" option; do
2024-06-26 15:45:06 -04:00
case " ${ option } " in
d)
ENABLE_PRIVATE_ENDPOINTS = false
; ;
g)
ENABLE_PRIVATE_ENDPOINTS = false
GRANT_DEV_ACCESS = 1 # true
; ;
2024-10-02 13:37:29 -04:00
s)
VALIDATE_SKUS_FLAG = false
; ;
2024-06-26 15:45:06 -04:00
p)
PARAMS_FILE = ${ OPTARG }
; ;
h | *)
usage
exit 0
; ;
esac
done
shift $(( OPTIND-1))
# check if required arguments are supplied
if [ ! -f $PARAMS_FILE ] ; then
echo "Error: invalid required argument."
usage
exit 1
fi
################################################################################
# Main Program #
################################################################################
2024-08-07 09:51:56 -04:00
startBanner
2024-06-26 15:45:06 -04:00
2024-08-07 09:51:56 -04:00
checkRequiredTools
2024-06-26 15:45:06 -04:00
populateParams $PARAMS_FILE
2024-10-26 01:14:53 -04:00
# Check SKU availability and quotas
validateSKUs $LOCATION $VALIDATE_SKUS_FLAG
2024-06-26 15:45:06 -04:00
# Create resource group
createResourceGroupIfNotExists $LOCATION $RESOURCE_GROUP
# Deploy Azure resources
2024-08-09 22:22:49 -04:00
checkForApimSoftDelete
2024-06-26 15:45:06 -04:00
deployAzureResources
2025-04-02 17:15:26 -04:00
# Deploy graphrag docker image to internal ACR if an external ACR was not provided
if [ -z " $CONTAINER_REGISTRY_LOGIN_SERVER " ] ; then
deployDockerImageToInternalACR
fi
2024-06-26 15:45:06 -04:00
2024-08-07 09:51:56 -04:00
# Retrieve AKS credentials and install GraphRAG helm chart
2025-04-02 17:15:26 -04:00
getAksCredentials $RESOURCE_GROUP
2024-06-26 15:45:06 -04:00
installGraphRAGHelmChart
# Import and setup GraphRAG API in APIM
2024-08-07 09:51:56 -04:00
deployDnsRecord
2024-06-26 15:45:06 -04:00
deployGraphragAPI
if [ $GRANT_DEV_ACCESS -eq 1 ] ; then
grantDevAccessToAzureResources
fi
2024-08-07 09:51:56 -04:00
successBanner