#!/usr/bin/env bash # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # set -ux # uncomment this line to debug # TODO: use https://www.shellcheck.net to lint this script and make recommended updates aksNamespace="graphrag" # Optional parameters with default values AI_SEARCH_AUDIENCE="https://search.azure.com" AISEARCH_ENDPOINT_SUFFIX="search.windows.net" APIM_NAME="" APIM_TIER="Developer" CLOUD_NAME="AzurePublicCloud" GRAPHRAG_IMAGE="graphrag:backend" PUBLISHER_EMAIL="publisher@microsoft.com" PUBLISHER_NAME="publisher" RESOURCE_BASE_NAME="" 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" requiredParams=( LOCATION 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 GRAPHRAG_API_BASE GRAPHRAG_API_VERSION GRAPHRAG_LLM_MODEL GRAPHRAG_LLM_MODEL_QUOTA GRAPHRAG_LLM_MODEL_VERSION GRAPHRAG_LLM_DEPLOYMENT_NAME GRAPHRAG_EMBEDDING_MODEL GRAPHRAG_EMBEDDING_MODEL_QUOTA GRAPHRAG_EMBEDDING_MODEL_VERSION GRAPHRAG_EMBEDDING_DEPLOYMENT_NAME ) 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" _____ __ _ / ____| / _| | | | (___ _ _ ___ ___ ___ ___ ___| |_ _ _| | \___ \| | | |/ __/ __/ _ / __/ __| _| | | | | ____) | |_| | (_| (_| __\__ \__ | | | |_| | | |_____/ \__,_|\___\___\___|___|___|_| \__,_|_| _ _ | | | | | | | | __| | ___ _ __ | | ___ _ _ _ __ ___ ___ _ __ | |_| | / _` |/ _ | '_ \| |/ _ \| | | | '_ ` _ \ / _ | '_ \| __| | | (_| | __| |_) | | (_) | |_| | | | | | | __| | | | |_|_| \__,_|\___| .__/|_|\___/ \__, |_| |_| |_|\___|_| |_|\__(_) | | __/ | |_| |___/ EOF printf "\n\n" } startBanner () { # https://patorjk.com/software/taag cat << "EOF" _____ _ _____ _____ / ____| | | | __ \ /\ / ____| | | __ _ __ __ _ _ __ | |__ | |__) | / \ | | __ | | |_ | '__/ _` | '_ \| '_ \| _ / / /\ \| | |_ | | |__| | | | (_| | |_) | | | | | \ \ / ____ | |__| | \_____|_| \__,_| .__/|_| |_|_| \_/_/_ \_\_____| /\ | | | | | | / \ ___ ___|_|_| | ___ _ __ __ _| |_ ___ _ __ / /\ \ / __/ __/ _ | |/ _ | '__/ _` | __/ _ \| '__| / ____ | (_| (_| __| | __| | | (_| | || (_) | | /_/ \_\___\___\___|_|\___|_| \__,_|\__\___/|_| EOF printf "\n\n" } exitIfCommandFailed () { local res=$1 local msg=$2 if [ 0 -ne $res ]; then errorBanner printf "$msg\n" exit 1 fi } exitIfValueEmpty () { local value=$1 local msg=$2 # check if the value is empty or "null" (jq returns "null" when a value is not found) if [ -z "$value" ] || [[ "$value" == "null" ]]; then errorBanner printf "$msg\n" exit 1 fi } 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 } 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 () { local JQ_VERSION local major minor patch local YQ_VERSION local AZ_VERSION printf "Checking for required tools... " which sed > /dev/null exitIfCommandFailed $? "sed is required, exiting..." which kubectl > /dev/null exitIfCommandFailed $? "kubectl is required, exiting..." which kubelogin > /dev/null exitIfCommandFailed $? "kubelogin is required, exiting..." 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..." # minimum version check for jq, yq, and az cli JQ_VERSION=$(jq --version | cut -d'-' -f2) IFS='.' read -r major minor patch <<< "$JQ_VERSION" if [ -z $patch ]; then # NOTE: older acceptable versions of jq report a version # number without the patch number. if patch version is # not present, set it to 0 patch=0 JQ_VERSION="$major.$minor.$patch" fi YQ_VERSION=$(yq --version | awk '{print substr($4,2)}') AZ_VERSION=$(az version -o json | jq -r '.["azure-cli"]') versionCheck "jq" $JQ_VERSION "1.6.0" versionCheck "yq" $YQ_VERSION "4.40.7" versionCheck "az cli" $AZ_VERSION "2.55.0" printf "Done.\n" } checkRequiredParams () { local paramsFile=$1 local paramValue for param in "${requiredParams[@]}"; do paramValue=$(jq -r .$param < $paramsFile) if [ -z "$paramValue" ] || [ "$paramValue" == "null" ]; then echo "Parameter $param is required, exiting..." exit 1 fi done } populateParams () { local paramsFile=$1 printf "Checking required parameters... " checkRequiredParams $paramsFile printf "Done.\n" # 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) # 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 } createResourceGroupIfNotExists () { local location=$1 local rg=$2 printf "Checking if resource group $rg exists... " az group show -n $rg -o json > /dev/null 2>&1 if [ $? -ne 0 ]; then printf "No.\n" printf "Creating resource group... " az group create -l $location -n $rg > /dev/null 2>&1 printf "Done.\n" else printf "Yes.\n" fi } getAksCredentials () { local rg=$1 local aks_name local principalId local scope printf "Getting AKS credentials... " 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 exitIfCommandFailed $? "Error getting AKS credentials, exiting..." kubelogin convert-kubeconfig -l azurecli exitIfCommandFailed $? "Error logging into AKS, exiting..." # get principal/object id of the signed in user principalId=$(az ad signed-in-user show --output json | jq -r .id) exitIfValueEmpty $principalId "Principal ID of deployer not found" # assign "Azure Kubernetes Service RBAC Admin" role to deployer scope=$(az aks show --resource-group $rg --name $aks_name --query "id" -o tsv) 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..." kubectl config set-context $aks_name --namespace=$aksNamespace printf "Done\n" } checkForApimSoftDelete () { local apimName local location local deleted_service_list_results 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. deleted_service_list_results=$(az apim deletedservice list -o json --query "[?contains(serviceId, 'resourceGroups/$RESOURCE_GROUP/')].{name:name, location:location}") exitIfCommandFailed $? "Error checking for soft-deleted APIM instances, exiting..." apimName=$(jq -r .[0].name <<< $deleted_service_list_results) location=$(jq -r .[0].location <<< $deleted_service_list_results) # 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" } deployAzureResources () { local deployAoai local existingAoaiId="" local deployAcr local graphragImageName local graphragImageVersion echo "Deploying Azure resources..." # 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" echo "Deployment name: $deployName" AZURE_DEPLOY_RESULTS=$(az deployment group create --name "$deployName" \ --no-prompt \ --resource-group $RESOURCE_GROUP \ --template-file ./main.bicep \ --parameters "resourceBaseName=$RESOURCE_BASE_NAME" \ --parameters "apimName=$APIM_NAME" \ --parameters "apimTier=$APIM_TIER" \ --parameters "apiPublisherEmail=$PUBLISHER_EMAIL" \ --parameters "apiPublisherName=$PUBLISHER_NAME" \ --parameters "enablePrivateEndpoints=$ENABLE_PRIVATE_ENDPOINTS" \ --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" \ --output json) # errors in deployment may not be caught by exitIfCommandFailed function so we also check the output for errors exitIfCommandFailed $? "Error deploying Azure resources..." exitIfValueEmpty "$AZURE_DEPLOY_RESULTS" "Error deploying Azure resources..." AZURE_DEPLOY_OUTPUTS=$(jq -r .properties.outputs <<< $AZURE_DEPLOY_RESULTS) exitIfCommandFailed $? "Error parsing outputs from Azure deployment..." 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..." } validateSKUs() { # Run SKU validation functions unless skip flag is set local location=$1 local validate_skus=$2 if [ $validate_skus = true ]; then checkSKUAvailability $location checkSKUQuotas $location fi } checkSKUAvailability() { # Function to validate that the required SKUs are not restricted for the given region local location=$1 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... " for sku in ${sku_checklist[@]}; do sku_check_result=$( az vm list-skus --location $location --size $sku --output json ) sku_validation_listing=$(jq -r .[0].name <<< $sku_check_result) exitIfValueEmpty $sku_validation_listing "SKU $sku is restricted for location $location under the current subscription." done printf "Done.\n" } checkSKUQuotas() { local location=$1 local vm_usage_report # Function to validation that the SKU quotas would not be exceeded during deployment printf "Checking Location for SKU Quota Usage... " vm_usage_report=$( az vm list-usage --location $location -o json ) # Check quota for Standard DSv5 Family vCPUs 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) exitIfThresholdExceeded $dsv5_reqVal $dsv5_limit "Not enough Standard DSv5 Family vCPU quota for deployment. At least 12 vCPU is required." # Check quota for Standard ESv5 Family vCPUs 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) exitIfThresholdExceeded $esv5_reqVal $esv5_limit "Not enough Standard ESv5 Family vCPU quota for deployment. At least 8 vCPU is required." printf "Done.\n" } installGraphRAGHelmChart () { local containerRegistryServer="" local graphragImageName graphragImageVersion local workloadId serviceAccountName appInsightsConnectionString aiSearchName cosmosEndpoint appHostname storageAccountBlobUrl local graphragApiBase graphragApiVersion graphragLlmModel graphragLlmModelDeployment graphragEmbeddingModel graphragEmbeddingModelDeployment echo "Deploying graphrag helm chart... " workloadId=$(jq -r .azure_workload_identity_client_id.value <<< $AZURE_DEPLOY_OUTPUTS) exitIfValueEmpty "$workloadId" "Unable to parse workload id from Azure outputs, exiting..." serviceAccountName=$(jq -r .azure_aks_service_account_name.value <<< $AZURE_DEPLOY_OUTPUTS) exitIfValueEmpty "$serviceAccountName" "Unable to parse service account name from Azure outputs, exiting..." appInsightsConnectionString=$(jq -r .azure_app_insights_connection_string.value <<< $AZURE_DEPLOY_OUTPUTS) exitIfValueEmpty "$appInsightsConnectionString" "Unable to parse app insights connection string from Azure outputs, exiting..." aiSearchName=$(jq -r .azure_ai_search_name.value <<< $AZURE_DEPLOY_OUTPUTS) exitIfValueEmpty "$aiSearchName" "Unable to parse AI search name from Azure outputs, exiting..." cosmosEndpoint=$(jq -r .azure_cosmosdb_endpoint.value <<< $AZURE_DEPLOY_OUTPUTS) exitIfValueEmpty "$cosmosEndpoint" "Unable to parse CosmosDB endpoint from Azure outputs, exiting..." appHostname=$(jq -r .azure_app_hostname.value <<< $AZURE_DEPLOY_OUTPUTS) exitIfValueEmpty "$appHostname" "Unable to parse graphrag hostname from deployment outputs, exiting..." storageAccountBlobUrl=$(jq -r .azure_storage_account_blob_url.value <<< $AZURE_DEPLOY_OUTPUTS) exitIfValueEmpty "$storageAccountBlobUrl" "Unable to parse storage account blob url from deployment outputs, exiting..." # 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 reset_x=true if ! [ -o xtrace ]; then set -x else reset_x=false fi helm upgrade -i graphrag ./helm/graphrag -f ./helm/graphrag/values.yaml \ --namespace $aksNamespace --create-namespace \ --set "serviceAccount.name=$serviceAccountName" \ --set "serviceAccount.annotations.azure\.workload\.identity/client-id=$workloadId" \ --set "master.image.repository=$containerRegistryServer/$graphragImageName" \ --set "master.image.tag=$graphragImageVersion" \ --set "ingress.host=$appHostname" \ --set "graphragConfig.AI_SEARCH_URL=https://$aiSearchName.$AISEARCH_ENDPOINT_SUFFIX" \ --set "graphragConfig.AI_SEARCH_AUDIENCE=$AI_SEARCH_AUDIENCE" \ --set "graphragConfig.APPLICATIONINSIGHTS_CONNECTION_STRING=$appInsightsConnectionString" \ --set "graphragConfig.COGNITIVE_SERVICES_AUDIENCE=$COGNITIVE_SERVICES_AUDIENCE" \ --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" \ --set "graphragConfig.STORAGE_ACCOUNT_BLOB_URL=$storageAccountBlobUrl" local helmResult helmResult=$? "$reset_x" && set +x exitIfCommandFailed $helmResult "Error deploying helm chart, exiting..." } waitForExternalIp () { local -i maxTries=14 local available="false" local TMP_GRAPHRAG_SERVICE_IP printf "Checking for GraphRAG external IP" for ((i=0;i < $maxTries; i++)); do 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 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 } waitForGraphragBackend () { local backendSwaggerUrl=$1 local -i maxTries=20 local available="false" printf "Checking for GraphRAG API availability..." 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" exitIfValueEmpty "" "GraphRAG API unavailable, exiting..." fi } deployDnsRecord () { waitForExternalIp exitIfValueEmpty "$GRAPHRAG_SERVICE_IP" "Unable to get GraphRAG external IP." local dnsZoneName dnsZoneName=$(jq -r .azure_dns_zone_name.value <<< $AZURE_DEPLOY_OUTPUTS) exitIfValueEmpty "$dnsZoneName" "Error parsing DNS zone name from azure outputs, exiting..." az deployment group create --only-show-errors --no-prompt \ --name graphrag-dns-deployment \ --resource-group $RESOURCE_GROUP \ --template-file core/vnet/private-dns-zone-a-record.bicep \ --parameters "name=graphrag" \ --parameters "dnsZoneName=$dnsZoneName" \ --parameters "ipv4Address=$GRAPHRAG_SERVICE_IP" > /dev/null exitIfCommandFailed $? "Error creating GraphRAG DNS record, exiting..." } deployGraphragAPI () { local apimGatewayUrl apimName backendSwaggerUrl graphragUrl echo "Registering GraphRAG API with APIM..." apimGatewayUrl=$(jq -r .azure_apim_gateway_url.value <<< $AZURE_DEPLOY_OUTPUTS) exitIfValueEmpty "$apimGatewayUrl" "Unable to parse APIM gateway url from Azure outputs, exiting..." apimName=$(jq -r .azure_apim_name.value <<< $AZURE_DEPLOY_OUTPUTS) exitIfValueEmpty "$apimName" "Error parsing apim name from azure outputs, exiting..." backendSwaggerUrl="$apimGatewayUrl/manpage/openapi.json" graphragUrl=$(jq -r .azure_app_url.value <<< $AZURE_DEPLOY_OUTPUTS) exitIfValueEmpty "$graphragUrl" "Error parsing GraphRAG URL from azure outputs, exiting..." waitForGraphragBackend $backendSwaggerUrl # download the openapi spec from the backend and load it into APIM az rest --only-show-errors --method get --url $backendSwaggerUrl -o json > core/apim/openapi.json exitIfCommandFailed $? "Error downloading graphrag openapi spec, exiting..." az deployment group create --only-show-errors --no-prompt \ --name upload-graphrag-api-to-apim \ --resource-group $RESOURCE_GROUP \ --template-file core/apim/apim.graphrag-api.bicep \ --parameters "backendUrl=$graphragUrl" \ --parameters "name=GraphRAG" \ --parameters "apiManagementName=$apimName" > /dev/null exitIfCommandFailed $? "Error registering graphrag API, exiting..." # cleanup #rm core/apim/openapi.json } grantDevAccessToAzureResources() { # This function is used to grant the deployer of this script "developer" access # to GraphRAG Azure resources by assigning the necessary RBAC roles for # Azure Storage, AI Search, and CosmosDB to the signed-in user. This grants # the deployer access to data in the storage account, cosmos db, and AI search services # from the Azure portal. echo "Granting deployer developer access to Azure resources..." # get subscription id of the active subscription local subscriptionId subscriptionId=$(az account show --output json | jq -r .id) exitIfValueEmpty $subscriptionId "Subscription ID not found" # get principal/object id of the signed in user local principalId principalId=$(az ad signed-in-user show --output json | jq -r .id) exitIfValueEmpty $principalId "Principal ID of deployer not found" # assign storage account roles local storageAccountName storageAccountName=$(az storage account list --resource-group $RESOURCE_GROUP --output json | jq -r .[0].name) exitIfValueEmpty $storageAccountName "Storage account not found" az role assignment create \ --role "Storage Blob Data Contributor" \ --assignee $principalId \ --scope "/subscriptions/$subscriptionId/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Storage/storageAccounts/$storageAccountName" > /dev/null # assign cosmos db role local cosmosDbName cosmosDbName=$(az cosmosdb list --resource-group $RESOURCE_GROUP -o json | jq -r .[0].name) exitIfValueEmpty $cosmosDbName "CosmosDB account not found" 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 # assign AI search roles local searchServiceName searchServiceName=$(az search service list --resource-group $RESOURCE_GROUP -o json | jq -r .[0].name) exitIfValueEmpty $searchServiceName "AI Search service not found" 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 } deployDockerImageToInternalACR() { local containerRegistry containerRegistry=$(jq -r .azure_acr_login_server.value <<< $AZURE_DEPLOY_OUTPUTS) exitIfValueEmpty "$containerRegistry" "Unable to parse container registry from azure deployment outputs, exiting..." echo "Deploying docker image '${GRAPHRAG_IMAGE}' to container registry '${containerRegistry}'..." local scriptDir scriptDir="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )"; az acr build --only-show-errors \ --registry $containerRegistry \ --file $scriptDir/../docker/Dockerfile-backend \ --image $GRAPHRAG_IMAGE \ $scriptDir/../ exitIfCommandFailed $? "Error deploying docker image, exiting..." } ################################################################################ # Help menu # ################################################################################ usage() { echo echo "Usage: bash $0 [-h|d|g|s] -p " echo "Description: Deployment script for the GraphRAG Solution Accelerator." echo "options:" echo " -h Print this help menu." echo " -d Disable private endpoint usage." 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." echo " -s Skip validation of SKU availability and quota for a faster deployment" 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 VALIDATE_SKUS_FLAG=true GRANT_DEV_ACCESS=0 # false PARAMS_FILE="" while getopts ":dgsp:h" option; do case "${option}" in d) ENABLE_PRIVATE_ENDPOINTS=false ;; g) ENABLE_PRIVATE_ENDPOINTS=false GRANT_DEV_ACCESS=1 # true ;; s) VALIDATE_SKUS_FLAG=false ;; 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 # ################################################################################ startBanner checkRequiredTools populateParams $PARAMS_FILE # Check SKU availability and quotas validateSKUs $LOCATION $VALIDATE_SKUS_FLAG # Create resource group createResourceGroupIfNotExists $LOCATION $RESOURCE_GROUP # Deploy Azure resources checkForApimSoftDelete deployAzureResources # Deploy graphrag docker image to internal ACR if an external ACR was not provided if [ -z "$CONTAINER_REGISTRY_LOGIN_SERVER" ]; then deployDockerImageToInternalACR fi # Retrieve AKS credentials and install GraphRAG helm chart getAksCredentials $RESOURCE_GROUP installGraphRAGHelmChart # Import and setup GraphRAG API in APIM deployDnsRecord deployGraphragAPI if [ $GRANT_DEV_ACCESS -eq 1 ]; then grantDevAccessToAzureResources fi successBanner