AI applications perform tasks such assummarizing articles, writing stories, and engaging in long conversations with chatbots. This is made possible bylarge language models (LLMs)like OpenAI ChatGPT, which are deep learning algorithms capable of recognizing, summarizing, translating, predicting, and generating text and other content. LLMs leverage the knowledge acquired from extensive datasets, enabling them to perform tasks beyond teaching AI human languages. These models have succeeded in diverse domains, including understanding proteins, writing software code, and more. Apart from their applications in natural language processing, such as translation, chatbots, and AI assistants, large language models are also extensively employed in healthcare, software development, and various other fields.
For more information on Azure OpenAI Service and Large Language Models (LLMs), see the following articles:
API Server VNET Integrationallows you to enable network communication between the API server and the cluster nodes without requiring a private link or tunnel. AKS clusters with API Server VNET integration provide a series of advantages, for example, they can have public network access or private cluster mode enabled or disabled without redeploying the cluster. For more information, seeCreate an Azure Kubernetes Service cluster with API Server VNet Integration.
Azure NAT Gatewayto manage outbound connections initiated by AKS-hosted workloads.
Event-driven Autoscaling (KEDA) add-onis a single-purpose and lightweight component that strives to make application autoscaling simple and is a CNCF Incubation project.
Dapr extension for Azure Kubernetes Service (AKS)allows you to installDapr, a portable, event-driven runtime that simplifies building resilient, stateless, and stateful applications that run on the cloud and edge and embrace the diversity of languages and developer frameworks. With its sidecar architecture, Dapr helps you tackle the challenges that come with building microservices and keeps your code platform agnostic.
Vertical Pod Autoscalingallows you to automatically sets resource requests and limits on containers per workload based on past usage. VPA makes certain pods are scheduled onto nodes that have the required CPU and memory resources. For more information, seeKubernetes Vertical Pod Autoscaling.
Image Cleanerto clean up stale images on your Azure Kubernetes Service cluster.
Open Service Mesh add-onis a lightweight, extensible, cloud-native service mesh that allows you to uniformly manage, secure, and get out-of-the-box observability features for highly dynamic microservice environments. Bicep modules allow to install the Open Service Mesh add-on as an alternative to the Istio Service Mesh add-on.NOTE: you can't install both the Open Service Mesh add-on and Istio Service Mesh add-on on the same AKS cluster.
The Bicep modules deploy the following Azure resources:
Microsoft.CognitiveServices/accounts: anAzure OpenAI Servicewith aGPT-3.5model used by the chatbot application. Azure OpenAI Service gives customers advanced language AI with OpenAI GPT-4, GPT-3, Codex, and DALL-E models with Azure's security and enterprise promise. Azure OpenAI co-develops the APIs with OpenAI, ensuring compatibility and a smooth transition from one to the other.
Microsoft.Compute/virtualMachines: Bicep modules can optionally create a jump-box virtual machine to manage the private AKS cluster.
Microsoft.Network/bastionHosts: a separate Azure Bastion is deployed in the AKS cluster virtual network to provide SSH connectivity to both agent nodes and virtual machines.
Microsoft.Network/natGateways: a bring-your-own (BYO)Azure NAT Gatewayto manage outbound connections initiated by AKS-hosted workloads. The NAT Gateway is associated to theSystemSubnet,UserSubnet, andPodSubnetsubnets. TheoutboundTypeproperty of the cluster is set touserAssignedNatGatewayto specify that a BYO NAT Gateway is used for outbound connections. NOTE: you can update theoutboundTypeafter cluster creation and this will deploy or remove resources as required to put the cluster into the new egress configuration. For more information, seeUpdating outboundType after cluster creation.
Microsoft.Storage/storageAccounts: this storage account is used to store the boot diagnostics logs of both the service provider and service consumer virtual machines. Boot Diagnostics is a debugging feature that allows you to view console output and screenshots to diagnose virtual machine status.
Microsoft.ContainerRegistry/registries: an Azure Container Registry (ACR) to build, store, and manage container images and artifacts in a private registry for all container deployments.
Microsoft.Resources/deploymentScripts: a deployment script is used to run theinstall-nginx-via-helm-and-create-sa.shBash script which creates the namespace and servicea account for the sample application and installs the following packages to the AKS cluster viaHelm. For more information on deployment scripts, seeUse deployment scripts in Bicep
NOTE You can find thearchitecture.vsdxfile used for the diagram under thevisiofolder.
What is Bicep?
Bicepis a domain-specific language (DSL) that uses a declarative syntax to deploy Azure resources. It provides concise syntax, reliable type safety, and support for code reuse. Bicep offers the best authoring experience for your infrastructure-as-code solutions in Azure.
What is Azure OpenAI Service?
TheAzure OpenAI Serviceis a platform offered by Microsoft Azure that provides cognitive services powered byOpenAImodels. One of the models available through this service is theChatGPTmodel, designed for interactive conversational tasks. It allows developers to integrate natural language understanding and generation capabilities into their applications.
Azure OpenAI Service provides REST API access to OpenAI's powerful language models, including theGPT-3,CodexandEmbeddingsmodel series. In addition, the newGPT-4andChatGPTmodel series have now reached general availability. These models can be easily adapted to your specific task, including but not limited to content generation, summarization, semantic search, and natural language to code translation. Users can access the service through REST APIs, Python SDK, or our web-based interface in the Azure OpenAI Studio.
TheChat Completion API, part of the Azure OpenAI Service, provides a dedicated interface for interacting with theChatGPTandGPT-4 models. This API is currently in preview and is the preferred method for accessing these models. The GPT-4 models can only be accessed through this API.
GPT-3,GPT-3.5, andGPT-4models from OpenAI are prompt-based. With prompt-based models, the user interacts with the model by entering a text prompt, to which the model responds with a text completion. This completion is the model’s continuation of the input text. While these models are extremely powerful, their behavior is also very sensitive to the prompt. This makes prompt construction a critical skill to develop. For more information, seeIntroduction to prompt engineering.
Prompt construction can be complex. In practice, the prompt acts to configure the model weights to complete the desired task, but it's more of an art than a science, often requiring experience and intuition to craft a successful prompt. The goal of this article is to help get you started with this learning process. It attempts to capture general concepts and patterns that apply to all GPT models. However, it's essential to understand that each model behaves differently, so the learnings may not apply equally to all models.
Prompt engineering refers to creating instructions called prompts for Large Language Models (LLMs), such as OpenAI’s ChatGPT. With the immense potential of LLMs to solve a wide range of tasks, leveraging prompt engineering can empower us to save significant time and facilitate the development of impressive applications. It holds the key to unleashing the full capabilities of these huge models, transforming how we interact and benefit from them. For more information, seePrompt engineering techniques.
Deploy the Bicep modules
You can deploy the Bicep modules in thebicepfolder using thedeploy.shBash script in the same folder. Specify a value for the following parameters in thedeploy.shscript andmain.parameters.jsonparameters file before deploying the Bicep modules.
prefix: specifies a prefix for all the Azure resources.
authenticationType: specifies the type of authentication when accessing the Virtual Machine.sshPublicKeyis the recommended value. Allowed values:sshPublicKeyandpassword.
vmAdminUsername: specifies the name of the administrator account of the virtual machine.
vmAdminPasswordOrKey: specifies the SSH Key or password for the virtual machine.
aksClusterSshPublicKey: specifies the SSH Key or password for AKS cluster agent nodes.
aadProfileAdminGroupObjectIDs: when deploying an AKS cluster with Azure AD and Azure RBAC integration, this array parameter contains the list of Azure AD group object IDs that will have the admin role of the cluster.
keyVaultObjectIds: Specifies the object ID of the service principals to configure in Key Vault access policies.
The following table contains the code from theopenAi.bicepBicep module used to deploy theAzure OpenAI Service.
// Parameters
@description('Specifies the name of the Azure OpenAI resource.')
param name string = 'aks-${uniqueString(resourceGroup().id)}'
@description('Specifies the resource model definition representing SKU.')
param sku object = {
name: 'S0'
}
@description('Specifies the identity of the OpenAI resource.')
param identity object = {
type: 'SystemAssigned'
}
@description('Specifies the location.')
param location string = resourceGroup().location
@description('Specifies the resource tags.')
param tags object
@description('Specifies an optional subdomain name used for token-based authentication.')
param customSubDomainName string = ''
@description('Specifies whether or not public endpoint access is allowed for this account..')
@allowed([
'Enabled'
'Disabled'
])
param publicNetworkAccess string = 'Enabled'
@description('Specifies the OpenAI deployments to create.')
param deployments array = [
{
name: 'text-embedding-ada-002'
version: '2'
raiPolicyName: ''
capacity: 1
scaleType: 'Standard'
}
{
name: 'gpt-35-turbo'
version: '0301'
raiPolicyName: ''
capacity: 1
scaleType: 'Standard'
}
{
name: 'text-davinci-003'
version: '1'
raiPolicyName: ''
capacity: 1
scaleType: 'Standard'
}
]
@description('Specifies the workspace id of the Log Analytics used to monitor the Application Gateway.')
param workspaceId string
// Variables
var diagnosticSettingsName = 'diagnosticSettings'
var openAiLogCategories = [
'Audit'
'RequestResponse'
'Trace'
]
var openAiMetricCategories = [
'AllMetrics'
]
var openAiLogs = [for category in openAiLogCategories: {
category: category
enabled: true
}]
var openAiMetrics = [for category in openAiMetricCategories: {
category: category
enabled: true
}]
// Resources
resource openAi 'Microsoft.CognitiveServices/accounts@2022-12-01' = {
name: name
location: location
sku: sku
kind: 'OpenAI'
identity: identity
tags: tags
properties: {
customSubDomainName: customSubDomainName
publicNetworkAccess: publicNetworkAccess
}
}
resource model 'Microsoft.CognitiveServices/accounts/deployments@2022-12-01' =
[for deployment in deployments: {
name: deployment.name
parent: openAi
properties: {
model: {
format: 'OpenAI'
name: deployment.name
version: deployment.version
}
raiPolicyName: deployment.raiPolicyName
scaleSettings: {
capacity: deployment.capacity
scaleType: deployment.scaleType
}
}
}]
resource openAiDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
name: diagnosticSettingsName
scope: openAi
properties: {
workspaceId: workspaceId
logs: openAiLogs
metrics: openAiMetrics
}
}
// Outputs
output id string = openAi.id
output name string = openAi.name
Azure Cognitive Services use custom subdomain names for each resource created through theAzure portal,Azure Cloud Shell,Azure CLI,Bicep,Azure Resource Manager (ARM), orTerraform. Unlike regional endpoints, common for all customers in a specific Azure region, custom subdomain names are unique to the resource. Custom subdomain names are required to enable authentication features like Azure Active Directory (Azure AD). We need to specify a custom subdomain for ourAzure OpenAI Service,as our chatbot application will use an Azure AD security token to access it. By default, themain.bicepmodule sets the value of thecustomSubDomainNameparameter to the lowercase name of the Azure OpenAI resource. For more information on custom subdomains, seeCustom subdomain names for Cognitive Services.
This bicep module allows you to pass an array containing the definition of one or more model deployments in thedeploymentsparameter. For more information on model deployments, seeCreate a resource and deploy a model using Azure OpenAI
AKS Cluster Bicep module
TheaksCluster.bicepBicep module is used to deploy theAzure Kubernetes Service(AKS)cluster. In particular, the following code snippet creates the user-defined managed identity used by the chatbot to acquire a security token from Azure Active Directory viaAzure AD workload identity. When the booleanopenAiEnabledparameter istrue, the Bicep code performs the following steps:
Creates a new user-defined managed identity.
Assign the new managed identity to the Cognitive Services User role with the resource group as a scope.
Federate the managed identity with the service account used by the chatbot. The following information are necessary to create the federated identity credentials:
The Kubernetes service account name.
The Kubernetes namespace that will host the chatbot application.
...
@description('Specifies the name of the user-defined managed identity used by the application that uses Azure AD workload identity to authenticate against Azure OpenAI.')
param workloadManagedIdentityName string
@description('Specifies whether creating the Azure OpenAi resource or not.')
param openAiEnabled bool = false
...
// This user-defined managed identity used by the workload to connect to the Azure OpenAI resource with a security token issued by Azure Active Directory
resource workloadManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = if (openAiEnabled) {
name: workloadManagedIdentityName
location: location
tags: tags
}
// Assign the Cognitive Services User role to the user-defined managed identity used by workloads
resource cognitiveServicesUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (openAiEnabled) {
name: guid(workloadManagedIdentity.id, cognitiveServicesUserRoleDefinitionId)
scope: resourceGroup()
properties: {
roleDefinitionId: cognitiveServicesUserRoleDefinitionId
principalId: workloadManagedIdentity.properties.principalId
principalType: 'ServicePrincipal'
}
}
// Create federated identity for the user-defined managed identity used by the workload
resource federatedIdentityCredentials 'Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials@2023-01-31' = {
name: letterCaseType == 'UpperCamelCase' ? '${toUpper(first(namespace))}${toLower(substring(namespace, 1, length(namespace) - 1))}FederatedIdentity' : letterCaseType == 'CamelCase' ? '${toLower(namespace)}FederatedIdentity' : '${toLower(namespace)}-federated-identity'
parent: workloadManagedIdentity
properties: {
issuer: aksCluster.properties.oidcIssuerProfile.issuerURL
subject: 'system:serviceaccount:${namespace}:${serviceAccountName}'
audiences: [
'api://AzureADTokenExchange'
]
}
}
...
// Output
output id string = aksCluster.id
output name string = aksCluster.name
output issuerUrl string = aksCluster.properties.oidcIssuerProfile.issuerURL
output workloadManagedIdentityClientId string = workloadManagedIdentity.properties.clientId
Validate the deployment
Open the Azure Portal, and navigate to the resource group. Open the Azure Open AI Service resource, navigate toKeys and Endpoint, and check that the endpoint contains a custom subdomain rather than the regional Cognitive Services endpoint.
Open to the<Prefix>WorkloadManagedIdentitymanaged identity, navigate to theFederated credentials, and verify that the federated identity credentials for themagic8ball-saservice account were created correctly, as shown in the following picture.
Use Azure AD workload identity with Azure Kubernetes Service (AKS)
Workloads deployed on an Azure Kubernetes Services (AKS) cluster require Azure Active Directory (Azure AD) application credentials or managed identities to access Azure AD-protected resources, such as Azure Key Vault and Microsoft Graph. Azure AD workload identity integrates with the capabilities native to Kubernetes to federate with external identity providers.
The sample makes use of aDeployment Scriptto run theinstall-nginx-via-helm-and-create-sa.shBash script that creates the namespace and service account for the sample application and installs the following packages to the AKS cluster viaHelm. For more information on deployment scripts, seeUse deployment scripts in Bicep.
Theinstall-nginx-via-helm-and-create-sa.shBash script returns the following outputs to the deployment script:
Namespace hosting the chatbot sample. You can change the defaultmagic8ballnamespace by assigning a different value to thenamespaceparameter of themain.bicepmodule.
The application is contained in a single file calledapp.py. The application makes use of the following libraries:
OpenAPI: The OpenAI Python library provides convenient access to the OpenAI API from applications written in Python. It includes a pre-defined set of classes for API resources that initialize themselves dynamically from API responses, making it compatible with a wide range of versions of the OpenAI API. You can find usage examples for the OpenAI Python library in ourAPI referenceand theOpenAI Cookbook.
Azure Identity: The Azure Identity library providesAzure Active Directory (Azure AD)token authentication support across the Azure SDK. It provides a set ofTokenCredentialimplementations, which can be used to construct Azure SDK clients that support Azure AD token authentication.
Streamlit: Streamlit is an open-source Python library that makes it easy to create and share beautiful, custom web apps for machine learning and data science. You can build and deploy powerful data apps in just a few minutes. For more information, seeStreamlit documentation.
Streamlit-chat: a Streamlit component that provides a configurable user interface for chatbot applications.
Dotenv: Python-dotenv reads key-value pairs from a .env file and can set them as environment variables. It helps in the development of applications following the12-factorprinciples.
Therequirements.txtfile under thescriptsfolder contains the list of packages used by theapp.pyapplication that you can restore using the following command:
pip install -r requirements.txt --upgrade
The following table contains the code of theapp.pychatbot:
# Import packages
import os
import sys
import time
import openai
import logging
import streamlit as st
from streamlit_chat import message
from azure.identity import DefaultAzureCredential
from dotenv import load_dotenv
from dotenv import dotenv_values
# Load environment variables from .env file
if os.path.exists(".env"):
load_dotenv(override=True)
config = dotenv_values(".env")
# Read environment variables
assistan_profile = """
You are the infamous Magic 8 Ball. You need to randomly reply to any question with one of the following answers:
- It is certain.
- It is decidedly so.
- Without a doubt.
- Yes definitely.
- You may rely on it.
- As I see it, yes.
- Most likely.
- Outlook good.
- Yes.
- Signs point to yes.
- Reply hazy, try again.
- Ask again later.
- Better not tell you now.
- Cannot predict now.
- Concentrate and ask again.
- Don't count on it.
- My reply is no.
- My sources say no.
- Outlook not so good.
- Very doubtful.
Add a short comment in a pirate style at the end! Follow your heart and be creative!
For mor information, see https://en.wikipedia.org/wiki/Magic_8_Ball
"""
title = os.environ.get("TITLE", "Magic 8 Ball")
text_input_label = os.environ.get("TEXT_INPUT_LABEL", "Pose your question and cross your fingers!")
image_file_name = os.environ.get("IMAGE_FILE_NAME", "magic8ball.png")
image_width = int(os.environ.get("IMAGE_WIDTH", 80))
temperature = float(os.environ.get("TEMPERATURE", 0.9))
system = os.environ.get("SYSTEM", assistan_profile)
api_base = os.getenv("AZURE_OPENAI_BASE")
api_key = os.getenv("AZURE_OPENAI_KEY")
api_type = os.environ.get("AZURE_OPENAI_TYPE", "azure")
api_version = os.environ.get("AZURE_OPENAI_VERSION", "2023-05-15")
engine = os.getenv("AZURE_OPENAI_DEPLOYMENT")
model = os.getenv("AZURE_OPENAI_MODEL")
# Configure OpenAI
openai.api_type = api_type
openai.api_version = api_version
openai.api_base = api_base
# Set default Azure credential
default_credential = DefaultAzureCredential() if openai.api_type == "azure_ad" else None
# Configure a logger
logging.basicConfig(stream = sys.stdout,
format = '[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s',
level = logging.INFO)
logger = logging.getLogger(__name__)
# Log variables
logger.info(f"title: {title}")
logger.info(f"text_input_label: {text_input_label}")
logger.info(f"image_file_name: {image_file_name}")
logger.info(f"image_width: {image_width}")
logger.info(f"temperature: {temperature}")
logger.info(f"system: {system}")
logger.info(f"api_base: {api_base}")
logger.info(f"api_key: {api_key}")
logger.info(f"api_type: {api_type}")
logger.info(f"api_version: {api_version}")
logger.info(f"engine: {engine}")
logger.info(f"model: {model}")
# Authenticate to Azure OpenAI
if openai.api_type == "azure":
openai.api_key = api_key
elif openai.api_type == "azure_ad":
openai_token = default_credential.get_token("https://cognitiveservices.azure.com/.default")
openai.api_key = openai_token.token
if 'openai_token' not in st.session_state:
st.session_state['openai_token'] = openai_token
else:
logger.error("Invalid API type. Please set the AZURE_OPENAI_TYPE environment variable to azure or azure_ad.")
raise ValueError("Invalid API type. Please set the AZURE_OPENAI_TYPE environment variable to azure or azure_ad.")
# Customize Streamlit UI using CSS
st.markdown("""
<style>
div.stButton > button:first-child {
background-color: #eb5424;
color: white;
font-size: 20px;
font-weight: bold;
border-radius: 0.5rem;
padding: 0.5rem 1rem;
border: none;
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
width: 300 px;
height: 42px;
transition: all 0.2s ease-in-out;
}
div.stButton > button:first-child:hover {
transform: translateY(-3px);
box-shadow: 0 1rem 2rem rgba(0,0,0,0.15);
}
div.stButton > button:first-child:active {
transform: translateY(-1px);
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
}
div.stButton > button:focus:not(:focus-visible) {
color: #FFFFFF;
}
@media only screen and (min-width: 768px) {
/* For desktop: */
div {
font-family: 'Roboto', sans-serif;
}
div.stButton > button:first-child {
background-color: #eb5424;
color: white;
font-size: 20px;
font-weight: bold;
border-radius: 0.5rem;
padding: 0.5rem 1rem;
border: none;
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
width: 300 px;
height: 42px;
transition: all 0.2s ease-in-out;
position: relative;
bottom: -32px;
right: 0px;
}
div.stButton > button:first-child:hover {
transform: translateY(-3px);
box-shadow: 0 1rem 2rem rgba(0,0,0,0.15);
}
div.stButton > button:first-child:active {
transform: translateY(-1px);
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
}
div.stButton > button:focus:not(:focus-visible) {
color: #FFFFFF;
}
input {
border-radius: 0.5rem;
padding: 0.5rem 1rem;
border: none;
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
transition: all 0.2s ease-in-out;
height: 40px;
}
}
</style>
""", unsafe_allow_html=True)
# Initialize Streamlit session state
if 'prompts' not in st.session_state:
st.session_state['prompts'] = [{"role": "system", "content": system}]
if 'generated' not in st.session_state:
st.session_state['generated'] = []
if 'past' not in st.session_state:
st.session_state['past'] = []
# Refresh the OpenAI security token every 45 minutes
def refresh_openai_token():
if st.session_state['openai_token'].expires_on < int(time.time()) - 45 * 60:
st.session_state['openai_token'] = default_credential.get_token("https://cognitiveservices.azure.com/.default")
openai.api_key = st.session_state['openai_token'].token
# Send user prompt to Azure OpenAI
def generate_response(prompt):
try:
st.session_state['prompts'].append({"role": "user", "content": prompt})
if openai.api_type == "azure_ad":
refresh_openai_token()
completion = openai.ChatCompletion.create(
engine = engine,
model = model,
messages = st.session_state['prompts'],
temperature = temperature,
)
message = completion.choices[0].message.content
return message
except Exception as e:
logging.exception(f"Exception in generate_response: {e}")
# Reset Streamlit session state to start a new chat from scratch
def new_click():
st.session_state['prompts'] = [{"role": "system", "content": system}]
st.session_state['past'] = []
st.session_state['generated'] = []
st.session_state['user'] = ""
# Handle on_change event for user input
def user_change():
# Avoid handling the event twice when clicking the Send button
chat_input = st.session_state['user']
st.session_state['user'] = ""
if (chat_input == '' or
(len(st.session_state['past']) > 0 and chat_input == st.session_state['past'][-1])):
return
# Generate response invoking Azure OpenAI LLM
if chat_input != '':
output = generate_response(chat_input)
# store the output
st.session_state['past'].append(chat_input)
st.session_state['generated'].append(output)
st.session_state['prompts'].append({"role": "assistant", "content": output})
# Create a 2-column layout. Note: Streamlit columns do not properly render on mobile devices.
# For more information, see https://github.com/streamlit/streamlit/issues/5003
col1, col2 = st.columns([1, 7])
# Display the robot image
with col1:
st.image(image = os.path.join("images", image_file_name), width = image_width)
# Display the title
with col2:
st.title(title)
# Create a 3-column layout. Note: Streamlit columns do not properly render on mobile devices.
# For more information, see https://github.com/streamlit/streamlit/issues/5003
col3, col4, col5 = st.columns([7, 1, 1])
# Create text input in column 1
with col3:
user_input = st.text_input(text_input_label, key = "user", on_change = user_change)
# Create send button in column 2
with col4:
st.button(label = "Send")
# Create new button in column 3
with col5:
st.button(label = "New", on_click = new_click)
# Display the chat history in two separate tabs
# - normal: display the chat history as a list of messages using the streamlit_chat message() function
# - rich: display the chat history as a list of messages using the Streamlit markdown() function
if st.session_state['generated']:
tab1, tab2 = st.tabs(["normal", "rich"])
with tab1:
for i in range(len(st.session_state['generated']) - 1, -1, -1):
message(st.session_state['past'][i], is_user = True, key = str(i) + '_user', avatar_style = "fun-emoji", seed = "Nala")
message(st.session_state['generated'][i], key = str(i), avatar_style = "bottts", seed = "Fluffy")
with tab2:
for i in range(len(st.session_state['generated']) - 1, -1, -1):
st.markdown(st.session_state['past'][i])
st.markdown(st.session_state['generated'][i])
def generate_response(prompt):
try:
st.session_state['prompts'].append({"role": "user", "content": prompt})
if openai.api_type == "azure_ad":
refresh_openai_token()
completion = openai.ChatCompletion.create(
engine = engine,
model = model,
messages = st.session_state['prompts'],
temperature = temperature,
)
message = completion.choices[0].message.content
return message
except Exception as e:
logging.exception(f"Exception in generate_response: {e}")
OpenAI trained the ChatGPT and GPT-4 models to accept input formatted as a conversation. The messages parameter takes an array of dictionaries with a conversation organized by role or message: system, user, and assistant. The format of a basic Chat Completion is as follows:
{"role": "system", "content": "Provide some context and/or instructions to the model"},
{"role": "user", "content": "The users messages goes here"},
{"role": "assistant", "content": "The response message goes here."}
Thesystemrole, also known as the system message, is included at the beginning of the array. This message provides the initial instructions for the model. You can provide various information in the system role, including:
A brief description of the assistant
Personality traits of the assistant
Instructions or rules you would like the assistant to follow
Data or information needed for the model, such as relevant questions from an FAQ
You can customize the system role for your use case or include basic instructions.
Thesystemrole or message is optional, but it's recommended to at least include a basic one to get the best results. Theuserrole or message represents an input or inquiry from the user, while theassistantmessage corresponds to the response generated by the GPT API. This dialog exchange aims to simulate a human-like conversation, where the user message initiates the interaction and the assistant message provides a relevant and informative answer. This context helps the chat model generate a more appropriate response later on. The last user message refers to the prompt currently requested. For more information, seeLearn how to work with the ChatGPT and GPT-4 models.
Application Configuration
Make sure to provide a value for the following environment variables when testing theapp.pyPython app locally, for example in Visual Studio Code. You can eventually define environment variables in a.envfile in the same folder as theapp.pyfile.
AZURE_OPENAI_TYPE: specifyazureif you want to let the application use the API key to authenticate against OpenAI. In this case, make sure to provide the Key in theAZURE_OPENAI_KEYenvironment variable. If you want to authenticate using an Azure AD security token, you need to specifyazure_adas a value. In this case, don't need to provide any value in theAZURE_OPENAI_KEYenvironment variable.
AZURE_OPENAI_BASE: the URL of your Azure OpenAI resource. If you use the API key to authenticate against OpenAI, you can specify the regional endpoint of your Azure OpenAI Service (e.g.,https://eastus.api.cognitive.microsoft.com/). If you instead plan to use Azure AD security tokens for authentication, you need to deploy your Azure OpenAI Service with a subdomain and specify the resource-specific endpoint url (e.g.,https://myopenai.openai.azure.com/).
AZURE_OPENAI_KEY: the key of your Azure OpenAI resource.
AZURE_OPENAI_DEPLOYMENT: the name of the ChatGPT deployment used by your Azure OpenAI resource, for examplegpt-35-turbo.
AZURE_OPENAI_MODEL: the name of the ChatGPT model used by your Azure OpenAI resource, for examplegpt-35-turbo.
TITLE: the title of the Streamlit app.
TEMPERATURE: the temperature used by the OpenAI API to generate the response.
SYSTEM: give the model instructions about how it should behave and any context it should reference when generating a response. Used to describe the assistant's personality.
When deploying the application to Azure Kubernetes Service (AKS), these values are provided in a KubernetesConfigMap. For more information, see the next section.
OpenAI Library
To use theopenailibrary with Microsoft Azure endpoints, you need to set theapi_type,api_baseandapi_versionin addition to theapi_key. Theapi_typemust be set to 'azure' and the others correspond to the properties of your endpoint. In addition, the deployment name must be passed as the engine parameter. To use OpenAI Key to authenticate to your Azure endpoint, you need to set theapi_typetoazureand pass the OpenAI Key toapi_key.
To use Microsoft Active Directory to authenticate to your Azure endpoint, you need to set theapi_typetoazure_adand pass the acquired credential token toapi_key. The rest of the parameters must be set as specified in the previous section.
You can use two different authentication methods in themagic8ballchatbot application:
API key: set theAZURE_OPENAI_TYPEenvironment variable to azure and the AZURE_OPENAI_KEY environment variable to the key of your Azure OpenAI resource. You can use the regional endpoint, such ashttps://eastus.api.cognitive.microsoft.com/, in theAZURE_OPENAI_BASEenvironment variable, to connect to the Azure OpenAI resource.
Azure Active Directory: set theAZURE_OPENAI_TYPEenvironment variable to azure_ad and use a service principal or managed identity with theDefaultAzureCredentialobject to acquire a security token from Azure Active Directory. For more information on the DefaultAzureCredential in Python, seeAuthenticate Python apps to Azure services by using the Azure SDK for Python. Make sure to assign theCognitive Services Userrole to the service principal or managed identity used to authenticate to your Azure OpenAI Service. For more information, seeHow to configure Azure OpenAI Service with managed identities. If you want to use Azure AD integrated security, you need to create a custom subdomain for your Azure OpenAI resource and use the specific endpoint containing the custom domain, such ashttps://myopenai.openai.azure.com/where myopenai is the custom subdomain. If you specify the regional endpoint, you get an error like the following:Subdomain does not map to a resource. Hence, pass the custom domain endpoint in the AZURE_OPENAI_BASE environment variable. In this case, you also need to refresh the security token periodically.
Build the container image
You can build the container image using the01-build-docker-image.shin thescriptsfolder.
Dockerfile
# app/Dockerfile
# # Stage 1 - Install build dependencies
# A Dockerfile must start with a FROM instruction which sets the base image for the container.
# The Python images come in many flavors, each designed for a specific use case.
# The python:3.11-slim image is a good base image for most applications.
# It is a minimal image built on top of Debian Linux and includes only the necessary packages to run Python.
# The slim image is a good choice because it is small and contains only the packages needed to run Python.
# For more information, see:
# * https://hub.docker.com/_/python
# * https://docs.streamlit.io/knowledge-base/tutorials/deploy/docker
FROM python:3.11-slim AS builder
# The WORKDIR instruction sets the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions that follow it in the Dockerfile.
# If the WORKDIR doesn’t exist, it will be created even if it’s not used in any subsequent Dockerfile instruction.
# For more information, see: https://docs.docker.com/engine/reference/builder/#workdir
WORKDIR /app
# Set environment variables.
# The ENV instruction sets the environment variable <key> to the value <value>.
# This value will be in the environment of all “descendant” Dockerfile commands and can be replaced inline in many as well.
# For more information, see: https://docs.docker.com/engine/reference/builder/#env
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# Install git so that we can clone the app code from a remote repo using the RUN instruction.
# The RUN comand has 2 forms:
# * RUN <command> (shell form, the command is run in a shell, which by default is /bin/sh -c on Linux or cmd /S /C on Windows)
# * RUN ["executable", "param1", "param2"] (exec form)
# The RUN instruction will execute any commands in a new layer on top of the current image and commit the results.
# The resulting committed image will be used for the next step in the Dockerfile.
# For more information, see: https://docs.docker.com/engine/reference/builder/#run
RUN apt-get update && apt-get install -y \
build-essential \
curl \
software-properties-common \
git \
&& rm -rf /var/lib/apt/lists/*
# Create a virtualenv to keep dependencies together
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Clone the requirements.txt which contains dependencies to WORKDIR
# COPY has two forms:
# * COPY <src> <dest> (this copies the files from the local machine to the container's own filesystem)
# * COPY ["<src>",... "<dest>"] (this form is required for paths containing whitespace)
# For more information, see: https://docs.docker.com/engine/reference/builder/#copy
COPY requirements.txt .
# Install the Python dependencies
RUN pip install --no-cache-dir --no-deps -r requirements.txt
# Stage 2 - Copy only necessary files to the runner stage
# The FROM instruction initializes a new build stage for the application
FROM python:3.11-slim
# Sets the working directory to /app
WORKDIR /app
# Copy the virtual environment from the builder stage
COPY --from=builder /opt/venv /opt/venv
# Set environment variables
ENV PATH="/opt/venv/bin:$PATH"
# Clone the app.py containing the application code
COPY app.py .
# Copy the images folder to WORKDIR
# The ADD instruction copies new files, directories or remote file URLs from <src> and adds them to the filesystem of the image at the path <dest>.
# For more information, see: https://docs.docker.com/engine/reference/builder/#add
ADD images ./images
# The EXPOSE instruction informs Docker that the container listens on the specified network ports at runtime.
# For more information, see: https://docs.docker.com/engine/reference/builder/#expose
EXPOSE 8501
# The HEALTHCHECK instruction has two forms:
# * HEALTHCHECK [OPTIONS] CMD command (check container health by running a command inside the container)
# * HEALTHCHECK NONE (disable any healthcheck inherited from the base image)
# The HEALTHCHECK instruction tells Docker how to test a container to check that it is still working.
# This can detect cases such as a web server that is stuck in an infinite loop and unable to handle new connections,
# even though the server process is still running. For more information, see: https://docs.docker.com/engine/reference/builder/#healthcheck
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
# The ENTRYPOINT instruction has two forms:
# * ENTRYPOINT ["executable", "param1", "param2"] (exec form, preferred)
# * ENTRYPOINT command param1 param2 (shell form)
# The ENTRYPOINT instruction allows you to configure a container that will run as an executable.
# For more information, see: https://docs.docker.com/engine/reference/builder/#entrypoint
ENTRYPOINT ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
Before running any script, make sure to customize the value of the variables inside the00-variables.shfile. This file is embedded in all the scripts and contains the following variables:
# Variables
acrName="CoralAcr"
acrResourceGrougName="CoralRG"
location="FranceCentral"
attachAcr=false
imageName="magic8ball"
tag="v2"
containerName="magic8ball"
image="$acrName.azurecr.io/$imageName:$tag"
imagePullPolicy="IfNotPresent" # Always, Never, IfNotPresent
managedIdentityName="OpenAiManagedIdentity"
federatedIdentityName="Magic8BallFederatedIdentity"
# Azure Subscription and Tenant
subscriptionId=$(az account show --query id --output tsv)
subscriptionName=$(az account show --query name --output tsv)
tenantId=$(az account show --query tenantId --output tsv)
# Parameters
title="Magic 8 Ball"
label="Pose your question and cross your fingers!"
temperature="0.9"
imageWidth="80"
# OpenAI
openAiName="CoralOpenAi "
openAiResourceGroupName="CoralRG"
openAiType="azure_ad"
openAiBase="https://coralopenai.openai.azure.com/"
openAiModel="gpt-35-turbo"
openAiDeployment="gpt-35-turbo"
# Nginx Ingress Controller
nginxNamespace="ingress-basic"
nginxRepoName="ingress-nginx"
nginxRepoUrl="https://kubernetes.github.io/ingress-nginx"
nginxChartName="ingress-nginx"
nginxReleaseName="nginx-ingress"
nginxReplicaCount=3
# Certificate Manager
cmNamespace="cert-manager"
cmRepoName="jetstack"
cmRepoUrl="https://charts.jetstack.io"
cmChartName="cert-manager"
cmReleaseName="cert-manager"
# Cluster Issuer
email="paolos@microsoft.com"
clusterIssuerName="letsencrypt-nginx"
clusterIssuerTemplate="cluster-issuer.yml"
# AKS Cluster
aksClusterName="CoralAks"
aksResourceGroupName="CoralRG"
# Sample Application
namespace="magic8ball"
serviceAccountName="magic8ball-sa"
deploymentTemplate="deployment.yml"
serviceTemplate="service.yml"
configMapTemplate="configMap.yml"
secretTemplate="secret.yml"
# Ingress and DNS
ingressTemplate="ingress.yml"
ingressName="magic8ball-ingress"
dnsZoneName="babosbird.com"
dnsZoneResourceGroupName="DnsResourceGroup"
subdomain="magic8ball"
host="$subdomain.$dnsZoneName"
Upload Docker container image to Azure Container Registry (ACR)
You can push the Docker container image to Azure Container Registry (ACR) using the03-push-docker-image.shscript in thescriptsfolder.
#!/bin/bash
# Variables
source ./00-variables.sh
# Login to ACR
az acr login --name $acrName
# Retrieve ACR login server. Each container image needs to be tagged with the loginServer name of the registry.
loginServer=$(az acr show --name $acrName --query loginServer --output tsv)
# Tag the local image with the loginServer of ACR
docker tag ${imageName,,}:$tag $loginServer/${imageName,,}:$tag
# Push latest container image to ACR
docker push $loginServer/${imageName,,}:$tag
Deployment Scripts
If you deployed the Azure infrastructure using the Bicep modules provided with this sample, you only need to deploy the application using the following scripts and YAML templates in thescriptsfolder.
09-deploy-app.sh
10-create-ingress.sh
11-configure-dns.sh
configMap.yml
deployment.yml
ingress.yml
service.yml
If you instead want to deploy the application in your AKS cluster, you can use the following scripts to configure your environment.
04-create-nginx-ingress-controller.sh
The scriptinstalls theNGINX Ingress Controllerusing Helm.
#!/bin/bash
# Variables
source ./00-variables.sh
# Use Helm to deploy an NGINX ingress controller
result=$(helm list -n $nginxNamespace | grep $nginxReleaseName | awk '{print $1}')
if [[ -n $result ]]; then
echo "[$nginxReleaseName] ingress controller already exists in the [$nginxNamespace] namespace"
else
# Check if the ingress-nginx repository is not already added
result=$(helm repo list | grep $nginxRepoName | awk '{print $1}')
if [[ -n $result ]]; then
echo "[$nginxRepoName] Helm repo already exists"
else
# Add the ingress-nginx repository
echo "Adding [$nginxRepoName] Helm repo..."
helm repo add $nginxRepoName $nginxRepoUrl
fi
# Update your local Helm chart repository cache
echo 'Updating Helm repos...'
helm repo update
# Deploy NGINX ingress controller
echo "Deploying [$nginxReleaseName] NGINX ingress controller to the [$nginxNamespace] namespace..."
helm install $nginxReleaseName $nginxRepoName/$nginxChartName \
--create-namespace \
--namespace $nginxNamespace \
--set controller.config.enable-modsecurity=true \
--set controller.config.enable-owasp-modsecurity-crs=true \
--set controller.config.modsecurity-snippet=\
'SecRuleEngine On
SecRequestBodyAccess On
SecAuditLog /dev/stdout
SecAuditLogFormat JSON
SecAuditEngine RelevantOnly
SecRule REMOTE_ADDR "@ipMatch 127.0.0.1" "id:87,phase:1,pass,nolog,ctl:ruleEngine=Off"' \
--set controller.metrics.enabled=true \
--set controller.metrics.serviceMonitor.enabled=true \
--set controller.metrics.serviceMonitor.additionalLabels.release="prometheus" \
--set controller.nodeSelector."kubernetes\.io/os"=linux \
--set controller.replicaCount=$replicaCount \
--set defaultBackend.nodeSelector."kubernetes\.io/os"=linux \
--set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"=/healthz
fi
05-install-cert-manager.sh This script installs thecert-managerusing Helm.
#/bin/bash
# Variables
source ./00-variables.sh
# Check if the ingress-nginx repository is not already added
result=$(helm repo list | grep $cmRepoName | awk '{print $1}')
if [[ -n $result ]]; then
echo "[$cmRepoName] Helm repo already exists"
else
# Add the Jetstack Helm repository
echo "Adding [$cmRepoName] Helm repo..."
helm repo add $cmRepoName $cmRepoUrl
fi
# Update your local Helm chart repository cache
echo 'Updating Helm repos...'
helm repo update
# Install cert-manager Helm chart
result=$(helm list -n $cmNamespace | grep $cmReleaseName | awk '{print $1}')
if [[ -n $result ]]; then
echo "[$cmReleaseName] cert-manager already exists in the $cmNamespace namespace"
else
# Install the cert-manager Helm chart
echo "Deploying [$cmReleaseName] cert-manager to the $cmNamespace namespace..."
helm install $cmReleaseName $cmRepoName/$cmChartName \
--create-namespace \
--namespace $cmNamespace \
--set installCRDs=true \
--set nodeSelector."kubernetes\.io/os"=linux
fi
06-create-cluster-issuer.sh
This script creates a cluster issuer for theNGINX Ingress Controllerbased on theLet's EncryptACME certificate issuer.
#/bin/bash
# Variables
source ./00-variables.sh
# Check if the cluster issuer already exists
result=$(kubectl get ClusterIssuer -o json | jq -r '.items[].metadata.name | select(. == "'$clusterIssuerName'")')
if [[ -n $result ]]; then
echo "[$clusterIssuerName] cluster issuer already exists"
exit
else
# Create the cluster issuer
echo "[$clusterIssuerName] cluster issuer does not exist"
echo "Creating [$clusterIssuerName] cluster issuer..."
cat $clusterIssuerTemplate |
yq "(.spec.acme.email)|="\""$email"\" |
kubectl apply -f -
fi
07-create-workload-managed-identity.sh
This script creates the managed identity used by themagic8ballchatbot and assigns it theCognitive Services Userrole on the Azure OpenAI Service.
#!/bin/bash
# Variables
source ./00-variables.sh
# Check if the user-assigned managed identity already exists
echo "Checking if [$managedIdentityName] user-assigned managed identity actually exists in the [$aksResourceGroupName] resource group..."
az identity show \
--name $managedIdentityName \
--resource-group $aksResourceGroupName &>/dev/null
if [[ $? != 0 ]]; then
echo "No [$managedIdentityName] user-assigned managed identity actually exists in the [$aksResourceGroupName] resource group"
echo "Creating [$managedIdentityName] user-assigned managed identity in the [$aksResourceGroupName] resource group..."
# Create the user-assigned managed identity
az identity create \
--name $managedIdentityName \
--resource-group $aksResourceGroupName \
--location $location \
--subscription $subscriptionId 1>/dev/null
if [[ $? == 0 ]]; then
echo "[$managedIdentityName] user-assigned managed identity successfully created in the [$aksResourceGroupName] resource group"
else
echo "Failed to create [$managedIdentityName] user-assigned managed identity in the [$aksResourceGroupName] resource group"
exit
fi
else
echo "[$managedIdentityName] user-assigned managed identity already exists in the [$aksResourceGroupName] resource group"
fi
# Retrieve the clientId of the user-assigned managed identity
echo "Retrieving clientId for [$managedIdentityName] managed identity..."
clientId=$(az identity show \
--name $managedIdentityName \
--resource-group $aksResourceGroupName \
--query clientId \
--output tsv)
if [[ -n $clientId ]]; then
echo "[$clientId] clientId for the [$managedIdentityName] managed identity successfully retrieved"
else
echo "Failed to retrieve clientId for the [$managedIdentityName] managed identity"
exit
fi
# Retrieve the principalId of the user-assigned managed identity
echo "Retrieving principalId for [$managedIdentityName] managed identity..."
principalId=$(az identity show \
--name $managedIdentityName \
--resource-group $aksResourceGroupName \
--query principalId \
--output tsv)
if [[ -n $principalId ]]; then
echo "[$principalId] principalId for the [$managedIdentityName] managed identity successfully retrieved"
else
echo "Failed to retrieve principalId for the [$managedIdentityName] managed identity"
exit
fi
# Get the resource id of the Azure OpenAI resource
openAiId=$(az cognitiveservices account show \
--name $openAiName \
--resource-group $openAiResourceGroupName \
--query id \
--output tsv)
if [[ -n $openAiId ]]; then
echo "Resource id for the [$openAiName] Azure OpenAI resource successfully retrieved"
else
echo "Failed to the resource id for the [$openAiName] Azure OpenAI resource"
exit -1
fi
# Assign the Cognitive Services User role on the Azure OpenAI resource to the managed identity
role="Cognitive Services User"
echo "Checking if the [$managedIdentityName] managed identity has been assigned to [$role] role with [$openAiName] Azure OpenAI resource as a scope..."
current=$(az role assignment list \
--assignee $principalId \
--scope $openAiId \
--query "[?roleDefinitionName=='$role'].roleDefinitionName" \
--output tsv 2>/dev/null)
if [[ $current == $role ]]; then
echo "[$managedIdentityName] managed identity is already assigned to the ["$current"] role with [$openAiName] Azure OpenAI resource as a scope"
else
echo "[$managedIdentityName] managed identity is not assigned to the [$role] role with [$openAiName] Azure OpenAI resource as a scope"
echo "Assigning the [$role] role to the [$managedIdentityName] managed identity with [$openAiName] Azure OpenAI resource as a scope..."
az role assignment create \
--assignee $principalId \
--role "$role" \
--scope $openAiId 1>/dev/null
if [[ $? == 0 ]]; then
echo "[$managedIdentityName] managed identity successfully assigned to the [$role] role with [$openAiName] Azure OpenAI resource as a scope"
else
echo "Failed to assign the [$managedIdentityName] managed identity to the [$role] role with [$openAiName] Azure OpenAI resource as a scope"
exit
fi
fi
08-create-service-account.sh
This script creates the namespace and service account for themagic8ballchatbot and federate the service account with the user-defined managed identity created in the previous step.
#!/bin/bash
# Variables for the user-assigned managed identity
source ./00-variables.sh
# Check if the namespace already exists
result=$(kubectl get namespace -o 'jsonpath={.items[?(@.metadata.name=="'$namespace'")].metadata.name'})
if [[ -n $result ]]; then
echo "[$namespace] namespace already exists"
else
# Create the namespace for your ingress resources
echo "[$namespace] namespace does not exist"
echo "Creating [$namespace] namespace..."
kubectl create namespace $namespace
fi
# Check if the service account already exists
result=$(kubectl get sa -n $namespace -o 'jsonpath={.items[?(@.metadata.name=="'$serviceAccountName'")].metadata.name'})
if [[ -n $result ]]; then
echo "[$serviceAccountName] service account already exists"
else
# Retrieve the resource id of the user-assigned managed identity
echo "Retrieving clientId for [$managedIdentityName] managed identity..."
managedIdentityClientId=$(az identity show \
--name $managedIdentityName \
--resource-group $aksResourceGroupName \
--query clientId \
--output tsv)
if [[ -n $managedIdentityClientId ]]; then
echo "[$managedIdentityClientId] clientId for the [$managedIdentityName] managed identity successfully retrieved"
else
echo "Failed to retrieve clientId for the [$managedIdentityName] managed identity"
exit
fi
# Create the service account
echo "[$serviceAccountName] service account does not exist"
echo "Creating [$serviceAccountName] service account..."
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
azure.workload.identity/client-id: $managedIdentityClientId
azure.workload.identity/tenant-id: $tenantId
labels:
azure.workload.identity/use: "true"
name: $serviceAccountName
namespace: $namespace
EOF
fi
# Show service account YAML manifest
echo "Service Account YAML manifest"
echo "-----------------------------"
kubectl get sa $serviceAccountName -n $namespace -o yaml
# Check if the federated identity credential already exists
echo "Checking if [$federatedIdentityName] federated identity credential actually exists in the [$aksResourceGroupName] resource group..."
az identity federated-credential show \
--name $federatedIdentityName \
--resource-group $aksResourceGroupName \
--identity-name $managedIdentityName &>/dev/null
if [[ $? != 0 ]]; then
echo "No [$federatedIdentityName] federated identity credential actually exists in the [$aksResourceGroupName] resource group"
# Get the OIDC Issuer URL
aksOidcIssuerUrl="$(az aks show \
--only-show-errors \
--name $aksClusterName \
--resource-group $aksResourceGroupName \
--query oidcIssuerProfile.issuerUrl \
--output tsv)"
# Show OIDC Issuer URL
if [[ -n $aksOidcIssuerUrl ]]; then
echo "The OIDC Issuer URL of the $aksClusterName cluster is $aksOidcIssuerUrl"
fi
echo "Creating [$federatedIdentityName] federated identity credential in the [$aksResourceGroupName] resource group..."
# Establish the federated identity credential between the managed identity, the service account issuer, and the subject.
az identity federated-credential create \
--name $federatedIdentityName \
--identity-name $managedIdentityName \
--resource-group $aksResourceGroupName \
--issuer $aksOidcIssuerUrl \
--subject system:serviceaccount:$namespace:$serviceAccountName
if [[ $? == 0 ]]; then
echo "[$federatedIdentityName] federated identity credential successfully created in the [$aksResourceGroupName] resource group"
else
echo "Failed to create [$federatedIdentityName] federated identity credential in the [$aksResourceGroupName] resource group"
exit
fi
else
echo "[$federatedIdentityName] federated identity credential already exists in the [$aksResourceGroupName] resource group"
fi
09-deploy-app.sh
This script creates the Kubernetes config map, deployment, and service used by themagic8ballchatbot.
#!/bin/bash
# Variables
source ./00-variables.sh
# Attach ACR to AKS cluster
if [[ $attachAcr == true ]]; then
echo "Attaching ACR $acrName to AKS cluster $aksClusterName..."
az aks update \
--name $aksClusterName \
--resource-group $aksResourceGroupName \
--attach-acr $acrName
fi
# Check if namespace exists in the cluster
result=$(kubectl get namespace -o jsonpath="{.items[?(@.metadata.name=='$namespace')].metadata.name}")
if [[ -n $result ]]; then
echo "$namespace namespace already exists in the cluster"
else
echo "$namespace namespace does not exist in the cluster"
echo "creating $namespace namespace in the cluster..."
kubectl create namespace $namespace
fi
# Create config map
cat $configMapTemplate |
yq "(.data.TITLE)|="\""$title"\" |
yq "(.data.LABEL)|="\""$label"\" |
yq "(.data.TEMPERATURE)|="\""$temperature"\" |
yq "(.data.IMAGE_WIDTH)|="\""$imageWidth"\" |
yq "(.data.AZURE_OPENAI_TYPE)|="\""$openAiType"\" |
yq "(.data.AZURE_OPENAI_BASE)|="\""$openAiBase"\" |
yq "(.data.AZURE_OPENAI_MODEL)|="\""$openAiModel"\" |
yq "(.data.AZURE_OPENAI_DEPLOYMENT)|="\""$openAiDeployment"\" |
kubectl apply -n $namespace -f -
# Create deployment
cat $deploymentTemplate |
yq "(.spec.template.spec.containers[0].image)|="\""$image"\" |
yq "(.spec.template.spec.containers[0].imagePullPolicy)|="\""$imagePullPolicy"\" |
yq "(.spec.template.spec.serviceAccountName)|="\""$serviceAccountName"\" |
kubectl apply -n $namespace -f -
# Create deployment
kubectl apply -f $serviceTemplate -n $namespace
10-create-ingress.sh
This script creates the ingress object to expose the service via theNGINX Ingress Controller.
This script creates an A record in the Azure DNS Zone to expose the application via a given subdomain (e.g.,https://magic8ball.example.com).
# Variables
source ./00-variables.sh
# Retrieve the public IP address from the ingress
echo "Retrieving the external IP address from the [$ingressName] ingress..."
publicIpAddress=$(kubectl get ingress $ingressName -n $namespace -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
if [ -n $publicIpAddress ]; then
echo "[$publicIpAddress] external IP address of the application gateway ingress controller successfully retrieved from the [$ingressName] ingress"
else
echo "Failed to retrieve the external IP address of the application gateway ingress controller from the [$ingressName] ingress"
exit
fi
# Check if an A record for todolist subdomain exists in the DNS Zone
echo "Retrieving the A record for the [$subdomain] subdomain from the [$dnsZoneName] DNS zone..."
ipv4Address=$(az network dns record-set a list \
--zone-name $dnsZoneName \
--resource-group $dnsZoneResourceGroupName \
--query "[?name=='$subdomain'].arecords[].ipv4Address" \
--output tsv)
if [[ -n $ipv4Address ]]; then
echo "An A record already exists in [$dnsZoneName] DNS zone for the [$subdomain] subdomain with [$ipv4Address] IP address"
if [[ $ipv4Address == $publicIpAddress ]]; then
echo "The [$ipv4Address] ip address of the existing A record is equal to the ip address of the [$ingressName] ingress"
echo "No additional step is required"
exit
else
echo "The [$ipv4Address] ip address of the existing A record is different than the ip address of the [$ingressName] ingress"
fi
# Retrieving name of the record set relative to the zone
echo "Retrieving the name of the record set relative to the [$dnsZoneName] zone..."
recordSetName=$(az network dns record-set a list \
--zone-name $dnsZoneName \
--resource-group $dnsZoneResourceGroupName \
--query "[?name=='$subdomain'].name" \
--output name 2>/dev/null)
if [[ -n $recordSetName ]]; then
"[$recordSetName] record set name successfully retrieved"
else
"Failed to retrieve the name of the record set relative to the [$dnsZoneName] zone"
exit
fi
# Remove the a record
echo "Removing the A record from the record set relative to the [$dnsZoneName] zone..."
az network dns record-set a remove-record \
--ipv4-address $ipv4Address \
--record-set-name $recordSetName \
--zone-name $dnsZoneName \
--resource-group $dnsZoneResourceGroupName
if [[ $? == 0 ]]; then
echo "[$ipv4Address] ip address successfully removed from the [$recordSetName] record set"
else
echo "Failed to remove the [$ipv4Address] ip address from the [$recordSetName] record set"
exit
fi
fi
# Create the a record
echo "Creating an A record in [$dnsZoneName] DNS zone for the [$subdomain] subdomain with [$publicIpAddress] IP address..."
az network dns record-set a add-record \
--zone-name $dnsZoneName \
--resource-group $dnsZoneResourceGroupName \
--record-set-name $subdomain \
--ipv4-address $publicIpAddress 1>/dev/null
if [[ $? == 0 ]]; then
echo "A record for the [$subdomain] subdomain with [$publicIpAddress] IP address successfully created in [$dnsZoneName] DNS zone"
else
echo "Failed to create an A record for the $subdomain subdomain with [$publicIpAddress] IP address in [$dnsZoneName] DNS zone"
fi
The scripts used to deploy the YAML template use theyqtool to customize the manifests with the value of the variables defined in the00-variables.shfile. This tool is a lightweight and portable command-line YAML, JSON and XML processor that usesjqlike syntax but works with YAML files as well as json, xml, properties, csv and tsv. It doesn't yet support everything jq does - but it does support the most common operations and functions, and more is being added continuously.
YAML manifests
Below, you can read the YAML manifests used to deploy themagic8ballchatbot to AKS.
configmap.yml
The configmap.yml defines a value for the environment variables passed to the application container. The config map does not define any environment variable for the OpenAI key as the container.
These are the parameters defined by the configmap:
AZURE_OPENAI_TYPE: specifyazureif you want to let the application use the API key to authenticate against OpenAI. In this case, make sure to provide the Key in theAZURE_OPENAI_KEYenvironment variable. If you want to authenticate using an Azure AD security token, you need to specifyazure_adas a value. In this case, don't need to provide any value in theAZURE_OPENAI_KEYenvironment variable.
AZURE_OPENAI_BASE: the URL of your Azure OpenAI resource. If you use the API key to authenticate against OpenAI, you can specify the regional endpoint of your Azure OpenAI Service (e.g.,https://eastus.api.cognitive.microsoft.com/). If you instead plan to use Azure AD security tokens for authentication, you need to deploy your Azure OpenAI Service with a subdomain and specify the resource-specific endpoint url (e.g.,https://myopenai.openai.azure.com/).
AZURE_OPENAI_KEY: the key of your Azure OpenAI resource. If you setAZURE_OPENAI_TYPEtoazure_adyou can leave this parameter empty.
AZURE_OPENAI_DEPLOYMENT: the name of the ChatGPT deployment used by your Azure OpenAI resource, for examplegpt-35-turbo.
AZURE_OPENAI_MODEL: the name of the ChatGPT model used by your Azure OpenAI resource, for examplegpt-35-turbo.
TITLE: the title of the Streamlit app.
TEMPERATURE: the temperature used by the OpenAI API to generate the response.
SYSTEM: give the model instructions about how it should behave and any context it should reference when generating a response. Used to describe the assistant's personality.
deployment.yml Thedeployment.ymlmanifest is used create a Kubernetesdeploymentthat defines the application pods to create azure.workload.identity/use label is required in the pod template spec. Only pods with this label will be mutated by the azure-workload-identity mutating admission webhook to inject the Azure specific environment variables and the projected service account token volume.
The ingress object defines the following annotations:
cert-manager.io/cluster-issuer: specifies the name of a cert-manager.io ClusterIssuer to acquire the certificate required for this Ingress. It does not matter which namespace your Ingress resides, as ClusterIssuers are non-namespaced resources. In this sample, the cert-manager is instructed to use theletsencrypt-nginxClusterIssuer that you can create using the06-create-cluster-issuer.shscript.
"}},"componentScriptGroups({\"componentId\":\"custom.widget.MicrosoftFooter\"})":{"__typename":"ComponentScriptGroups","scriptGroups":{"__typename":"ComponentScriptGroupsDefinition","afterInteractive":{"__typename":"PageScriptGroupDefinition","group":"AFTER_INTERACTIVE","scriptIds":[]},"lazyOnLoad":{"__typename":"PageScriptGroupDefinition","group":"LAZY_ON_LOAD","scriptIds":[]}},"componentScripts":[]},"cachedText({\"lastModified\":\"1745505307000\",\"locale\":\"en-US\",\"namespaces\":[\"components/community/NavbarDropdownToggle\"]})":[{"__ref":"CachedAsset:text:en_US-components/community/NavbarDropdownToggle-1745505307000"}],"cachedText({\"lastModified\":\"1745505307000\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/users/UserAvatar\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/users/UserAvatar-1745505307000"}],"cachedText({\"lastModified\":\"1745505307000\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/ranks/UserRankLabel\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/ranks/UserRankLabel-1745505307000"}],"cachedText({\"lastModified\":\"1745505307000\",\"locale\":\"en-US\",\"namespaces\":[\"components/tags/TagView/TagViewChip\"]})":[{"__ref":"CachedAsset:text:en_US-components/tags/TagView/TagViewChip-1745505307000"}],"cachedText({\"lastModified\":\"1745505307000\",\"locale\":\"en-US\",\"namespaces\":[\"components/users/UserRegistrationDate\"]})":[{"__ref":"CachedAsset:text:en_US-components/users/UserRegistrationDate-1745505307000"}],"cachedText({\"lastModified\":\"1745505307000\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/nodes/NodeAvatar\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/nodes/NodeAvatar-1745505307000"}],"cachedText({\"lastModified\":\"1745505307000\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/nodes/NodeDescription\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/nodes/NodeDescription-1745505307000"}],"cachedText({\"lastModified\":\"1745505307000\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageListMenu\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageListMenu-1745505307000"}],"message({\"id\":\"message:3843200\"})":{"__ref":"BlogReplyMessage:message:3843200"},"message({\"id\":\"message:3843192\"})":{"__ref":"BlogReplyMessage:message:3843192"},"cachedText({\"lastModified\":\"1745505307000\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/nodes/NodeIcon\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/nodes/NodeIcon-1745505307000"}]},"Theme:customTheme1":{"__typename":"Theme","id":"customTheme1"},"User:user:-1":{"__typename":"User","id":"user:-1","uid":-1,"login":"Deleted","email":"","avatar":null,"rank":null,"kudosWeight":1,"registrationData":{"__typename":"RegistrationData","status":"ANONYMOUS","registrationTime":null,"confirmEmailStatus":false,"registrationAccessLevel":"VIEW","ssoRegistrationFields":[]},"ssoId":null,"profileSettings":{"__typename":"ProfileSettings","dateDisplayStyle":{"__typename":"InheritableStringSettingWithPossibleValues","key":"layout.friendly_dates_enabled","value":"false","localValue":"true","possibleValues":["true","false"]},"dateDisplayFormat":{"__typename":"InheritableStringSetting","key":"layout.format_pattern_date","value":"MMM dd yyyy","localValue":"MM-dd-yyyy"},"language":{"__typename":"InheritableStringSettingWithPossibleValues","key":"profile.language","value":"en-US","localValue":null,"possibleValues":["en-US","es-ES"]},"repliesSortOrder":{"__typename":"InheritableStringSettingWithPossibleValues","key":"config.user_replies_sort_order","value":"DEFAULT","localValue":"DEFAULT","possibleValues":["DEFAULT","LIKES","PUBLISH_TIME","REVERSE_PUBLISH_TIME"]}},"deleted":false},"CachedAsset:pages-1746563246387":{"__typename":"CachedAsset","id":"pages-1746563246387","value":[{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"BlogViewAllPostsPage","type":"BLOG","urlPath":"/category/:categoryId/blog/:boardId/all-posts/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"CasePortalPage","type":"CASE_PORTAL","urlPath":"/caseportal","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"CreateGroupHubPage","type":"GROUP_HUB","urlPath":"/groups/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"CaseViewPage","type":"CASE_DETAILS","urlPath":"/case/:caseId/:caseNumber","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"InboxPage","type":"COMMUNITY","urlPath":"/inbox","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"HelpFAQPage","type":"COMMUNITY","urlPath":"/help","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"IdeaMessagePage","type":"IDEA_POST","urlPath":"/idea/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"IdeaViewAllIdeasPage","type":"IDEA","urlPath":"/category/:categoryId/ideas/:boardId/all-ideas/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"LoginPage","type":"USER","urlPath":"/signin","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"BlogPostPage","type":"BLOG","urlPath":"/category/:categoryId/blogs/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"UserBlogPermissions.Page","type":"COMMUNITY","urlPath":"/c/user-blog-permissions/page","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"ThemeEditorPage","type":"COMMUNITY","urlPath":"/designer/themes","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"TkbViewAllArticlesPage","type":"TKB","urlPath":"/category/:categoryId/kb/:boardId/all-articles/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1730819800000,"localOverride":null,"page":{"id":"AllEvents","type":"CUSTOM","urlPath":"/Events","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"OccasionEditPage","type":"EVENT","urlPath":"/event/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"OAuthAuthorizationAllowPage","type":"USER","urlPath":"/auth/authorize/allow","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"PageEditorPage","type":"COMMUNITY","urlPath":"/designer/pages","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"PostPage","type":"COMMUNITY","urlPath":"/category/:categoryId/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"ForumBoardPage","type":"FORUM","urlPath":"/category/:categoryId/discussions/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"TkbBoardPage","type":"TKB","urlPath":"/category/:categoryId/kb/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"EventPostPage","type":"EVENT","urlPath":"/category/:categoryId/events/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"UserBadgesPage","type":"COMMUNITY","urlPath":"/users/:login/:userId/badges","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"GroupHubMembershipAction","type":"GROUP_HUB","urlPath":"/membership/join/:nodeId/:membershipType","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"MaintenancePage","type":"COMMUNITY","urlPath":"/maintenance","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"IdeaReplyPage","type":"IDEA_REPLY","urlPath":"/idea/:boardId/:messageSubject/:messageId/comments/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"UserSettingsPage","type":"USER","urlPath":"/mysettings/:userSettingsTab","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"GroupHubsPage","type":"GROUP_HUB","urlPath":"/groups","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"ForumPostPage","type":"FORUM","urlPath":"/category/:categoryId/discussions/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"OccasionRsvpActionPage","type":"OCCASION","urlPath":"/event/:boardId/:messageSubject/:messageId/rsvp/:responseType","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"VerifyUserEmailPage","type":"USER","urlPath":"/verifyemail/:userId/:verifyEmailToken","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"AllOccasionsPage","type":"OCCASION","urlPath":"/category/:categoryId/events/:boardId/all-events/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"EventBoardPage","type":"EVENT","urlPath":"/category/:categoryId/events/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"TkbReplyPage","type":"TKB_REPLY","urlPath":"/kb/:boardId/:messageSubject/:messageId/comments/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"IdeaBoardPage","type":"IDEA","urlPath":"/category/:categoryId/ideas/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"CommunityGuideLinesPage","type":"COMMUNITY","urlPath":"/communityguidelines","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"CaseCreatePage","type":"SALESFORCE_CASE_CREATION","urlPath":"/caseportal/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"TkbEditPage","type":"TKB","urlPath":"/kb/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"ForgotPasswordPage","type":"USER","urlPath":"/forgotpassword","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"IdeaEditPage","type":"IDEA","urlPath":"/idea/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"TagPage","type":"COMMUNITY","urlPath":"/tag/:tagName","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"BlogBoardPage","type":"BLOG","urlPath":"/category/:categoryId/blog/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"OccasionMessagePage","type":"OCCASION_TOPIC","urlPath":"/event/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"ManageContentPage","type":"COMMUNITY","urlPath":"/managecontent","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"ClosedMembershipNodeNonMembersPage","type":"GROUP_HUB","urlPath":"/closedgroup/:groupHubId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"CommunityPage","type":"COMMUNITY","urlPath":"/","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"ForumMessagePage","type":"FORUM_TOPIC","urlPath":"/discussions/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"IdeaPostPage","type":"IDEA","urlPath":"/category/:categoryId/ideas/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1730819800000,"localOverride":null,"page":{"id":"CommunityHub.Page","type":"CUSTOM","urlPath":"/Directory","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"BlogMessagePage","type":"BLOG_ARTICLE","urlPath":"/blog/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"RegistrationPage","type":"USER","urlPath":"/register","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"EditGroupHubPage","type":"GROUP_HUB","urlPath":"/group/:groupHubId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"ForumEditPage","type":"FORUM","urlPath":"/discussions/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"ResetPasswordPage","type":"USER","urlPath":"/resetpassword/:userId/:resetPasswordToken","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1730819800000,"localOverride":null,"page":{"id":"AllBlogs.Page","type":"CUSTOM","urlPath":"/blogs","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"TkbMessagePage","type":"TKB_ARTICLE","urlPath":"/kb/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"BlogEditPage","type":"BLOG","urlPath":"/blog/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"ManageUsersPage","type":"USER","urlPath":"/users/manage/:tab?/:manageUsersTab?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"ForumReplyPage","type":"FORUM_REPLY","urlPath":"/discussions/:boardId/:messageSubject/:messageId/replies/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"PrivacyPolicyPage","type":"COMMUNITY","urlPath":"/privacypolicy","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"NotificationPage","type":"COMMUNITY","urlPath":"/notifications","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"UserPage","type":"USER","urlPath":"/users/:login/:userId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"HealthCheckPage","type":"COMMUNITY","urlPath":"/health","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"OccasionReplyPage","type":"OCCASION_REPLY","urlPath":"/event/:boardId/:messageSubject/:messageId/comments/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"ManageMembersPage","type":"GROUP_HUB","urlPath":"/group/:groupHubId/manage/:tab?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"SearchResultsPage","type":"COMMUNITY","urlPath":"/search","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"BlogReplyPage","type":"BLOG_REPLY","urlPath":"/blog/:boardId/:messageSubject/:messageId/replies/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"GroupHubPage","type":"GROUP_HUB","urlPath":"/group/:groupHubId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"TermsOfServicePage","type":"COMMUNITY","urlPath":"/termsofservice","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"CategoryPage","type":"CATEGORY","urlPath":"/category/:categoryId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"ForumViewAllTopicsPage","type":"FORUM","urlPath":"/category/:categoryId/discussions/:boardId/all-topics/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"TkbPostPage","type":"TKB","urlPath":"/category/:categoryId/kbs/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1746563246387,"localOverride":null,"page":{"id":"GroupHubPostPage","type":"GROUP_HUB","urlPath":"/group/:groupHubId/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"}],"localOverride":false},"CachedAsset:text:en_US-components/context/AppContext/AppContextProvider-0":{"__typename":"CachedAsset","id":"text:en_US-components/context/AppContext/AppContextProvider-0","value":{"noCommunity":"Cannot find community","noUser":"Cannot find current user","noNode":"Cannot find node with id {nodeId}","noMessage":"Cannot find message with id {messageId}","userBanned":"We're sorry, but you have been banned from using this site.","userBannedReason":"You have been banned for the following reason: {reason}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/common/Loading/LoadingDot-0":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/common/Loading/LoadingDot-0","value":{"title":"Loading..."},"localOverride":false},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/cmstNC05WEo0blc\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/cmstNC05WEo0blc","height":512,"width":512,"mimeType":"image/png"},"Rank:rank:4":{"__typename":"Rank","id":"rank:4","position":6,"name":"Microsoft","color":"333333","icon":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/cmstNC05WEo0blc\"}"},"rankStyle":"OUTLINE"},"User:user:988334":{"__typename":"User","id":"user:988334","uid":988334,"login":"paolosalvatori","deleted":false,"avatar":{"__typename":"UserAvatar","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/dS05ODgzMzQtMzg1MjYyaTE4QTU5MkIyQUVCMkM0MDE"},"rank":{"__ref":"Rank:rank:4"},"email":"","messagesCount":67,"biography":null,"topicsCount":30,"kudosReceivedCount":160,"kudosGivenCount":29,"kudosWeight":1,"registrationData":{"__typename":"RegistrationData","status":null,"registrationTime":"2021-03-05T07:56:49.951-08:00","confirmEmailStatus":null},"followersCount":null,"solutionsCount":0,"entityType":"USER","eventPath":"community:gxcuf89792/user:988334"},"Category:category:FastTrack":{"__typename":"Category","id":"category:FastTrack","entityType":"CATEGORY","displayId":"FastTrack","nodeType":"category","depth":3,"title":"Microsoft FastTrack","shortTitle":"Microsoft FastTrack","parent":{"__ref":"Category:category:products-services"}},"Category:category:top":{"__typename":"Category","id":"category:top","entityType":"CATEGORY","displayId":"top","nodeType":"category","depth":0,"title":"Top","shortTitle":"Top"},"Category:category:communities":{"__typename":"Category","id":"category:communities","entityType":"CATEGORY","displayId":"communities","nodeType":"category","depth":1,"parent":{"__ref":"Category:category:top"},"title":"Communities","shortTitle":"Communities"},"Category:category:products-services":{"__typename":"Category","id":"category:products-services","entityType":"CATEGORY","displayId":"products-services","nodeType":"category","depth":2,"parent":{"__ref":"Category:category:communities"},"title":"Products","shortTitle":"Products"},"Blog:board:FastTrackforAzureBlog":{"__typename":"Blog","id":"board:FastTrackforAzureBlog","entityType":"BLOG","displayId":"FastTrackforAzureBlog","nodeType":"board","depth":4,"conversationStyle":"BLOG","repliesProperties":{"__typename":"RepliesProperties","sortOrder":"REVERSE_PUBLISH_TIME","repliesFormat":"threaded"},"tagProperties":{"__typename":"TagNodeProperties","tagsEnabled":{"__typename":"PolicyResult","failureReason":null}},"requireTags":true,"tagType":"PRESET_ONLY","description":"","title":"FastTrack for Azure","shortTitle":"FastTrack for Azure","parent":{"__ref":"Category:category:FastTrack"},"ancestors":{"__typename":"CoreNodeConnection","edges":[{"__typename":"CoreNodeEdge","node":{"__ref":"Community:community:gxcuf89792"}},{"__typename":"CoreNodeEdge","node":{"__ref":"Category:category:communities"}},{"__typename":"CoreNodeEdge","node":{"__ref":"Category:category:products-services"}},{"__typename":"CoreNodeEdge","node":{"__ref":"Category:category:FastTrack"}}]},"userContext":{"__typename":"NodeUserContext","canAddAttachments":false,"canUpdateNode":false,"canPostMessages":false,"isSubscribed":false},"theme":{"__ref":"Theme:customTheme1"},"boardPolicies":{"__typename":"BoardPolicies","canViewSpamDashBoard":{"__typename":"PolicyResult","failureReason":{"__typename":"FailureReason","message":"error.lithium.policies.feature.moderation_spam.action.access_spam_quarantine.allowed.accessDenied","key":"error.lithium.policies.feature.moderation_spam.action.access_spam_quarantine.allowed.accessDenied","args":[]}},"canArchiveMessage":{"__typename":"PolicyResult","failureReason":{"__typename":"FailureReason","message":"error.lithium.policies.content_archivals.enable_content_archival_settings.accessDenied","key":"error.lithium.policies.content_archivals.enable_content_archival_settings.accessDenied","args":[]}},"canPublishArticleOnCreate":{"__typename":"PolicyResult","failureReason":{"__typename":"FailureReason","message":"error.lithium.policies.forums.policy_can_publish_on_create_workflow_action.accessDenied","key":"error.lithium.policies.forums.policy_can_publish_on_create_workflow_action.accessDenied","args":[]}}},"eventPath":"category:FastTrack/category:products-services/category:communities/community:gxcuf89792board:FastTrackforAzureBlog/"},"BlogTopicMessage:message:3834619":{"__typename":"BlogTopicMessage","uid":3834619,"subject":"Deploy and run a Azure OpenAI/ChatGPT application on AKS via Bicep","id":"message:3834619","revisionNum":5,"repliesCount":2,"author":{"__ref":"User:user:988334"},"depth":0,"hasGivenKudo":false,"board":{"__ref":"Blog:board:FastTrackforAzureBlog"},"conversation":{"__ref":"Conversation:conversation:3834619"},"messagePolicies":{"__typename":"MessagePolicies","canPublishArticleOnEdit":{"__typename":"PolicyResult","failureReason":{"__typename":"FailureReason","message":"error.lithium.policies.forums.policy_can_publish_on_edit_workflow_action.accessDenied","key":"error.lithium.policies.forums.policy_can_publish_on_edit_workflow_action.accessDenied","args":[]}},"canModerateSpamMessage":{"__typename":"PolicyResult","failureReason":{"__typename":"FailureReason","message":"error.lithium.policies.feature.moderation_spam.action.moderate_entity.allowed.accessDenied","key":"error.lithium.policies.feature.moderation_spam.action.moderate_entity.allowed.accessDenied","args":[]}}},"contentWorkflow":{"__typename":"ContentWorkflow","state":"PUBLISH","scheduledPublishTime":null,"scheduledTimezone":null,"userContext":{"__typename":"MessageWorkflowContext","canSubmitForReview":null,"canEdit":false,"canRecall":null,"canSubmitForPublication":null,"canReturnToAuthor":null,"canPublish":null,"canReturnToReview":null,"canSchedule":false},"shortScheduledTimezone":null},"readOnly":false,"editFrozen":false,"moderationData":{"__ref":"ModerationData:moderation_data:3834619"},"teaser":"
AI applications perform tasks such assummarizing articles, writing stories, and engaging in long conversations with chatbots. This is made possible bylarge language models (LLMs)like OpenAI ChatGPT, which are deep learning algorithms capable of recognizing, summarizing, translating, predicting, and generating text and other content. LLMs leverage the knowledge acquired from extensive datasets, enabling them to perform tasks beyond teaching AI human languages. These models have succeeded in diverse domains, including understanding proteins, writing software code, and more. Apart from their applications in natural language processing, such as translation, chatbots, and AI assistants, large language models are also extensively employed in healthcare, software development, and various other fields.
\n
For more information on Azure OpenAI Service and Large Language Models (LLMs), see the following articles:
API Server VNET Integrationallows you to enable network communication between the API server and the cluster nodes without requiring a private link or tunnel. AKS clusters with API Server VNET integration provide a series of advantages, for example, they can have public network access or private cluster mode enabled or disabled without redeploying the cluster. For more information, seeCreate an Azure Kubernetes Service cluster with API Server VNet Integration.
\n
Azure NAT Gatewayto manage outbound connections initiated by AKS-hosted workloads.
\n
Event-driven Autoscaling (KEDA) add-onis a single-purpose and lightweight component that strives to make application autoscaling simple and is a CNCF Incubation project.
\n
Dapr extension for Azure Kubernetes Service (AKS)allows you to installDapr, a portable, event-driven runtime that simplifies building resilient, stateless, and stateful applications that run on the cloud and edge and embrace the diversity of languages and developer frameworks. With its sidecar architecture, Dapr helps you tackle the challenges that come with building microservices and keeps your code platform agnostic.
Vertical Pod Autoscalingallows you to automatically sets resource requests and limits on containers per workload based on past usage. VPA makes certain pods are scheduled onto nodes that have the required CPU and memory resources. For more information, seeKubernetes Vertical Pod Autoscaling.
Image Cleanerto clean up stale images on your Azure Kubernetes Service cluster.
\n
Open Service Mesh add-onis a lightweight, extensible, cloud-native service mesh that allows you to uniformly manage, secure, and get out-of-the-box observability features for highly dynamic microservice environments. Bicep modules allow to install the Open Service Mesh add-on as an alternative to the Istio Service Mesh add-on.NOTE: you can't install both the Open Service Mesh add-on and Istio Service Mesh add-on on the same AKS cluster.
The Bicep modules deploy the following Azure resources:
\n
\n
\n
Microsoft.CognitiveServices/accounts: anAzure OpenAI Servicewith aGPT-3.5model used by the chatbot application. Azure OpenAI Service gives customers advanced language AI with OpenAI GPT-4, GPT-3, Codex, and DALL-E models with Azure's security and enterprise promise. Azure OpenAI co-develops the APIs with OpenAI, ensuring compatibility and a smooth transition from one to the other.
Microsoft.Compute/virtualMachines: Bicep modules can optionally create a jump-box virtual machine to manage the private AKS cluster.
\n
Microsoft.Network/bastionHosts: a separate Azure Bastion is deployed in the AKS cluster virtual network to provide SSH connectivity to both agent nodes and virtual machines.
\n
Microsoft.Network/natGateways: a bring-your-own (BYO)Azure NAT Gatewayto manage outbound connections initiated by AKS-hosted workloads. The NAT Gateway is associated to theSystemSubnet,UserSubnet, andPodSubnetsubnets. TheoutboundTypeproperty of the cluster is set touserAssignedNatGatewayto specify that a BYO NAT Gateway is used for outbound connections. NOTE: you can update theoutboundTypeafter cluster creation and this will deploy or remove resources as required to put the cluster into the new egress configuration. For more information, seeUpdating outboundType after cluster creation.
\n
Microsoft.Storage/storageAccounts: this storage account is used to store the boot diagnostics logs of both the service provider and service consumer virtual machines. Boot Diagnostics is a debugging feature that allows you to view console output and screenshots to diagnose virtual machine status.
\n
Microsoft.ContainerRegistry/registries: an Azure Container Registry (ACR) to build, store, and manage container images and artifacts in a private registry for all container deployments.
Microsoft.Resources/deploymentScripts: a deployment script is used to run theinstall-nginx-via-helm-and-create-sa.shBash script which creates the namespace and servicea account for the sample application and installs the following packages to the AKS cluster viaHelm. For more information on deployment scripts, seeUse deployment scripts in Bicep\n
NOTE You can find thearchitecture.vsdxfile used for the diagram under thevisiofolder.
\n
\n
\n
What is Bicep?
\n
Bicepis a domain-specific language (DSL) that uses a declarative syntax to deploy Azure resources. It provides concise syntax, reliable type safety, and support for code reuse. Bicep offers the best authoring experience for your infrastructure-as-code solutions in Azure.
\n
\n
What is Azure OpenAI Service?
\n
TheAzure OpenAI Serviceis a platform offered by Microsoft Azure that provides cognitive services powered byOpenAImodels. One of the models available through this service is theChatGPTmodel, designed for interactive conversational tasks. It allows developers to integrate natural language understanding and generation capabilities into their applications.
\n
Azure OpenAI Service provides REST API access to OpenAI's powerful language models, including theGPT-3,CodexandEmbeddingsmodel series. In addition, the newGPT-4andChatGPTmodel series have now reached general availability. These models can be easily adapted to your specific task, including but not limited to content generation, summarization, semantic search, and natural language to code translation. Users can access the service through REST APIs, Python SDK, or our web-based interface in the Azure OpenAI Studio.
\n
TheChat Completion API, part of the Azure OpenAI Service, provides a dedicated interface for interacting with theChatGPTandGPT-4 models. This API is currently in preview and is the preferred method for accessing these models. The GPT-4 models can only be accessed through this API.
\n
GPT-3,GPT-3.5, andGPT-4models from OpenAI are prompt-based. With prompt-based models, the user interacts with the model by entering a text prompt, to which the model responds with a text completion. This completion is the model’s continuation of the input text. While these models are extremely powerful, their behavior is also very sensitive to the prompt. This makes prompt construction a critical skill to develop. For more information, seeIntroduction to prompt engineering.
\n
Prompt construction can be complex. In practice, the prompt acts to configure the model weights to complete the desired task, but it's more of an art than a science, often requiring experience and intuition to craft a successful prompt. The goal of this article is to help get you started with this learning process. It attempts to capture general concepts and patterns that apply to all GPT models. However, it's essential to understand that each model behaves differently, so the learnings may not apply equally to all models.
\n
Prompt engineering refers to creating instructions called prompts for Large Language Models (LLMs), such as OpenAI’s ChatGPT. With the immense potential of LLMs to solve a wide range of tasks, leveraging prompt engineering can empower us to save significant time and facilitate the development of impressive applications. It holds the key to unleashing the full capabilities of these huge models, transforming how we interact and benefit from them. For more information, seePrompt engineering techniques.
\n
\n
Deploy the Bicep modules
\n
You can deploy the Bicep modules in thebicepfolder using thedeploy.shBash script in the same folder. Specify a value for the following parameters in thedeploy.shscript andmain.parameters.jsonparameters file before deploying the Bicep modules.
\n
\n
\n
prefix: specifies a prefix for all the Azure resources.
\n
authenticationType: specifies the type of authentication when accessing the Virtual Machine.sshPublicKeyis the recommended value. Allowed values:sshPublicKeyandpassword.
\n
vmAdminUsername: specifies the name of the administrator account of the virtual machine.
\n
vmAdminPasswordOrKey: specifies the SSH Key or password for the virtual machine.
\n
aksClusterSshPublicKey: specifies the SSH Key or password for AKS cluster agent nodes.
\n
aadProfileAdminGroupObjectIDs: when deploying an AKS cluster with Azure AD and Azure RBAC integration, this array parameter contains the list of Azure AD group object IDs that will have the admin role of the cluster.
\n
keyVaultObjectIds: Specifies the object ID of the service principals to configure in Key Vault access policies.
The following table contains the code from theopenAi.bicepBicep module used to deploy theAzure OpenAI Service.
\n
\n
// Parameters\n@description('Specifies the name of the Azure OpenAI resource.')\nparam name string = 'aks-${uniqueString(resourceGroup().id)}'\n\n@description('Specifies the resource model definition representing SKU.')\nparam sku object = {\n name: 'S0'\n}\n\n@description('Specifies the identity of the OpenAI resource.')\nparam identity object = {\n type: 'SystemAssigned'\n}\n\n@description('Specifies the location.')\nparam location string = resourceGroup().location\n\n@description('Specifies the resource tags.')\nparam tags object\n\n@description('Specifies an optional subdomain name used for token-based authentication.')\nparam customSubDomainName string = ''\n\n@description('Specifies whether or not public endpoint access is allowed for this account..')\n@allowed([\n 'Enabled'\n 'Disabled'\n])\nparam publicNetworkAccess string = 'Enabled'\n\n@description('Specifies the OpenAI deployments to create.')\nparam deployments array = [\n {\n name: 'text-embedding-ada-002'\n version: '2'\n raiPolicyName: ''\n capacity: 1\n scaleType: 'Standard'\n }\n {\n name: 'gpt-35-turbo'\n version: '0301'\n raiPolicyName: ''\n capacity: 1\n scaleType: 'Standard'\n }\n {\n name: 'text-davinci-003'\n version: '1'\n raiPolicyName: ''\n capacity: 1\n scaleType: 'Standard'\n }\n]\n\n@description('Specifies the workspace id of the Log Analytics used to monitor the Application Gateway.')\nparam workspaceId string\n\n// Variables\nvar diagnosticSettingsName = 'diagnosticSettings'\nvar openAiLogCategories = [\n 'Audit'\n 'RequestResponse'\n 'Trace'\n]\nvar openAiMetricCategories = [\n 'AllMetrics'\n]\nvar openAiLogs = [for category in openAiLogCategories: {\n category: category\n enabled: true\n}]\nvar openAiMetrics = [for category in openAiMetricCategories: {\n category: category\n enabled: true\n}]\n\n// Resources\nresource openAi 'Microsoft.CognitiveServices/accounts@2022-12-01' = {\n name: name\n location: location\n sku: sku\n kind: 'OpenAI'\n identity: identity\n tags: tags\n properties: {\n customSubDomainName: customSubDomainName\n publicNetworkAccess: publicNetworkAccess\n }\n}\n\nresource model 'Microsoft.CognitiveServices/accounts/deployments@2022-12-01' = \n [for deployment in deployments: {\n name: deployment.name\n parent: openAi\n properties: {\n model: {\n format: 'OpenAI'\n name: deployment.name\n version: deployment.version\n }\n raiPolicyName: deployment.raiPolicyName\n scaleSettings: {\n capacity: deployment.capacity\n scaleType: deployment.scaleType\n }\n }\n}]\n\nresource openAiDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {\n name: diagnosticSettingsName\n scope: openAi\n properties: {\n workspaceId: workspaceId\n logs: openAiLogs\n metrics: openAiMetrics\n }\n}\n\n// Outputs\noutput id string = openAi.id\noutput name string = openAi.name
\n
\n
Azure Cognitive Services use custom subdomain names for each resource created through theAzure portal,Azure Cloud Shell,Azure CLI,Bicep,Azure Resource Manager (ARM), orTerraform. Unlike regional endpoints, common for all customers in a specific Azure region, custom subdomain names are unique to the resource. Custom subdomain names are required to enable authentication features like Azure Active Directory (Azure AD). We need to specify a custom subdomain for ourAzure OpenAI Service,as our chatbot application will use an Azure AD security token to access it. By default, themain.bicepmodule sets the value of thecustomSubDomainNameparameter to the lowercase name of the Azure OpenAI resource. For more information on custom subdomains, seeCustom subdomain names for Cognitive Services.
\n
\n
This bicep module allows you to pass an array containing the definition of one or more model deployments in thedeploymentsparameter. For more information on model deployments, seeCreate a resource and deploy a model using Azure OpenAI
\n
\n
AKS Cluster Bicep module
\n
TheaksCluster.bicepBicep module is used to deploy theAzure Kubernetes Service(AKS)cluster. In particular, the following code snippet creates the user-defined managed identity used by the chatbot to acquire a security token from Azure Active Directory viaAzure AD workload identity. When the booleanopenAiEnabledparameter istrue, the Bicep code performs the following steps:
\n
\n
\n
Creates a new user-defined managed identity.
\n
Assign the new managed identity to the Cognitive Services User role with the resource group as a scope.
\n
Federate the managed identity with the service account used by the chatbot. The following information are necessary to create the federated identity credentials:\n
\n
The Kubernetes service account name.
\n
The Kubernetes namespace that will host the chatbot application.
...\n@description('Specifies the name of the user-defined managed identity used by the application that uses Azure AD workload identity to authenticate against Azure OpenAI.')\nparam workloadManagedIdentityName string\n\n@description('Specifies whether creating the Azure OpenAi resource or not.')\nparam openAiEnabled bool = false\n...\n// This user-defined managed identity used by the workload to connect to the Azure OpenAI resource with a security token issued by Azure Active Directory\nresource workloadManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = if (openAiEnabled) {\n name: workloadManagedIdentityName\n location: location\n tags: tags\n}\n\n// Assign the Cognitive Services User role to the user-defined managed identity used by workloads\nresource cognitiveServicesUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (openAiEnabled) {\n name: guid(workloadManagedIdentity.id, cognitiveServicesUserRoleDefinitionId)\n scope: resourceGroup()\n properties: {\n roleDefinitionId: cognitiveServicesUserRoleDefinitionId\n principalId: workloadManagedIdentity.properties.principalId\n principalType: 'ServicePrincipal'\n }\n}\n\n// Create federated identity for the user-defined managed identity used by the workload\nresource federatedIdentityCredentials 'Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials@2023-01-31' = {\n name: letterCaseType == 'UpperCamelCase' ? '${toUpper(first(namespace))}${toLower(substring(namespace, 1, length(namespace) - 1))}FederatedIdentity' : letterCaseType == 'CamelCase' ? '${toLower(namespace)}FederatedIdentity' : '${toLower(namespace)}-federated-identity'\n parent: workloadManagedIdentity\n properties: {\n issuer: aksCluster.properties.oidcIssuerProfile.issuerURL\n subject: 'system:serviceaccount:${namespace}:${serviceAccountName}'\n audiences: [\n 'api://AzureADTokenExchange'\n ]\n }\n}\n...\n// Output\noutput id string = aksCluster.id\noutput name string = aksCluster.name\noutput issuerUrl string = aksCluster.properties.oidcIssuerProfile.issuerURL\noutput workloadManagedIdentityClientId string = workloadManagedIdentity.properties.clientId
\n
\n
Validate the deployment
\n
Open the Azure Portal, and navigate to the resource group. Open the Azure Open AI Service resource, navigate toKeys and Endpoint, and check that the endpoint contains a custom subdomain rather than the regional Cognitive Services endpoint.
\n
\n
\n
\n
\n
Open to the<Prefix>WorkloadManagedIdentitymanaged identity, navigate to theFederated credentials, and verify that the federated identity credentials for themagic8ball-saservice account were created correctly, as shown in the following picture.
\n
\n
\n
\n
Use Azure AD workload identity with Azure Kubernetes Service (AKS)
\n
Workloads deployed on an Azure Kubernetes Services (AKS) cluster require Azure Active Directory (Azure AD) application credentials or managed identities to access Azure AD-protected resources, such as Azure Key Vault and Microsoft Graph. Azure AD workload identity integrates with the capabilities native to Kubernetes to federate with external identity providers.
The sample makes use of aDeployment Scriptto run theinstall-nginx-via-helm-and-create-sa.shBash script that creates the namespace and service account for the sample application and installs the following packages to the AKS cluster viaHelm. For more information on deployment scripts, seeUse deployment scripts in Bicep.
Theinstall-nginx-via-helm-and-create-sa.shBash script returns the following outputs to the deployment script:
\n
\n
\n
Namespace hosting the chatbot sample. You can change the defaultmagic8ballnamespace by assigning a different value to thenamespaceparameter of themain.bicepmodule.
The application is contained in a single file calledapp.py. The application makes use of the following libraries:
\n
\n
\n
OpenAPI: The OpenAI Python library provides convenient access to the OpenAI API from applications written in Python. It includes a pre-defined set of classes for API resources that initialize themselves dynamically from API responses, making it compatible with a wide range of versions of the OpenAI API. You can find usage examples for the OpenAI Python library in ourAPI referenceand theOpenAI Cookbook.
\n
Azure Identity: The Azure Identity library providesAzure Active Directory (Azure AD)token authentication support across the Azure SDK. It provides a set ofTokenCredentialimplementations, which can be used to construct Azure SDK clients that support Azure AD token authentication.
\n
Streamlit: Streamlit is an open-source Python library that makes it easy to create and share beautiful, custom web apps for machine learning and data science. You can build and deploy powerful data apps in just a few minutes. For more information, seeStreamlit documentation.
\n
Streamlit-chat: a Streamlit component that provides a configurable user interface for chatbot applications.
\n
Dotenv: Python-dotenv reads key-value pairs from a .env file and can set them as environment variables. It helps in the development of applications following the12-factorprinciples.
\n
\n
Therequirements.txtfile under thescriptsfolder contains the list of packages used by theapp.pyapplication that you can restore using the following command:
\n
\n
pip install -r requirements.txt --upgrade
\n
\n
The following table contains the code of theapp.pychatbot:
\n
\n
# Import packages\nimport os\nimport sys\nimport time\nimport openai\nimport logging\nimport streamlit as st\nfrom streamlit_chat import message\nfrom azure.identity import DefaultAzureCredential\nfrom dotenv import load_dotenv\nfrom dotenv import dotenv_values\n\n# Load environment variables from .env file\nif os.path.exists(\".env\"):\n load_dotenv(override=True)\n config = dotenv_values(\".env\")\n\n# Read environment variables\nassistan_profile = \"\"\"\nYou are the infamous Magic 8 Ball. You need to randomly reply to any question with one of the following answers:\n\n- It is certain.\n- It is decidedly so.\n- Without a doubt.\n- Yes definitely.\n- You may rely on it.\n- As I see it, yes.\n- Most likely.\n- Outlook good.\n- Yes.\n- Signs point to yes.\n- Reply hazy, try again.\n- Ask again later.\n- Better not tell you now.\n- Cannot predict now.\n- Concentrate and ask again.\n- Don't count on it.\n- My reply is no.\n- My sources say no.\n- Outlook not so good.\n- Very doubtful.\n\nAdd a short comment in a pirate style at the end! Follow your heart and be creative! \nFor mor information, see https://en.wikipedia.org/wiki/Magic_8_Ball\n\"\"\"\ntitle = os.environ.get(\"TITLE\", \"Magic 8 Ball\")\ntext_input_label = os.environ.get(\"TEXT_INPUT_LABEL\", \"Pose your question and cross your fingers!\")\nimage_file_name = os.environ.get(\"IMAGE_FILE_NAME\", \"magic8ball.png\")\nimage_width = int(os.environ.get(\"IMAGE_WIDTH\", 80))\ntemperature = float(os.environ.get(\"TEMPERATURE\", 0.9))\nsystem = os.environ.get(\"SYSTEM\", assistan_profile)\napi_base = os.getenv(\"AZURE_OPENAI_BASE\")\napi_key = os.getenv(\"AZURE_OPENAI_KEY\")\napi_type = os.environ.get(\"AZURE_OPENAI_TYPE\", \"azure\")\napi_version = os.environ.get(\"AZURE_OPENAI_VERSION\", \"2023-05-15\")\nengine = os.getenv(\"AZURE_OPENAI_DEPLOYMENT\")\nmodel = os.getenv(\"AZURE_OPENAI_MODEL\")\n\n# Configure OpenAI\nopenai.api_type = api_type\nopenai.api_version = api_version\nopenai.api_base = api_base \n\n# Set default Azure credential\ndefault_credential = DefaultAzureCredential() if openai.api_type == \"azure_ad\" else None\n\n# Configure a logger\nlogging.basicConfig(stream = sys.stdout, \n format = '[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s',\n level = logging.INFO)\nlogger = logging.getLogger(__name__)\n\n# Log variables\nlogger.info(f\"title: {title}\")\nlogger.info(f\"text_input_label: {text_input_label}\")\nlogger.info(f\"image_file_name: {image_file_name}\")\nlogger.info(f\"image_width: {image_width}\")\nlogger.info(f\"temperature: {temperature}\")\nlogger.info(f\"system: {system}\")\nlogger.info(f\"api_base: {api_base}\")\nlogger.info(f\"api_key: {api_key}\")\nlogger.info(f\"api_type: {api_type}\")\nlogger.info(f\"api_version: {api_version}\")\nlogger.info(f\"engine: {engine}\")\nlogger.info(f\"model: {model}\")\n\n# Authenticate to Azure OpenAI\nif openai.api_type == \"azure\":\n openai.api_key = api_key\nelif openai.api_type == \"azure_ad\":\n openai_token = default_credential.get_token(\"https://cognitiveservices.azure.com/.default\")\n openai.api_key = openai_token.token\n if 'openai_token' not in st.session_state:\n st.session_state['openai_token'] = openai_token\nelse:\n logger.error(\"Invalid API type. Please set the AZURE_OPENAI_TYPE environment variable to azure or azure_ad.\")\n raise ValueError(\"Invalid API type. Please set the AZURE_OPENAI_TYPE environment variable to azure or azure_ad.\")\n\n# Customize Streamlit UI using CSS\nst.markdown(\"\"\"\n<style>\n\ndiv.stButton > button:first-child {\n background-color: #eb5424;\n color: white;\n font-size: 20px;\n font-weight: bold;\n border-radius: 0.5rem;\n padding: 0.5rem 1rem;\n border: none;\n box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);\n width: 300 px;\n height: 42px;\n transition: all 0.2s ease-in-out;\n} \n\ndiv.stButton > button:first-child:hover {\n transform: translateY(-3px);\n box-shadow: 0 1rem 2rem rgba(0,0,0,0.15);\n}\n\ndiv.stButton > button:first-child:active {\n transform: translateY(-1px);\n box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);\n}\n\ndiv.stButton > button:focus:not(:focus-visible) {\n color: #FFFFFF;\n}\n\n@media only screen and (min-width: 768px) {\n /* For desktop: */\n div {\n font-family: 'Roboto', sans-serif;\n }\n\n div.stButton > button:first-child {\n background-color: #eb5424;\n color: white;\n font-size: 20px;\n font-weight: bold;\n border-radius: 0.5rem;\n padding: 0.5rem 1rem;\n border: none;\n box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);\n width: 300 px;\n height: 42px;\n transition: all 0.2s ease-in-out;\n position: relative;\n bottom: -32px;\n right: 0px;\n } \n\n div.stButton > button:first-child:hover {\n transform: translateY(-3px);\n box-shadow: 0 1rem 2rem rgba(0,0,0,0.15);\n }\n\n div.stButton > button:first-child:active {\n transform: translateY(-1px);\n box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);\n }\n\n div.stButton > button:focus:not(:focus-visible) {\n color: #FFFFFF;\n }\n\n input {\n border-radius: 0.5rem;\n padding: 0.5rem 1rem;\n border: none;\n box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);\n transition: all 0.2s ease-in-out;\n height: 40px;\n }\n}\n</style>\n\"\"\", unsafe_allow_html=True)\n\n# Initialize Streamlit session state\nif 'prompts' not in st.session_state:\n st.session_state['prompts'] = [{\"role\": \"system\", \"content\": system}]\n\nif 'generated' not in st.session_state:\n st.session_state['generated'] = []\n\nif 'past' not in st.session_state:\n st.session_state['past'] = []\n\n# Refresh the OpenAI security token every 45 minutes\ndef refresh_openai_token():\n if st.session_state['openai_token'].expires_on < int(time.time()) - 45 * 60:\n st.session_state['openai_token'] = default_credential.get_token(\"https://cognitiveservices.azure.com/.default\")\n openai.api_key = st.session_state['openai_token'].token\n\n# Send user prompt to Azure OpenAI \ndef generate_response(prompt):\n try:\n st.session_state['prompts'].append({\"role\": \"user\", \"content\": prompt})\n\n if openai.api_type == \"azure_ad\":\n refresh_openai_token()\n\n completion = openai.ChatCompletion.create(\n engine = engine,\n model = model,\n messages = st.session_state['prompts'],\n temperature = temperature,\n )\n \n message = completion.choices[0].message.content\n return message\n except Exception as e:\n logging.exception(f\"Exception in generate_response: {e}\")\n\n# Reset Streamlit session state to start a new chat from scratch\ndef new_click():\n st.session_state['prompts'] = [{\"role\": \"system\", \"content\": system}]\n st.session_state['past'] = []\n st.session_state['generated'] = []\n st.session_state['user'] = \"\"\n\n# Handle on_change event for user input\ndef user_change():\n # Avoid handling the event twice when clicking the Send button\n chat_input = st.session_state['user']\n st.session_state['user'] = \"\"\n if (chat_input == '' or\n (len(st.session_state['past']) > 0 and chat_input == st.session_state['past'][-1])):\n return\n \n # Generate response invoking Azure OpenAI LLM\n if chat_input != '':\n output = generate_response(chat_input)\n \n # store the output\n st.session_state['past'].append(chat_input)\n st.session_state['generated'].append(output)\n st.session_state['prompts'].append({\"role\": \"assistant\", \"content\": output})\n\n# Create a 2-column layout. Note: Streamlit columns do not properly render on mobile devices.\n# For more information, see https://github.com/streamlit/streamlit/issues/5003\ncol1, col2 = st.columns([1, 7])\n\n# Display the robot image\nwith col1:\n st.image(image = os.path.join(\"images\", image_file_name), width = image_width)\n\n# Display the title\nwith col2:\n st.title(title)\n\n# Create a 3-column layout. Note: Streamlit columns do not properly render on mobile devices.\n# For more information, see https://github.com/streamlit/streamlit/issues/5003\ncol3, col4, col5 = st.columns([7, 1, 1])\n\n# Create text input in column 1\nwith col3:\n user_input = st.text_input(text_input_label, key = \"user\", on_change = user_change)\n\n# Create send button in column 2\nwith col4:\n st.button(label = \"Send\")\n\n# Create new button in column 3\nwith col5:\n st.button(label = \"New\", on_click = new_click)\n\n# Display the chat history in two separate tabs\n# - normal: display the chat history as a list of messages using the streamlit_chat message() function \n# - rich: display the chat history as a list of messages using the Streamlit markdown() function\nif st.session_state['generated']:\n tab1, tab2 = st.tabs([\"normal\", \"rich\"])\n with tab1:\n for i in range(len(st.session_state['generated']) - 1, -1, -1):\n message(st.session_state['past'][i], is_user = True, key = str(i) + '_user', avatar_style = \"fun-emoji\", seed = \"Nala\")\n message(st.session_state['generated'][i], key = str(i), avatar_style = \"bottts\", seed = \"Fluffy\")\n with tab2:\n for i in range(len(st.session_state['generated']) - 1, -1, -1):\n st.markdown(st.session_state['past'][i])\n st.markdown(st.session_state['generated'][i])
def generate_response(prompt):\n try:\n st.session_state['prompts'].append({\"role\": \"user\", \"content\": prompt})\n\n if openai.api_type == \"azure_ad\":\n refresh_openai_token()\n\n completion = openai.ChatCompletion.create(\n engine = engine,\n model = model,\n messages = st.session_state['prompts'],\n temperature = temperature,\n )\n \n message = completion.choices[0].message.content\n return message\n except Exception as e:\n logging.exception(f\"Exception in generate_response: {e}\")
\n
\n
OpenAI trained the ChatGPT and GPT-4 models to accept input formatted as a conversation. The messages parameter takes an array of dictionaries with a conversation organized by role or message: system, user, and assistant. The format of a basic Chat Completion is as follows:
\n
\n
{\"role\": \"system\", \"content\": \"Provide some context and/or instructions to the model\"},\n{\"role\": \"user\", \"content\": \"The users messages goes here\"},\n{\"role\": \"assistant\", \"content\": \"The response message goes here.\"}
\n
\n
Thesystemrole, also known as the system message, is included at the beginning of the array. This message provides the initial instructions for the model. You can provide various information in the system role, including:
\n
\n
\n
A brief description of the assistant
\n
Personality traits of the assistant
\n
Instructions or rules you would like the assistant to follow
\n
Data or information needed for the model, such as relevant questions from an FAQ
\n
You can customize the system role for your use case or include basic instructions.
\n
\n
\n
Thesystemrole or message is optional, but it's recommended to at least include a basic one to get the best results. Theuserrole or message represents an input or inquiry from the user, while theassistantmessage corresponds to the response generated by the GPT API. This dialog exchange aims to simulate a human-like conversation, where the user message initiates the interaction and the assistant message provides a relevant and informative answer. This context helps the chat model generate a more appropriate response later on. The last user message refers to the prompt currently requested. For more information, seeLearn how to work with the ChatGPT and GPT-4 models.
\n
\n
Application Configuration
\n
Make sure to provide a value for the following environment variables when testing theapp.pyPython app locally, for example in Visual Studio Code. You can eventually define environment variables in a.envfile in the same folder as theapp.pyfile.
\n
\n
\n
AZURE_OPENAI_TYPE: specifyazureif you want to let the application use the API key to authenticate against OpenAI. In this case, make sure to provide the Key in theAZURE_OPENAI_KEYenvironment variable. If you want to authenticate using an Azure AD security token, you need to specifyazure_adas a value. In this case, don't need to provide any value in theAZURE_OPENAI_KEYenvironment variable.
\n
AZURE_OPENAI_BASE: the URL of your Azure OpenAI resource. If you use the API key to authenticate against OpenAI, you can specify the regional endpoint of your Azure OpenAI Service (e.g.,https://eastus.api.cognitive.microsoft.com/). If you instead plan to use Azure AD security tokens for authentication, you need to deploy your Azure OpenAI Service with a subdomain and specify the resource-specific endpoint url (e.g.,https://myopenai.openai.azure.com/).
\n
AZURE_OPENAI_KEY: the key of your Azure OpenAI resource.
\n
AZURE_OPENAI_DEPLOYMENT: the name of the ChatGPT deployment used by your Azure OpenAI resource, for examplegpt-35-turbo.
\n
AZURE_OPENAI_MODEL: the name of the ChatGPT model used by your Azure OpenAI resource, for examplegpt-35-turbo.
\n
TITLE: the title of the Streamlit app.
\n
TEMPERATURE: the temperature used by the OpenAI API to generate the response.
\n
SYSTEM: give the model instructions about how it should behave and any context it should reference when generating a response. Used to describe the assistant's personality.
\n
\n
\n
When deploying the application to Azure Kubernetes Service (AKS), these values are provided in a KubernetesConfigMap. For more information, see the next section.
\n
\n
OpenAI Library
\n
To use theopenailibrary with Microsoft Azure endpoints, you need to set theapi_type,api_baseandapi_versionin addition to theapi_key. Theapi_typemust be set to 'azure' and the others correspond to the properties of your endpoint. In addition, the deployment name must be passed as the engine parameter. To use OpenAI Key to authenticate to your Azure endpoint, you need to set theapi_typetoazureand pass the OpenAI Key toapi_key.
To use Microsoft Active Directory to authenticate to your Azure endpoint, you need to set theapi_typetoazure_adand pass the acquired credential token toapi_key. The rest of the parameters must be set as specified in the previous section.
You can use two different authentication methods in themagic8ballchatbot application:
\n
\n
\n
API key: set theAZURE_OPENAI_TYPEenvironment variable to azure and the AZURE_OPENAI_KEY environment variable to the key of your Azure OpenAI resource. You can use the regional endpoint, such ashttps://eastus.api.cognitive.microsoft.com/, in theAZURE_OPENAI_BASEenvironment variable, to connect to the Azure OpenAI resource.
\n
Azure Active Directory: set theAZURE_OPENAI_TYPEenvironment variable to azure_ad and use a service principal or managed identity with theDefaultAzureCredentialobject to acquire a security token from Azure Active Directory. For more information on the DefaultAzureCredential in Python, seeAuthenticate Python apps to Azure services by using the Azure SDK for Python. Make sure to assign theCognitive Services Userrole to the service principal or managed identity used to authenticate to your Azure OpenAI Service. For more information, seeHow to configure Azure OpenAI Service with managed identities. If you want to use Azure AD integrated security, you need to create a custom subdomain for your Azure OpenAI resource and use the specific endpoint containing the custom domain, such ashttps://myopenai.openai.azure.com/where myopenai is the custom subdomain. If you specify the regional endpoint, you get an error like the following:Subdomain does not map to a resource. Hence, pass the custom domain endpoint in the AZURE_OPENAI_BASE environment variable. In this case, you also need to refresh the security token periodically.
\n
\n
\n
Build the container image
\n
You can build the container image using the01-build-docker-image.shin thescriptsfolder.
\n
\n
Dockerfile
\n
\n
# app/Dockerfile\n\n# # Stage 1 - Install build dependencies\n\n# A Dockerfile must start with a FROM instruction which sets the base image for the container.\n# The Python images come in many flavors, each designed for a specific use case.\n# The python:3.11-slim image is a good base image for most applications.\n# It is a minimal image built on top of Debian Linux and includes only the necessary packages to run Python.\n# The slim image is a good choice because it is small and contains only the packages needed to run Python.\n# For more information, see: \n# * https://hub.docker.com/_/python \n# * https://docs.streamlit.io/knowledge-base/tutorials/deploy/docker\nFROM python:3.11-slim AS builder\n\n# The WORKDIR instruction sets the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions that follow it in the Dockerfile.\n# If the WORKDIR doesn’t exist, it will be created even if it’s not used in any subsequent Dockerfile instruction.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#workdir\nWORKDIR /app\n\n# Set environment variables. \n# The ENV instruction sets the environment variable <key> to the value <value>.\n# This value will be in the environment of all “descendant” Dockerfile commands and can be replaced inline in many as well.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#env\nENV PYTHONDONTWRITEBYTECODE 1\nENV PYTHONUNBUFFERED 1\n\n# Install git so that we can clone the app code from a remote repo using the RUN instruction.\n# The RUN comand has 2 forms:\n# * RUN <command> (shell form, the command is run in a shell, which by default is /bin/sh -c on Linux or cmd /S /C on Windows)\n# * RUN [\"executable\", \"param1\", \"param2\"] (exec form)\n# The RUN instruction will execute any commands in a new layer on top of the current image and commit the results. \n# The resulting committed image will be used for the next step in the Dockerfile.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#run\nRUN apt-get update && apt-get install -y \\\n build-essential \\\n curl \\\n software-properties-common \\\n git \\\n && rm -rf /var/lib/apt/lists/*\n\n# Create a virtualenv to keep dependencies together\nRUN python -m venv /opt/venv\nENV PATH=\"/opt/venv/bin:$PATH\"\n\n# Clone the requirements.txt which contains dependencies to WORKDIR\n# COPY has two forms:\n# * COPY <src> <dest> (this copies the files from the local machine to the container's own filesystem)\n# * COPY [\"<src>\",... \"<dest>\"] (this form is required for paths containing whitespace)\n# For more information, see: https://docs.docker.com/engine/reference/builder/#copy\nCOPY requirements.txt .\n\n# Install the Python dependencies\nRUN pip install --no-cache-dir --no-deps -r requirements.txt\n\n# Stage 2 - Copy only necessary files to the runner stage\n\n# The FROM instruction initializes a new build stage for the application\nFROM python:3.11-slim\n\n# Sets the working directory to /app\nWORKDIR /app\n\n# Copy the virtual environment from the builder stage\nCOPY --from=builder /opt/venv /opt/venv\n\n# Set environment variables\nENV PATH=\"/opt/venv/bin:$PATH\"\n\n# Clone the app.py containing the application code\nCOPY app.py .\n\n# Copy the images folder to WORKDIR\n# The ADD instruction copies new files, directories or remote file URLs from <src> and adds them to the filesystem of the image at the path <dest>.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#add\nADD images ./images\n\n# The EXPOSE instruction informs Docker that the container listens on the specified network ports at runtime.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#expose\nEXPOSE 8501\n\n# The HEALTHCHECK instruction has two forms:\n# * HEALTHCHECK [OPTIONS] CMD command (check container health by running a command inside the container)\n# * HEALTHCHECK NONE (disable any healthcheck inherited from the base image)\n# The HEALTHCHECK instruction tells Docker how to test a container to check that it is still working. \n# This can detect cases such as a web server that is stuck in an infinite loop and unable to handle new connections, \n# even though the server process is still running. For more information, see: https://docs.docker.com/engine/reference/builder/#healthcheck\nHEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health\n\n# The ENTRYPOINT instruction has two forms:\n# * ENTRYPOINT [\"executable\", \"param1\", \"param2\"] (exec form, preferred)\n# * ENTRYPOINT command param1 param2 (shell form)\n# The ENTRYPOINT instruction allows you to configure a container that will run as an executable.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#entrypoint\nENTRYPOINT [\"streamlit\", \"run\", \"app.py\", \"--server.port=8501\", \"--server.address=0.0.0.0\"]
Before running any script, make sure to customize the value of the variables inside the00-variables.shfile. This file is embedded in all the scripts and contains the following variables:
\n
\n
# Variables\nacrName=\"CoralAcr\"\nacrResourceGrougName=\"CoralRG\"\nlocation=\"FranceCentral\"\nattachAcr=false\nimageName=\"magic8ball\"\ntag=\"v2\"\ncontainerName=\"magic8ball\"\nimage=\"$acrName.azurecr.io/$imageName:$tag\"\nimagePullPolicy=\"IfNotPresent\" # Always, Never, IfNotPresent\nmanagedIdentityName=\"OpenAiManagedIdentity\"\nfederatedIdentityName=\"Magic8BallFederatedIdentity\"\n\n# Azure Subscription and Tenant\nsubscriptionId=$(az account show --query id --output tsv)\nsubscriptionName=$(az account show --query name --output tsv)\ntenantId=$(az account show --query tenantId --output tsv)\n\n# Parameters\ntitle=\"Magic 8 Ball\"\nlabel=\"Pose your question and cross your fingers!\"\ntemperature=\"0.9\"\nimageWidth=\"80\"\n\n# OpenAI\nopenAiName=\"CoralOpenAi \"\nopenAiResourceGroupName=\"CoralRG\"\nopenAiType=\"azure_ad\"\nopenAiBase=\"https://coralopenai.openai.azure.com/\"\nopenAiModel=\"gpt-35-turbo\"\nopenAiDeployment=\"gpt-35-turbo\"\n\n# Nginx Ingress Controller\nnginxNamespace=\"ingress-basic\"\nnginxRepoName=\"ingress-nginx\"\nnginxRepoUrl=\"https://kubernetes.github.io/ingress-nginx\"\nnginxChartName=\"ingress-nginx\"\nnginxReleaseName=\"nginx-ingress\"\nnginxReplicaCount=3\n\n# Certificate Manager\ncmNamespace=\"cert-manager\"\ncmRepoName=\"jetstack\"\ncmRepoUrl=\"https://charts.jetstack.io\"\ncmChartName=\"cert-manager\"\ncmReleaseName=\"cert-manager\"\n\n# Cluster Issuer\nemail=\"paolos@microsoft.com\"\nclusterIssuerName=\"letsencrypt-nginx\"\nclusterIssuerTemplate=\"cluster-issuer.yml\"\n\n# AKS Cluster\naksClusterName=\"CoralAks\"\naksResourceGroupName=\"CoralRG\"\n\n# Sample Application\nnamespace=\"magic8ball\"\nserviceAccountName=\"magic8ball-sa\"\ndeploymentTemplate=\"deployment.yml\"\nserviceTemplate=\"service.yml\"\nconfigMapTemplate=\"configMap.yml\"\nsecretTemplate=\"secret.yml\"\n\n# Ingress and DNS\ningressTemplate=\"ingress.yml\"\ningressName=\"magic8ball-ingress\"\ndnsZoneName=\"babosbird.com\"\ndnsZoneResourceGroupName=\"DnsResourceGroup\"\nsubdomain=\"magic8ball\"\nhost=\"$subdomain.$dnsZoneName\"
\n
\n
Upload Docker container image to Azure Container Registry (ACR)
\n
You can push the Docker container image to Azure Container Registry (ACR) using the03-push-docker-image.shscript in thescriptsfolder.
\n
\n
#!/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Login to ACR\naz acr login --name $acrName \n\n# Retrieve ACR login server. Each container image needs to be tagged with the loginServer name of the registry. \nloginServer=$(az acr show --name $acrName --query loginServer --output tsv)\n\n# Tag the local image with the loginServer of ACR\ndocker tag ${imageName,,}:$tag $loginServer/${imageName,,}:$tag\n\n# Push latest container image to ACR\ndocker push $loginServer/${imageName,,}:$tag
\n
\n
Deployment Scripts
\n
If you deployed the Azure infrastructure using the Bicep modules provided with this sample, you only need to deploy the application using the following scripts and YAML templates in thescriptsfolder.
\n
\n
\n
09-deploy-app.sh
\n
10-create-ingress.sh
\n
11-configure-dns.sh
\n
configMap.yml
\n
deployment.yml
\n
ingress.yml
\n
service.yml
\n
\n
\n
If you instead want to deploy the application in your AKS cluster, you can use the following scripts to configure your environment.
\n
\n
04-create-nginx-ingress-controller.sh
\n
The scriptinstalls theNGINX Ingress Controllerusing Helm.
\n
\n
#!/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Use Helm to deploy an NGINX ingress controller\nresult=$(helm list -n $nginxNamespace | grep $nginxReleaseName | awk '{print $1}')\n\nif [[ -n $result ]]; then\n echo \"[$nginxReleaseName] ingress controller already exists in the [$nginxNamespace] namespace\"\nelse\n # Check if the ingress-nginx repository is not already added\n result=$(helm repo list | grep $nginxRepoName | awk '{print $1}')\n\n if [[ -n $result ]]; then\n echo \"[$nginxRepoName] Helm repo already exists\"\n else\n # Add the ingress-nginx repository\n echo \"Adding [$nginxRepoName] Helm repo...\"\n helm repo add $nginxRepoName $nginxRepoUrl\n fi\n\n # Update your local Helm chart repository cache\n echo 'Updating Helm repos...'\n helm repo update\n\n # Deploy NGINX ingress controller\n echo \"Deploying [$nginxReleaseName] NGINX ingress controller to the [$nginxNamespace] namespace...\"\n helm install $nginxReleaseName $nginxRepoName/$nginxChartName \\\n --create-namespace \\\n --namespace $nginxNamespace \\\n --set controller.config.enable-modsecurity=true \\\n --set controller.config.enable-owasp-modsecurity-crs=true \\\n --set controller.config.modsecurity-snippet=\\\n'SecRuleEngine On\nSecRequestBodyAccess On\nSecAuditLog /dev/stdout\nSecAuditLogFormat JSON\nSecAuditEngine RelevantOnly\nSecRule REMOTE_ADDR \"@ipMatch 127.0.0.1\" \"id:87,phase:1,pass,nolog,ctl:ruleEngine=Off\"' \\\n --set controller.metrics.enabled=true \\\n --set controller.metrics.serviceMonitor.enabled=true \\\n --set controller.metrics.serviceMonitor.additionalLabels.release=\"prometheus\" \\\n --set controller.nodeSelector.\"kubernetes\\.io/os\"=linux \\\n --set controller.replicaCount=$replicaCount \\\n --set defaultBackend.nodeSelector.\"kubernetes\\.io/os\"=linux \\\n --set controller.service.annotations.\"service\\.beta\\.kubernetes\\.io/azure-load-balancer-health-probe-request-path\"=/healthz\nfi
\n
\n
05-install-cert-manager.sh This script installs thecert-managerusing Helm.
\n
\n
#/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Check if the ingress-nginx repository is not already added\nresult=$(helm repo list | grep $cmRepoName | awk '{print $1}')\n\nif [[ -n $result ]]; then\n echo \"[$cmRepoName] Helm repo already exists\"\nelse\n # Add the Jetstack Helm repository\n echo \"Adding [$cmRepoName] Helm repo...\"\n helm repo add $cmRepoName $cmRepoUrl\nfi\n\n# Update your local Helm chart repository cache\necho 'Updating Helm repos...'\nhelm repo update\n\n# Install cert-manager Helm chart\nresult=$(helm list -n $cmNamespace | grep $cmReleaseName | awk '{print $1}')\n\nif [[ -n $result ]]; then\n echo \"[$cmReleaseName] cert-manager already exists in the $cmNamespace namespace\"\nelse\n # Install the cert-manager Helm chart\n echo \"Deploying [$cmReleaseName] cert-manager to the $cmNamespace namespace...\"\n helm install $cmReleaseName $cmRepoName/$cmChartName \\\n --create-namespace \\\n --namespace $cmNamespace \\\n --set installCRDs=true \\\n --set nodeSelector.\"kubernetes\\.io/os\"=linux\nfi
\n
\n
06-create-cluster-issuer.sh
\n
This script creates a cluster issuer for theNGINX Ingress Controllerbased on theLet's EncryptACME certificate issuer.
This script creates the managed identity used by themagic8ballchatbot and assigns it theCognitive Services Userrole on the Azure OpenAI Service.
\n
\n
#!/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Check if the user-assigned managed identity already exists\necho \"Checking if [$managedIdentityName] user-assigned managed identity actually exists in the [$aksResourceGroupName] resource group...\"\n\naz identity show \\\n --name $managedIdentityName \\\n --resource-group $aksResourceGroupName &>/dev/null\n\nif [[ $? != 0 ]]; then\n echo \"No [$managedIdentityName] user-assigned managed identity actually exists in the [$aksResourceGroupName] resource group\"\n echo \"Creating [$managedIdentityName] user-assigned managed identity in the [$aksResourceGroupName] resource group...\"\n\n # Create the user-assigned managed identity\n az identity create \\\n --name $managedIdentityName \\\n --resource-group $aksResourceGroupName \\\n --location $location \\\n --subscription $subscriptionId 1>/dev/null\n\n if [[ $? == 0 ]]; then\n echo \"[$managedIdentityName] user-assigned managed identity successfully created in the [$aksResourceGroupName] resource group\"\n else\n echo \"Failed to create [$managedIdentityName] user-assigned managed identity in the [$aksResourceGroupName] resource group\"\n exit\n fi\nelse\n echo \"[$managedIdentityName] user-assigned managed identity already exists in the [$aksResourceGroupName] resource group\"\nfi\n\n# Retrieve the clientId of the user-assigned managed identity\necho \"Retrieving clientId for [$managedIdentityName] managed identity...\"\nclientId=$(az identity show \\\n --name $managedIdentityName \\\n --resource-group $aksResourceGroupName \\\n --query clientId \\\n --output tsv)\n\nif [[ -n $clientId ]]; then\n echo \"[$clientId] clientId for the [$managedIdentityName] managed identity successfully retrieved\"\nelse\n echo \"Failed to retrieve clientId for the [$managedIdentityName] managed identity\"\n exit\nfi\n\n# Retrieve the principalId of the user-assigned managed identity\necho \"Retrieving principalId for [$managedIdentityName] managed identity...\"\nprincipalId=$(az identity show \\\n --name $managedIdentityName \\\n --resource-group $aksResourceGroupName \\\n --query principalId \\\n --output tsv)\n\nif [[ -n $principalId ]]; then\n echo \"[$principalId] principalId for the [$managedIdentityName] managed identity successfully retrieved\"\nelse\n echo \"Failed to retrieve principalId for the [$managedIdentityName] managed identity\"\n exit\nfi\n\n# Get the resource id of the Azure OpenAI resource\nopenAiId=$(az cognitiveservices account show \\\n --name $openAiName \\\n --resource-group $openAiResourceGroupName \\\n --query id \\\n --output tsv)\n\nif [[ -n $openAiId ]]; then\n echo \"Resource id for the [$openAiName] Azure OpenAI resource successfully retrieved\"\nelse\n echo \"Failed to the resource id for the [$openAiName] Azure OpenAI resource\"\n exit -1\nfi\n\n# Assign the Cognitive Services User role on the Azure OpenAI resource to the managed identity\nrole=\"Cognitive Services User\"\necho \"Checking if the [$managedIdentityName] managed identity has been assigned to [$role] role with [$openAiName] Azure OpenAI resource as a scope...\"\ncurrent=$(az role assignment list \\\n --assignee $principalId \\\n --scope $openAiId \\\n --query \"[?roleDefinitionName=='$role'].roleDefinitionName\" \\\n --output tsv 2>/dev/null)\n\nif [[ $current == $role ]]; then\n echo \"[$managedIdentityName] managed identity is already assigned to the [\"$current\"] role with [$openAiName] Azure OpenAI resource as a scope\"\nelse\n echo \"[$managedIdentityName] managed identity is not assigned to the [$role] role with [$openAiName] Azure OpenAI resource as a scope\"\n echo \"Assigning the [$role] role to the [$managedIdentityName] managed identity with [$openAiName] Azure OpenAI resource as a scope...\"\n\n az role assignment create \\\n --assignee $principalId \\\n --role \"$role\" \\\n --scope $openAiId 1>/dev/null\n\n if [[ $? == 0 ]]; then\n echo \"[$managedIdentityName] managed identity successfully assigned to the [$role] role with [$openAiName] Azure OpenAI resource as a scope\"\n else\n echo \"Failed to assign the [$managedIdentityName] managed identity to the [$role] role with [$openAiName] Azure OpenAI resource as a scope\"\n exit\n fi\nfi
\n
\n
08-create-service-account.sh
\n
This script creates the namespace and service account for themagic8ballchatbot and federate the service account with the user-defined managed identity created in the previous step.
\n
\n
#!/bin/bash\n\n# Variables for the user-assigned managed identity\nsource ./00-variables.sh\n\n# Check if the namespace already exists\nresult=$(kubectl get namespace -o 'jsonpath={.items[?(@.metadata.name==\"'$namespace'\")].metadata.name'})\n\nif [[ -n $result ]]; then\n echo \"[$namespace] namespace already exists\"\nelse\n # Create the namespace for your ingress resources\n echo \"[$namespace] namespace does not exist\"\n echo \"Creating [$namespace] namespace...\"\n kubectl create namespace $namespace\nfi\n\n# Check if the service account already exists\nresult=$(kubectl get sa -n $namespace -o 'jsonpath={.items[?(@.metadata.name==\"'$serviceAccountName'\")].metadata.name'})\n\nif [[ -n $result ]]; then\n echo \"[$serviceAccountName] service account already exists\"\nelse\n # Retrieve the resource id of the user-assigned managed identity\n echo \"Retrieving clientId for [$managedIdentityName] managed identity...\"\n managedIdentityClientId=$(az identity show \\\n --name $managedIdentityName \\\n --resource-group $aksResourceGroupName \\\n --query clientId \\\n --output tsv)\n\n if [[ -n $managedIdentityClientId ]]; then\n echo \"[$managedIdentityClientId] clientId for the [$managedIdentityName] managed identity successfully retrieved\"\n else\n echo \"Failed to retrieve clientId for the [$managedIdentityName] managed identity\"\n exit\n fi\n\n # Create the service account\n echo \"[$serviceAccountName] service account does not exist\"\n echo \"Creating [$serviceAccountName] service account...\"\n cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n annotations:\n azure.workload.identity/client-id: $managedIdentityClientId\n azure.workload.identity/tenant-id: $tenantId\n labels:\n azure.workload.identity/use: \"true\"\n name: $serviceAccountName\n namespace: $namespace\nEOF\nfi\n\n# Show service account YAML manifest\necho \"Service Account YAML manifest\"\necho \"-----------------------------\"\nkubectl get sa $serviceAccountName -n $namespace -o yaml\n\n# Check if the federated identity credential already exists\necho \"Checking if [$federatedIdentityName] federated identity credential actually exists in the [$aksResourceGroupName] resource group...\"\n\naz identity federated-credential show \\\n --name $federatedIdentityName \\\n --resource-group $aksResourceGroupName \\\n --identity-name $managedIdentityName &>/dev/null\n\nif [[ $? != 0 ]]; then\n echo \"No [$federatedIdentityName] federated identity credential actually exists in the [$aksResourceGroupName] resource group\"\n\n # Get the OIDC Issuer URL\n aksOidcIssuerUrl=\"$(az aks show \\\n --only-show-errors \\\n --name $aksClusterName \\\n --resource-group $aksResourceGroupName \\\n --query oidcIssuerProfile.issuerUrl \\\n --output tsv)\"\n\n # Show OIDC Issuer URL\n if [[ -n $aksOidcIssuerUrl ]]; then\n echo \"The OIDC Issuer URL of the $aksClusterName cluster is $aksOidcIssuerUrl\"\n fi\n\n echo \"Creating [$federatedIdentityName] federated identity credential in the [$aksResourceGroupName] resource group...\"\n\n # Establish the federated identity credential between the managed identity, the service account issuer, and the subject.\n az identity federated-credential create \\\n --name $federatedIdentityName \\\n --identity-name $managedIdentityName \\\n --resource-group $aksResourceGroupName \\\n --issuer $aksOidcIssuerUrl \\\n --subject system:serviceaccount:$namespace:$serviceAccountName\n\n if [[ $? == 0 ]]; then\n echo \"[$federatedIdentityName] federated identity credential successfully created in the [$aksResourceGroupName] resource group\"\n else\n echo \"Failed to create [$federatedIdentityName] federated identity credential in the [$aksResourceGroupName] resource group\"\n exit\n fi\nelse\n echo \"[$federatedIdentityName] federated identity credential already exists in the [$aksResourceGroupName] resource group\"\nfi
\n
\n
09-deploy-app.sh
\n
This script creates the Kubernetes config map, deployment, and service used by themagic8ballchatbot.
\n
\n
#!/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Attach ACR to AKS cluster\nif [[ $attachAcr == true ]]; then\n echo \"Attaching ACR $acrName to AKS cluster $aksClusterName...\"\n az aks update \\\n --name $aksClusterName \\\n --resource-group $aksResourceGroupName \\\n --attach-acr $acrName\nfi\n\n# Check if namespace exists in the cluster\nresult=$(kubectl get namespace -o jsonpath=\"{.items[?(@.metadata.name=='$namespace')].metadata.name}\")\n\nif [[ -n $result ]]; then\n echo \"$namespace namespace already exists in the cluster\"\nelse\n echo \"$namespace namespace does not exist in the cluster\"\n echo \"creating $namespace namespace in the cluster...\"\n kubectl create namespace $namespace\nfi\n\n# Create config map\ncat $configMapTemplate |\n yq \"(.data.TITLE)|=\"\\\"\"$title\"\\\" |\n yq \"(.data.LABEL)|=\"\\\"\"$label\"\\\" |\n yq \"(.data.TEMPERATURE)|=\"\\\"\"$temperature\"\\\" |\n yq \"(.data.IMAGE_WIDTH)|=\"\\\"\"$imageWidth\"\\\" |\n yq \"(.data.AZURE_OPENAI_TYPE)|=\"\\\"\"$openAiType\"\\\" |\n yq \"(.data.AZURE_OPENAI_BASE)|=\"\\\"\"$openAiBase\"\\\" |\n yq \"(.data.AZURE_OPENAI_MODEL)|=\"\\\"\"$openAiModel\"\\\" |\n yq \"(.data.AZURE_OPENAI_DEPLOYMENT)|=\"\\\"\"$openAiDeployment\"\\\" |\n kubectl apply -n $namespace -f -\n\n# Create deployment\ncat $deploymentTemplate |\n yq \"(.spec.template.spec.containers[0].image)|=\"\\\"\"$image\"\\\" |\n yq \"(.spec.template.spec.containers[0].imagePullPolicy)|=\"\\\"\"$imagePullPolicy\"\\\" |\n yq \"(.spec.template.spec.serviceAccountName)|=\"\\\"\"$serviceAccountName\"\\\" |\n kubectl apply -n $namespace -f -\n\n# Create deployment\nkubectl apply -f $serviceTemplate -n $namespace
\n
\n
10-create-ingress.sh
\n
This script creates the ingress object to expose the service via theNGINX Ingress Controller.
\n
\n
#/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Create the ingress\necho \"[$ingressName] ingress does not exist\"\necho \"Creating [$ingressName] ingress...\"\ncat $ingressTemplate |\n yq \"(.spec.tls[0].hosts[0])|=\"\\\"\"$host\"\\\" |\n yq \"(.spec.rules[0].host)|=\"\\\"\"$host\"\\\" |\n kubectl apply -n $namespace -f -
\n
\n
11-configure-dns.sh
\n
This script creates an A record in the Azure DNS Zone to expose the application via a given subdomain (e.g.,https://magic8ball.example.com).
\n
\n
# Variables\nsource ./00-variables.sh\n\n# Retrieve the public IP address from the ingress\necho \"Retrieving the external IP address from the [$ingressName] ingress...\"\npublicIpAddress=$(kubectl get ingress $ingressName -n $namespace -o jsonpath='{.status.loadBalancer.ingress[0].ip}')\n\nif [ -n $publicIpAddress ]; then\n echo \"[$publicIpAddress] external IP address of the application gateway ingress controller successfully retrieved from the [$ingressName] ingress\"\nelse\n echo \"Failed to retrieve the external IP address of the application gateway ingress controller from the [$ingressName] ingress\"\n exit\nfi\n\n# Check if an A record for todolist subdomain exists in the DNS Zone\necho \"Retrieving the A record for the [$subdomain] subdomain from the [$dnsZoneName] DNS zone...\"\nipv4Address=$(az network dns record-set a list \\\n --zone-name $dnsZoneName \\\n --resource-group $dnsZoneResourceGroupName \\\n --query \"[?name=='$subdomain'].arecords[].ipv4Address\" \\\n --output tsv)\n\nif [[ -n $ipv4Address ]]; then\n echo \"An A record already exists in [$dnsZoneName] DNS zone for the [$subdomain] subdomain with [$ipv4Address] IP address\"\n\n if [[ $ipv4Address == $publicIpAddress ]]; then\n echo \"The [$ipv4Address] ip address of the existing A record is equal to the ip address of the [$ingressName] ingress\"\n echo \"No additional step is required\"\n exit\n else\n echo \"The [$ipv4Address] ip address of the existing A record is different than the ip address of the [$ingressName] ingress\"\n fi\n\n # Retrieving name of the record set relative to the zone\n echo \"Retrieving the name of the record set relative to the [$dnsZoneName] zone...\"\n\n recordSetName=$(az network dns record-set a list \\\n --zone-name $dnsZoneName \\\n --resource-group $dnsZoneResourceGroupName \\\n --query \"[?name=='$subdomain'].name\" \\\n --output name 2>/dev/null)\n\n if [[ -n $recordSetName ]]; then\n \"[$recordSetName] record set name successfully retrieved\"\n else\n \"Failed to retrieve the name of the record set relative to the [$dnsZoneName] zone\"\n exit\n fi\n\n # Remove the a record\n echo \"Removing the A record from the record set relative to the [$dnsZoneName] zone...\"\n\n az network dns record-set a remove-record \\\n --ipv4-address $ipv4Address \\\n --record-set-name $recordSetName \\\n --zone-name $dnsZoneName \\\n --resource-group $dnsZoneResourceGroupName\n\n if [[ $? == 0 ]]; then\n echo \"[$ipv4Address] ip address successfully removed from the [$recordSetName] record set\"\n else\n echo \"Failed to remove the [$ipv4Address] ip address from the [$recordSetName] record set\"\n exit\n fi\nfi\n\n# Create the a record\necho \"Creating an A record in [$dnsZoneName] DNS zone for the [$subdomain] subdomain with [$publicIpAddress] IP address...\"\naz network dns record-set a add-record \\\n --zone-name $dnsZoneName \\\n --resource-group $dnsZoneResourceGroupName \\\n --record-set-name $subdomain \\\n --ipv4-address $publicIpAddress 1>/dev/null\n\nif [[ $? == 0 ]]; then\n echo \"A record for the [$subdomain] subdomain with [$publicIpAddress] IP address successfully created in [$dnsZoneName] DNS zone\"\nelse\n echo \"Failed to create an A record for the $subdomain subdomain with [$publicIpAddress] IP address in [$dnsZoneName] DNS zone\"\nfi
\n
\n
The scripts used to deploy the YAML template use theyqtool to customize the manifests with the value of the variables defined in the00-variables.shfile. This tool is a lightweight and portable command-line YAML, JSON and XML processor that usesjqlike syntax but works with YAML files as well as json, xml, properties, csv and tsv. It doesn't yet support everything jq does - but it does support the most common operations and functions, and more is being added continuously.
\n
\n
YAML manifests
\n
Below, you can read the YAML manifests used to deploy themagic8ballchatbot to AKS.
\n
\n
configmap.yml
\n
The configmap.yml defines a value for the environment variables passed to the application container. The config map does not define any environment variable for the OpenAI key as the container.
These are the parameters defined by the configmap:
\n
\n
\n
AZURE_OPENAI_TYPE: specifyazureif you want to let the application use the API key to authenticate against OpenAI. In this case, make sure to provide the Key in theAZURE_OPENAI_KEYenvironment variable. If you want to authenticate using an Azure AD security token, you need to specifyazure_adas a value. In this case, don't need to provide any value in theAZURE_OPENAI_KEYenvironment variable.
\n
AZURE_OPENAI_BASE: the URL of your Azure OpenAI resource. If you use the API key to authenticate against OpenAI, you can specify the regional endpoint of your Azure OpenAI Service (e.g.,https://eastus.api.cognitive.microsoft.com/). If you instead plan to use Azure AD security tokens for authentication, you need to deploy your Azure OpenAI Service with a subdomain and specify the resource-specific endpoint url (e.g.,https://myopenai.openai.azure.com/).
\n
AZURE_OPENAI_KEY: the key of your Azure OpenAI resource. If you setAZURE_OPENAI_TYPEtoazure_adyou can leave this parameter empty.
\n
AZURE_OPENAI_DEPLOYMENT: the name of the ChatGPT deployment used by your Azure OpenAI resource, for examplegpt-35-turbo.
\n
AZURE_OPENAI_MODEL: the name of the ChatGPT model used by your Azure OpenAI resource, for examplegpt-35-turbo.
\n
TITLE: the title of the Streamlit app.
\n
TEMPERATURE: the temperature used by the OpenAI API to generate the response.
\n
SYSTEM: give the model instructions about how it should behave and any context it should reference when generating a response. Used to describe the assistant's personality.
\n
\n
\n
deployment.yml Thedeployment.ymlmanifest is used create a Kubernetesdeploymentthat defines the application pods to create azure.workload.identity/use label is required in the pod template spec. Only pods with this label will be mutated by the azure-workload-identity mutating admission webhook to inject the Azure specific environment variables and the projected service account token volume.
The ingress object defines the following annotations:
\n
\n
\n
cert-manager.io/cluster-issuer: specifies the name of a cert-manager.io ClusterIssuer to acquire the certificate required for this Ingress. It does not matter which namespace your Ingress resides, as ClusterIssuers are non-namespaced resources. In this sample, the cert-manager is instructed to use theletsencrypt-nginxClusterIssuer that you can create using the06-create-cluster-issuer.shscript.
AI applications perform tasks such assummarizing articles, writing stories, and engaging in long conversations with chatbots. This is made possible bylarge language models (LLMs)like OpenAI ChatGPT, which are deep learning algorithms capable of recognizing, summarizing, translating, predicting, and generating text and other content. LLMs leverage the knowledge acquired from extensive datasets, enabling them to perform tasks beyond teaching AI human languages. These models have succeeded in diverse domains, including understanding proteins, writing software code, and more. Apart from their applications in natural language processing, such as translation, chatbots, and AI assistants, large language models are also extensively employed in healthcare, software development, and various other fields.
\n
For more information on Azure OpenAI Service and Large Language Models (LLMs), see the following articles:
API Server VNET Integrationallows you to enable network communication between the API server and the cluster nodes without requiring a private link or tunnel. AKS clusters with API Server VNET integration provide a series of advantages, for example, they can have public network access or private cluster mode enabled or disabled without redeploying the cluster. For more information, seeCreate an Azure Kubernetes Service cluster with API Server VNet Integration.
\n
Azure NAT Gatewayto manage outbound connections initiated by AKS-hosted workloads.
\n
Event-driven Autoscaling (KEDA) add-onis a single-purpose and lightweight component that strives to make application autoscaling simple and is a CNCF Incubation project.
\n
Dapr extension for Azure Kubernetes Service (AKS)allows you to installDapr, a portable, event-driven runtime that simplifies building resilient, stateless, and stateful applications that run on the cloud and edge and embrace the diversity of languages and developer frameworks. With its sidecar architecture, Dapr helps you tackle the challenges that come with building microservices and keeps your code platform agnostic.
Vertical Pod Autoscalingallows you to automatically sets resource requests and limits on containers per workload based on past usage. VPA makes certain pods are scheduled onto nodes that have the required CPU and memory resources. For more information, seeKubernetes Vertical Pod Autoscaling.
Image Cleanerto clean up stale images on your Azure Kubernetes Service cluster.
\n
Open Service Mesh add-onis a lightweight, extensible, cloud-native service mesh that allows you to uniformly manage, secure, and get out-of-the-box observability features for highly dynamic microservice environments. Bicep modules allow to install the Open Service Mesh add-on as an alternative to the Istio Service Mesh add-on.NOTE: you can't install both the Open Service Mesh add-on and Istio Service Mesh add-on on the same AKS cluster.
The Bicep modules deploy the following Azure resources:
\n
\n
\n
Microsoft.CognitiveServices/accounts: anAzure OpenAI Servicewith aGPT-3.5model used by the chatbot application. Azure OpenAI Service gives customers advanced language AI with OpenAI GPT-4, GPT-3, Codex, and DALL-E models with Azure's security and enterprise promise. Azure OpenAI co-develops the APIs with OpenAI, ensuring compatibility and a smooth transition from one to the other.
Microsoft.Compute/virtualMachines: Bicep modules can optionally create a jump-box virtual machine to manage the private AKS cluster.
\n
Microsoft.Network/bastionHosts: a separate Azure Bastion is deployed in the AKS cluster virtual network to provide SSH connectivity to both agent nodes and virtual machines.
\n
Microsoft.Network/natGateways: a bring-your-own (BYO)Azure NAT Gatewayto manage outbound connections initiated by AKS-hosted workloads. The NAT Gateway is associated to theSystemSubnet,UserSubnet, andPodSubnetsubnets. TheoutboundTypeproperty of the cluster is set touserAssignedNatGatewayto specify that a BYO NAT Gateway is used for outbound connections. NOTE: you can update theoutboundTypeafter cluster creation and this will deploy or remove resources as required to put the cluster into the new egress configuration. For more information, seeUpdating outboundType after cluster creation.
\n
Microsoft.Storage/storageAccounts: this storage account is used to store the boot diagnostics logs of both the service provider and service consumer virtual machines. Boot Diagnostics is a debugging feature that allows you to view console output and screenshots to diagnose virtual machine status.
\n
Microsoft.ContainerRegistry/registries: an Azure Container Registry (ACR) to build, store, and manage container images and artifacts in a private registry for all container deployments.
Microsoft.Resources/deploymentScripts: a deployment script is used to run theinstall-nginx-via-helm-and-create-sa.shBash script which creates the namespace and servicea account for the sample application and installs the following packages to the AKS cluster viaHelm. For more information on deployment scripts, seeUse deployment scripts in Bicep\n
NOTE You can find thearchitecture.vsdxfile used for the diagram under thevisiofolder.
\n
\n
\n
What is Bicep?
\n
Bicepis a domain-specific language (DSL) that uses a declarative syntax to deploy Azure resources. It provides concise syntax, reliable type safety, and support for code reuse. Bicep offers the best authoring experience for your infrastructure-as-code solutions in Azure.
\n
\n
What is Azure OpenAI Service?
\n
TheAzure OpenAI Serviceis a platform offered by Microsoft Azure that provides cognitive services powered byOpenAImodels. One of the models available through this service is theChatGPTmodel, designed for interactive conversational tasks. It allows developers to integrate natural language understanding and generation capabilities into their applications.
\n
Azure OpenAI Service provides REST API access to OpenAI's powerful language models, including theGPT-3,CodexandEmbeddingsmodel series. In addition, the newGPT-4andChatGPTmodel series have now reached general availability. These models can be easily adapted to your specific task, including but not limited to content generation, summarization, semantic search, and natural language to code translation. Users can access the service through REST APIs, Python SDK, or our web-based interface in the Azure OpenAI Studio.
\n
TheChat Completion API, part of the Azure OpenAI Service, provides a dedicated interface for interacting with theChatGPTandGPT-4 models. This API is currently in preview and is the preferred method for accessing these models. The GPT-4 models can only be accessed through this API.
\n
GPT-3,GPT-3.5, andGPT-4models from OpenAI are prompt-based. With prompt-based models, the user interacts with the model by entering a text prompt, to which the model responds with a text completion. This completion is the model’s continuation of the input text. While these models are extremely powerful, their behavior is also very sensitive to the prompt. This makes prompt construction a critical skill to develop. For more information, seeIntroduction to prompt engineering.
\n
Prompt construction can be complex. In practice, the prompt acts to configure the model weights to complete the desired task, but it's more of an art than a science, often requiring experience and intuition to craft a successful prompt. The goal of this article is to help get you started with this learning process. It attempts to capture general concepts and patterns that apply to all GPT models. However, it's essential to understand that each model behaves differently, so the learnings may not apply equally to all models.
\n
Prompt engineering refers to creating instructions called prompts for Large Language Models (LLMs), such as OpenAI’s ChatGPT. With the immense potential of LLMs to solve a wide range of tasks, leveraging prompt engineering can empower us to save significant time and facilitate the development of impressive applications. It holds the key to unleashing the full capabilities of these huge models, transforming how we interact and benefit from them. For more information, seePrompt engineering techniques.
\n
\n
Deploy the Bicep modules
\n
You can deploy the Bicep modules in thebicepfolder using thedeploy.shBash script in the same folder. Specify a value for the following parameters in thedeploy.shscript andmain.parameters.jsonparameters file before deploying the Bicep modules.
\n
\n
\n
prefix: specifies a prefix for all the Azure resources.
\n
authenticationType: specifies the type of authentication when accessing the Virtual Machine.sshPublicKeyis the recommended value. Allowed values:sshPublicKeyandpassword.
\n
vmAdminUsername: specifies the name of the administrator account of the virtual machine.
\n
vmAdminPasswordOrKey: specifies the SSH Key or password for the virtual machine.
\n
aksClusterSshPublicKey: specifies the SSH Key or password for AKS cluster agent nodes.
\n
aadProfileAdminGroupObjectIDs: when deploying an AKS cluster with Azure AD and Azure RBAC integration, this array parameter contains the list of Azure AD group object IDs that will have the admin role of the cluster.
\n
keyVaultObjectIds: Specifies the object ID of the service principals to configure in Key Vault access policies.
The following table contains the code from theopenAi.bicepBicep module used to deploy theAzure OpenAI Service.
\n
\n// Parameters\n@description('Specifies the name of the Azure OpenAI resource.')\nparam name string = 'aks-${uniqueString(resourceGroup().id)}'\n\n@description('Specifies the resource model definition representing SKU.')\nparam sku object = {\n name: 'S0'\n}\n\n@description('Specifies the identity of the OpenAI resource.')\nparam identity object = {\n type: 'SystemAssigned'\n}\n\n@description('Specifies the location.')\nparam location string = resourceGroup().location\n\n@description('Specifies the resource tags.')\nparam tags object\n\n@description('Specifies an optional subdomain name used for token-based authentication.')\nparam customSubDomainName string = ''\n\n@description('Specifies whether or not public endpoint access is allowed for this account..')\n@allowed([\n 'Enabled'\n 'Disabled'\n])\nparam publicNetworkAccess string = 'Enabled'\n\n@description('Specifies the OpenAI deployments to create.')\nparam deployments array = [\n {\n name: 'text-embedding-ada-002'\n version: '2'\n raiPolicyName: ''\n capacity: 1\n scaleType: 'Standard'\n }\n {\n name: 'gpt-35-turbo'\n version: '0301'\n raiPolicyName: ''\n capacity: 1\n scaleType: 'Standard'\n }\n {\n name: 'text-davinci-003'\n version: '1'\n raiPolicyName: ''\n capacity: 1\n scaleType: 'Standard'\n }\n]\n\n@description('Specifies the workspace id of the Log Analytics used to monitor the Application Gateway.')\nparam workspaceId string\n\n// Variables\nvar diagnosticSettingsName = 'diagnosticSettings'\nvar openAiLogCategories = [\n 'Audit'\n 'RequestResponse'\n 'Trace'\n]\nvar openAiMetricCategories = [\n 'AllMetrics'\n]\nvar openAiLogs = [for category in openAiLogCategories: {\n category: category\n enabled: true\n}]\nvar openAiMetrics = [for category in openAiMetricCategories: {\n category: category\n enabled: true\n}]\n\n// Resources\nresource openAi 'Microsoft.CognitiveServices/accounts@2022-12-01' = {\n name: name\n location: location\n sku: sku\n kind: 'OpenAI'\n identity: identity\n tags: tags\n properties: {\n customSubDomainName: customSubDomainName\n publicNetworkAccess: publicNetworkAccess\n }\n}\n\nresource model 'Microsoft.CognitiveServices/accounts/deployments@2022-12-01' = \n [for deployment in deployments: {\n name: deployment.name\n parent: openAi\n properties: {\n model: {\n format: 'OpenAI'\n name: deployment.name\n version: deployment.version\n }\n raiPolicyName: deployment.raiPolicyName\n scaleSettings: {\n capacity: deployment.capacity\n scaleType: deployment.scaleType\n }\n }\n}]\n\nresource openAiDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {\n name: diagnosticSettingsName\n scope: openAi\n properties: {\n workspaceId: workspaceId\n logs: openAiLogs\n metrics: openAiMetrics\n }\n}\n\n// Outputs\noutput id string = openAi.id\noutput name string = openAi.name\n
\n
Azure Cognitive Services use custom subdomain names for each resource created through theAzure portal,Azure Cloud Shell,Azure CLI,Bicep,Azure Resource Manager (ARM), orTerraform. Unlike regional endpoints, common for all customers in a specific Azure region, custom subdomain names are unique to the resource. Custom subdomain names are required to enable authentication features like Azure Active Directory (Azure AD). We need to specify a custom subdomain for ourAzure OpenAI Service,as our chatbot application will use an Azure AD security token to access it. By default, themain.bicepmodule sets the value of thecustomSubDomainNameparameter to the lowercase name of the Azure OpenAI resource. For more information on custom subdomains, seeCustom subdomain names for Cognitive Services.
\n
\n
This bicep module allows you to pass an array containing the definition of one or more model deployments in thedeploymentsparameter. For more information on model deployments, seeCreate a resource and deploy a model using Azure OpenAI
\n
\n
AKS Cluster Bicep module
\n
TheaksCluster.bicepBicep module is used to deploy theAzure Kubernetes Service(AKS)cluster. In particular, the following code snippet creates the user-defined managed identity used by the chatbot to acquire a security token from Azure Active Directory viaAzure AD workload identity. When the booleanopenAiEnabledparameter istrue, the Bicep code performs the following steps:
\n
\n
\n
Creates a new user-defined managed identity.
\n
Assign the new managed identity to the Cognitive Services User role with the resource group as a scope.
\n
Federate the managed identity with the service account used by the chatbot. The following information are necessary to create the federated identity credentials:\n
\n
The Kubernetes service account name.
\n
The Kubernetes namespace that will host the chatbot application.
\n...\n@description('Specifies the name of the user-defined managed identity used by the application that uses Azure AD workload identity to authenticate against Azure OpenAI.')\nparam workloadManagedIdentityName string\n\n@description('Specifies whether creating the Azure OpenAi resource or not.')\nparam openAiEnabled bool = false\n...\n// This user-defined managed identity used by the workload to connect to the Azure OpenAI resource with a security token issued by Azure Active Directory\nresource workloadManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = if (openAiEnabled) {\n name: workloadManagedIdentityName\n location: location\n tags: tags\n}\n\n// Assign the Cognitive Services User role to the user-defined managed identity used by workloads\nresource cognitiveServicesUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (openAiEnabled) {\n name: guid(workloadManagedIdentity.id, cognitiveServicesUserRoleDefinitionId)\n scope: resourceGroup()\n properties: {\n roleDefinitionId: cognitiveServicesUserRoleDefinitionId\n principalId: workloadManagedIdentity.properties.principalId\n principalType: 'ServicePrincipal'\n }\n}\n\n// Create federated identity for the user-defined managed identity used by the workload\nresource federatedIdentityCredentials 'Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials@2023-01-31' = {\n name: letterCaseType == 'UpperCamelCase' ? '${toUpper(first(namespace))}${toLower(substring(namespace, 1, length(namespace) - 1))}FederatedIdentity' : letterCaseType == 'CamelCase' ? '${toLower(namespace)}FederatedIdentity' : '${toLower(namespace)}-federated-identity'\n parent: workloadManagedIdentity\n properties: {\n issuer: aksCluster.properties.oidcIssuerProfile.issuerURL\n subject: 'system:serviceaccount:${namespace}:${serviceAccountName}'\n audiences: [\n 'api://AzureADTokenExchange'\n ]\n }\n}\n...\n// Output\noutput id string = aksCluster.id\noutput name string = aksCluster.name\noutput issuerUrl string = aksCluster.properties.oidcIssuerProfile.issuerURL\noutput workloadManagedIdentityClientId string = workloadManagedIdentity.properties.clientId\n
\n
Validate the deployment
\n
Open the Azure Portal, and navigate to the resource group. Open the Azure Open AI Service resource, navigate toKeys and Endpoint, and check that the endpoint contains a custom subdomain rather than the regional Cognitive Services endpoint.
\n
\n
\n
\n
\n
Open to the<Prefix>WorkloadManagedIdentitymanaged identity, navigate to theFederated credentials, and verify that the federated identity credentials for themagic8ball-saservice account were created correctly, as shown in the following picture.
\n
\n
\n
\n
Use Azure AD workload identity with Azure Kubernetes Service (AKS)
\n
Workloads deployed on an Azure Kubernetes Services (AKS) cluster require Azure Active Directory (Azure AD) application credentials or managed identities to access Azure AD-protected resources, such as Azure Key Vault and Microsoft Graph. Azure AD workload identity integrates with the capabilities native to Kubernetes to federate with external identity providers.
The sample makes use of aDeployment Scriptto run theinstall-nginx-via-helm-and-create-sa.shBash script that creates the namespace and service account for the sample application and installs the following packages to the AKS cluster viaHelm. For more information on deployment scripts, seeUse deployment scripts in Bicep.
Theinstall-nginx-via-helm-and-create-sa.shBash script returns the following outputs to the deployment script:
\n
\n
\n
Namespace hosting the chatbot sample. You can change the defaultmagic8ballnamespace by assigning a different value to thenamespaceparameter of themain.bicepmodule.
The application is contained in a single file calledapp.py. The application makes use of the following libraries:
\n
\n
\n
OpenAPI: The OpenAI Python library provides convenient access to the OpenAI API from applications written in Python. It includes a pre-defined set of classes for API resources that initialize themselves dynamically from API responses, making it compatible with a wide range of versions of the OpenAI API. You can find usage examples for the OpenAI Python library in ourAPI referenceand theOpenAI Cookbook.
\n
Azure Identity: The Azure Identity library providesAzure Active Directory (Azure AD)token authentication support across the Azure SDK. It provides a set ofTokenCredentialimplementations, which can be used to construct Azure SDK clients that support Azure AD token authentication.
\n
Streamlit: Streamlit is an open-source Python library that makes it easy to create and share beautiful, custom web apps for machine learning and data science. You can build and deploy powerful data apps in just a few minutes. For more information, seeStreamlit documentation.
\n
Streamlit-chat: a Streamlit component that provides a configurable user interface for chatbot applications.
\n
Dotenv: Python-dotenv reads key-value pairs from a .env file and can set them as environment variables. It helps in the development of applications following the12-factorprinciples.
\n
\n
Therequirements.txtfile under thescriptsfolder contains the list of packages used by theapp.pyapplication that you can restore using the following command:
\n
\npip install -r requirements.txt --upgrade\n
\n
The following table contains the code of theapp.pychatbot:
\n
\n# Import packages\nimport os\nimport sys\nimport time\nimport openai\nimport logging\nimport streamlit as st\nfrom streamlit_chat import message\nfrom azure.identity import DefaultAzureCredential\nfrom dotenv import load_dotenv\nfrom dotenv import dotenv_values\n\n# Load environment variables from .env file\nif os.path.exists(\".env\"):\n load_dotenv(override=True)\n config = dotenv_values(\".env\")\n\n# Read environment variables\nassistan_profile = \"\"\"\nYou are the infamous Magic 8 Ball. You need to randomly reply to any question with one of the following answers:\n\n- It is certain.\n- It is decidedly so.\n- Without a doubt.\n- Yes definitely.\n- You may rely on it.\n- As I see it, yes.\n- Most likely.\n- Outlook good.\n- Yes.\n- Signs point to yes.\n- Reply hazy, try again.\n- Ask again later.\n- Better not tell you now.\n- Cannot predict now.\n- Concentrate and ask again.\n- Don't count on it.\n- My reply is no.\n- My sources say no.\n- Outlook not so good.\n- Very doubtful.\n\nAdd a short comment in a pirate style at the end! Follow your heart and be creative! \nFor mor information, see https://en.wikipedia.org/wiki/Magic_8_Ball\n\"\"\"\ntitle = os.environ.get(\"TITLE\", \"Magic 8 Ball\")\ntext_input_label = os.environ.get(\"TEXT_INPUT_LABEL\", \"Pose your question and cross your fingers!\")\nimage_file_name = os.environ.get(\"IMAGE_FILE_NAME\", \"magic8ball.png\")\nimage_width = int(os.environ.get(\"IMAGE_WIDTH\", 80))\ntemperature = float(os.environ.get(\"TEMPERATURE\", 0.9))\nsystem = os.environ.get(\"SYSTEM\", assistan_profile)\napi_base = os.getenv(\"AZURE_OPENAI_BASE\")\napi_key = os.getenv(\"AZURE_OPENAI_KEY\")\napi_type = os.environ.get(\"AZURE_OPENAI_TYPE\", \"azure\")\napi_version = os.environ.get(\"AZURE_OPENAI_VERSION\", \"2023-05-15\")\nengine = os.getenv(\"AZURE_OPENAI_DEPLOYMENT\")\nmodel = os.getenv(\"AZURE_OPENAI_MODEL\")\n\n# Configure OpenAI\nopenai.api_type = api_type\nopenai.api_version = api_version\nopenai.api_base = api_base \n\n# Set default Azure credential\ndefault_credential = DefaultAzureCredential() if openai.api_type == \"azure_ad\" else None\n\n# Configure a logger\nlogging.basicConfig(stream = sys.stdout, \n format = '[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s',\n level = logging.INFO)\nlogger = logging.getLogger(__name__)\n\n# Log variables\nlogger.info(f\"title: {title}\")\nlogger.info(f\"text_input_label: {text_input_label}\")\nlogger.info(f\"image_file_name: {image_file_name}\")\nlogger.info(f\"image_width: {image_width}\")\nlogger.info(f\"temperature: {temperature}\")\nlogger.info(f\"system: {system}\")\nlogger.info(f\"api_base: {api_base}\")\nlogger.info(f\"api_key: {api_key}\")\nlogger.info(f\"api_type: {api_type}\")\nlogger.info(f\"api_version: {api_version}\")\nlogger.info(f\"engine: {engine}\")\nlogger.info(f\"model: {model}\")\n\n# Authenticate to Azure OpenAI\nif openai.api_type == \"azure\":\n openai.api_key = api_key\nelif openai.api_type == \"azure_ad\":\n openai_token = default_credential.get_token(\"https://cognitiveservices.azure.com/.default\")\n openai.api_key = openai_token.token\n if 'openai_token' not in st.session_state:\n st.session_state['openai_token'] = openai_token\nelse:\n logger.error(\"Invalid API type. Please set the AZURE_OPENAI_TYPE environment variable to azure or azure_ad.\")\n raise ValueError(\"Invalid API type. Please set the AZURE_OPENAI_TYPE environment variable to azure or azure_ad.\")\n\n# Customize Streamlit UI using CSS\nst.markdown(\"\"\"\n<style>\n\ndiv.stButton > button:first-child {\n background-color: #eb5424;\n color: white;\n font-size: 20px;\n font-weight: bold;\n border-radius: 0.5rem;\n padding: 0.5rem 1rem;\n border: none;\n box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);\n width: 300 px;\n height: 42px;\n transition: all 0.2s ease-in-out;\n} \n\ndiv.stButton > button:first-child:hover {\n transform: translateY(-3px);\n box-shadow: 0 1rem 2rem rgba(0,0,0,0.15);\n}\n\ndiv.stButton > button:first-child:active {\n transform: translateY(-1px);\n box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);\n}\n\ndiv.stButton > button:focus:not(:focus-visible) {\n color: #FFFFFF;\n}\n\n@media only screen and (min-width: 768px) {\n /* For desktop: */\n div {\n font-family: 'Roboto', sans-serif;\n }\n\n div.stButton > button:first-child {\n background-color: #eb5424;\n color: white;\n font-size: 20px;\n font-weight: bold;\n border-radius: 0.5rem;\n padding: 0.5rem 1rem;\n border: none;\n box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);\n width: 300 px;\n height: 42px;\n transition: all 0.2s ease-in-out;\n position: relative;\n bottom: -32px;\n right: 0px;\n } \n\n div.stButton > button:first-child:hover {\n transform: translateY(-3px);\n box-shadow: 0 1rem 2rem rgba(0,0,0,0.15);\n }\n\n div.stButton > button:first-child:active {\n transform: translateY(-1px);\n box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);\n }\n\n div.stButton > button:focus:not(:focus-visible) {\n color: #FFFFFF;\n }\n\n input {\n border-radius: 0.5rem;\n padding: 0.5rem 1rem;\n border: none;\n box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);\n transition: all 0.2s ease-in-out;\n height: 40px;\n }\n}\n</style>\n\"\"\", unsafe_allow_html=True)\n\n# Initialize Streamlit session state\nif 'prompts' not in st.session_state:\n st.session_state['prompts'] = [{\"role\": \"system\", \"content\": system}]\n\nif 'generated' not in st.session_state:\n st.session_state['generated'] = []\n\nif 'past' not in st.session_state:\n st.session_state['past'] = []\n\n# Refresh the OpenAI security token every 45 minutes\ndef refresh_openai_token():\n if st.session_state['openai_token'].expires_on < int(time.time()) - 45 * 60:\n st.session_state['openai_token'] = default_credential.get_token(\"https://cognitiveservices.azure.com/.default\")\n openai.api_key = st.session_state['openai_token'].token\n\n# Send user prompt to Azure OpenAI \ndef generate_response(prompt):\n try:\n st.session_state['prompts'].append({\"role\": \"user\", \"content\": prompt})\n\n if openai.api_type == \"azure_ad\":\n refresh_openai_token()\n\n completion = openai.ChatCompletion.create(\n engine = engine,\n model = model,\n messages = st.session_state['prompts'],\n temperature = temperature,\n )\n \n message = completion.choices[0].message.content\n return message\n except Exception as e:\n logging.exception(f\"Exception in generate_response: {e}\")\n\n# Reset Streamlit session state to start a new chat from scratch\ndef new_click():\n st.session_state['prompts'] = [{\"role\": \"system\", \"content\": system}]\n st.session_state['past'] = []\n st.session_state['generated'] = []\n st.session_state['user'] = \"\"\n\n# Handle on_change event for user input\ndef user_change():\n # Avoid handling the event twice when clicking the Send button\n chat_input = st.session_state['user']\n st.session_state['user'] = \"\"\n if (chat_input == '' or\n (len(st.session_state['past']) > 0 and chat_input == st.session_state['past'][-1])):\n return\n \n # Generate response invoking Azure OpenAI LLM\n if chat_input != '':\n output = generate_response(chat_input)\n \n # store the output\n st.session_state['past'].append(chat_input)\n st.session_state['generated'].append(output)\n st.session_state['prompts'].append({\"role\": \"assistant\", \"content\": output})\n\n# Create a 2-column layout. Note: Streamlit columns do not properly render on mobile devices.\n# For more information, see https://github.com/streamlit/streamlit/issues/5003\ncol1, col2 = st.columns([1, 7])\n\n# Display the robot image\nwith col1:\n st.image(image = os.path.join(\"images\", image_file_name), width = image_width)\n\n# Display the title\nwith col2:\n st.title(title)\n\n# Create a 3-column layout. Note: Streamlit columns do not properly render on mobile devices.\n# For more information, see https://github.com/streamlit/streamlit/issues/5003\ncol3, col4, col5 = st.columns([7, 1, 1])\n\n# Create text input in column 1\nwith col3:\n user_input = st.text_input(text_input_label, key = \"user\", on_change = user_change)\n\n# Create send button in column 2\nwith col4:\n st.button(label = \"Send\")\n\n# Create new button in column 3\nwith col5:\n st.button(label = \"New\", on_click = new_click)\n\n# Display the chat history in two separate tabs\n# - normal: display the chat history as a list of messages using the streamlit_chat message() function \n# - rich: display the chat history as a list of messages using the Streamlit markdown() function\nif st.session_state['generated']:\n tab1, tab2 = st.tabs([\"normal\", \"rich\"])\n with tab1:\n for i in range(len(st.session_state['generated']) - 1, -1, -1):\n message(st.session_state['past'][i], is_user = True, key = str(i) + '_user', avatar_style = \"fun-emoji\", seed = \"Nala\")\n message(st.session_state['generated'][i], key = str(i), avatar_style = \"bottts\", seed = \"Fluffy\")\n with tab2:\n for i in range(len(st.session_state['generated']) - 1, -1, -1):\n st.markdown(st.session_state['past'][i])\n st.markdown(st.session_state['generated'][i])\n
\ndef generate_response(prompt):\n try:\n st.session_state['prompts'].append({\"role\": \"user\", \"content\": prompt})\n\n if openai.api_type == \"azure_ad\":\n refresh_openai_token()\n\n completion = openai.ChatCompletion.create(\n engine = engine,\n model = model,\n messages = st.session_state['prompts'],\n temperature = temperature,\n )\n \n message = completion.choices[0].message.content\n return message\n except Exception as e:\n logging.exception(f\"Exception in generate_response: {e}\")\n
\n
OpenAI trained the ChatGPT and GPT-4 models to accept input formatted as a conversation. The messages parameter takes an array of dictionaries with a conversation organized by role or message: system, user, and assistant. The format of a basic Chat Completion is as follows:
\n
\n{\"role\": \"system\", \"content\": \"Provide some context and/or instructions to the model\"},\n{\"role\": \"user\", \"content\": \"The users messages goes here\"},\n{\"role\": \"assistant\", \"content\": \"The response message goes here.\"}\n
\n
Thesystemrole, also known as the system message, is included at the beginning of the array. This message provides the initial instructions for the model. You can provide various information in the system role, including:
\n
\n
\n
A brief description of the assistant
\n
Personality traits of the assistant
\n
Instructions or rules you would like the assistant to follow
\n
Data or information needed for the model, such as relevant questions from an FAQ
\n
You can customize the system role for your use case or include basic instructions.
\n
\n
\n
Thesystemrole or message is optional, but it's recommended to at least include a basic one to get the best results. Theuserrole or message represents an input or inquiry from the user, while theassistantmessage corresponds to the response generated by the GPT API. This dialog exchange aims to simulate a human-like conversation, where the user message initiates the interaction and the assistant message provides a relevant and informative answer. This context helps the chat model generate a more appropriate response later on. The last user message refers to the prompt currently requested. For more information, seeLearn how to work with the ChatGPT and GPT-4 models.
\n
\n
Application Configuration
\n
Make sure to provide a value for the following environment variables when testing theapp.pyPython app locally, for example in Visual Studio Code. You can eventually define environment variables in a.envfile in the same folder as theapp.pyfile.
\n
\n
\n
AZURE_OPENAI_TYPE: specifyazureif you want to let the application use the API key to authenticate against OpenAI. In this case, make sure to provide the Key in theAZURE_OPENAI_KEYenvironment variable. If you want to authenticate using an Azure AD security token, you need to specifyazure_adas a value. In this case, don't need to provide any value in theAZURE_OPENAI_KEYenvironment variable.
\n
AZURE_OPENAI_BASE: the URL of your Azure OpenAI resource. If you use the API key to authenticate against OpenAI, you can specify the regional endpoint of your Azure OpenAI Service (e.g.,https://eastus.api.cognitive.microsoft.com/). If you instead plan to use Azure AD security tokens for authentication, you need to deploy your Azure OpenAI Service with a subdomain and specify the resource-specific endpoint url (e.g.,https://myopenai.openai.azure.com/).
\n
AZURE_OPENAI_KEY: the key of your Azure OpenAI resource.
\n
AZURE_OPENAI_DEPLOYMENT: the name of the ChatGPT deployment used by your Azure OpenAI resource, for examplegpt-35-turbo.
\n
AZURE_OPENAI_MODEL: the name of the ChatGPT model used by your Azure OpenAI resource, for examplegpt-35-turbo.
\n
TITLE: the title of the Streamlit app.
\n
TEMPERATURE: the temperature used by the OpenAI API to generate the response.
\n
SYSTEM: give the model instructions about how it should behave and any context it should reference when generating a response. Used to describe the assistant's personality.
\n
\n
\n
When deploying the application to Azure Kubernetes Service (AKS), these values are provided in a KubernetesConfigMap. For more information, see the next section.
\n
\n
OpenAI Library
\n
To use theopenailibrary with Microsoft Azure endpoints, you need to set theapi_type,api_baseandapi_versionin addition to theapi_key. Theapi_typemust be set to 'azure' and the others correspond to the properties of your endpoint. In addition, the deployment name must be passed as the engine parameter. To use OpenAI Key to authenticate to your Azure endpoint, you need to set theapi_typetoazureand pass the OpenAI Key toapi_key.
To use Microsoft Active Directory to authenticate to your Azure endpoint, you need to set theapi_typetoazure_adand pass the acquired credential token toapi_key. The rest of the parameters must be set as specified in the previous section.
You can use two different authentication methods in themagic8ballchatbot application:
\n
\n
\n
API key: set theAZURE_OPENAI_TYPEenvironment variable to azure and the AZURE_OPENAI_KEY environment variable to the key of your Azure OpenAI resource. You can use the regional endpoint, such ashttps://eastus.api.cognitive.microsoft.com/, in theAZURE_OPENAI_BASEenvironment variable, to connect to the Azure OpenAI resource.
\n
Azure Active Directory: set theAZURE_OPENAI_TYPEenvironment variable to azure_ad and use a service principal or managed identity with theDefaultAzureCredentialobject to acquire a security token from Azure Active Directory. For more information on the DefaultAzureCredential in Python, seeAuthenticate Python apps to Azure services by using the Azure SDK for Python. Make sure to assign theCognitive Services Userrole to the service principal or managed identity used to authenticate to your Azure OpenAI Service. For more information, seeHow to configure Azure OpenAI Service with managed identities. If you want to use Azure AD integrated security, you need to create a custom subdomain for your Azure OpenAI resource and use the specific endpoint containing the custom domain, such ashttps://myopenai.openai.azure.com/where myopenai is the custom subdomain. If you specify the regional endpoint, you get an error like the following:Subdomain does not map to a resource. Hence, pass the custom domain endpoint in the AZURE_OPENAI_BASE environment variable. In this case, you also need to refresh the security token periodically.
\n
\n
\n
Build the container image
\n
You can build the container image using the01-build-docker-image.shin thescriptsfolder.
\n
\n
Dockerfile
\n
\n# app/Dockerfile\n\n# # Stage 1 - Install build dependencies\n\n# A Dockerfile must start with a FROM instruction which sets the base image for the container.\n# The Python images come in many flavors, each designed for a specific use case.\n# The python:3.11-slim image is a good base image for most applications.\n# It is a minimal image built on top of Debian Linux and includes only the necessary packages to run Python.\n# The slim image is a good choice because it is small and contains only the packages needed to run Python.\n# For more information, see: \n# * https://hub.docker.com/_/python \n# * https://docs.streamlit.io/knowledge-base/tutorials/deploy/docker\nFROM python:3.11-slim AS builder\n\n# The WORKDIR instruction sets the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions that follow it in the Dockerfile.\n# If the WORKDIR doesn’t exist, it will be created even if it’s not used in any subsequent Dockerfile instruction.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#workdir\nWORKDIR /app\n\n# Set environment variables. \n# The ENV instruction sets the environment variable <key> to the value <value>.\n# This value will be in the environment of all “descendant” Dockerfile commands and can be replaced inline in many as well.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#env\nENV PYTHONDONTWRITEBYTECODE 1\nENV PYTHONUNBUFFERED 1\n\n# Install git so that we can clone the app code from a remote repo using the RUN instruction.\n# The RUN comand has 2 forms:\n# * RUN <command> (shell form, the command is run in a shell, which by default is /bin/sh -c on Linux or cmd /S /C on Windows)\n# * RUN [\"executable\", \"param1\", \"param2\"] (exec form)\n# The RUN instruction will execute any commands in a new layer on top of the current image and commit the results. \n# The resulting committed image will be used for the next step in the Dockerfile.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#run\nRUN apt-get update && apt-get install -y \\\n build-essential \\\n curl \\\n software-properties-common \\\n git \\\n && rm -rf /var/lib/apt/lists/*\n\n# Create a virtualenv to keep dependencies together\nRUN python -m venv /opt/venv\nENV PATH=\"/opt/venv/bin:$PATH\"\n\n# Clone the requirements.txt which contains dependencies to WORKDIR\n# COPY has two forms:\n# * COPY <src> <dest> (this copies the files from the local machine to the container's own filesystem)\n# * COPY [\"<src>\",... \"<dest>\"] (this form is required for paths containing whitespace)\n# For more information, see: https://docs.docker.com/engine/reference/builder/#copy\nCOPY requirements.txt .\n\n# Install the Python dependencies\nRUN pip install --no-cache-dir --no-deps -r requirements.txt\n\n# Stage 2 - Copy only necessary files to the runner stage\n\n# The FROM instruction initializes a new build stage for the application\nFROM python:3.11-slim\n\n# Sets the working directory to /app\nWORKDIR /app\n\n# Copy the virtual environment from the builder stage\nCOPY --from=builder /opt/venv /opt/venv\n\n# Set environment variables\nENV PATH=\"/opt/venv/bin:$PATH\"\n\n# Clone the app.py containing the application code\nCOPY app.py .\n\n# Copy the images folder to WORKDIR\n# The ADD instruction copies new files, directories or remote file URLs from <src> and adds them to the filesystem of the image at the path <dest>.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#add\nADD images ./images\n\n# The EXPOSE instruction informs Docker that the container listens on the specified network ports at runtime.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#expose\nEXPOSE 8501\n\n# The HEALTHCHECK instruction has two forms:\n# * HEALTHCHECK [OPTIONS] CMD command (check container health by running a command inside the container)\n# * HEALTHCHECK NONE (disable any healthcheck inherited from the base image)\n# The HEALTHCHECK instruction tells Docker how to test a container to check that it is still working. \n# This can detect cases such as a web server that is stuck in an infinite loop and unable to handle new connections, \n# even though the server process is still running. For more information, see: https://docs.docker.com/engine/reference/builder/#healthcheck\nHEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health\n\n# The ENTRYPOINT instruction has two forms:\n# * ENTRYPOINT [\"executable\", \"param1\", \"param2\"] (exec form, preferred)\n# * ENTRYPOINT command param1 param2 (shell form)\n# The ENTRYPOINT instruction allows you to configure a container that will run as an executable.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#entrypoint\nENTRYPOINT [\"streamlit\", \"run\", \"app.py\", \"--server.port=8501\", \"--server.address=0.0.0.0\"]\n
Before running any script, make sure to customize the value of the variables inside the00-variables.shfile. This file is embedded in all the scripts and contains the following variables:
\n
\n# Variables\nacrName=\"CoralAcr\"\nacrResourceGrougName=\"CoralRG\"\nlocation=\"FranceCentral\"\nattachAcr=false\nimageName=\"magic8ball\"\ntag=\"v2\"\ncontainerName=\"magic8ball\"\nimage=\"$acrName.azurecr.io/$imageName:$tag\"\nimagePullPolicy=\"IfNotPresent\" # Always, Never, IfNotPresent\nmanagedIdentityName=\"OpenAiManagedIdentity\"\nfederatedIdentityName=\"Magic8BallFederatedIdentity\"\n\n# Azure Subscription and Tenant\nsubscriptionId=$(az account show --query id --output tsv)\nsubscriptionName=$(az account show --query name --output tsv)\ntenantId=$(az account show --query tenantId --output tsv)\n\n# Parameters\ntitle=\"Magic 8 Ball\"\nlabel=\"Pose your question and cross your fingers!\"\ntemperature=\"0.9\"\nimageWidth=\"80\"\n\n# OpenAI\nopenAiName=\"CoralOpenAi \"\nopenAiResourceGroupName=\"CoralRG\"\nopenAiType=\"azure_ad\"\nopenAiBase=\"https://coralopenai.openai.azure.com/\"\nopenAiModel=\"gpt-35-turbo\"\nopenAiDeployment=\"gpt-35-turbo\"\n\n# Nginx Ingress Controller\nnginxNamespace=\"ingress-basic\"\nnginxRepoName=\"ingress-nginx\"\nnginxRepoUrl=\"https://kubernetes.github.io/ingress-nginx\"\nnginxChartName=\"ingress-nginx\"\nnginxReleaseName=\"nginx-ingress\"\nnginxReplicaCount=3\n\n# Certificate Manager\ncmNamespace=\"cert-manager\"\ncmRepoName=\"jetstack\"\ncmRepoUrl=\"https://charts.jetstack.io\"\ncmChartName=\"cert-manager\"\ncmReleaseName=\"cert-manager\"\n\n# Cluster Issuer\nemail=\"paolos@microsoft.com\"\nclusterIssuerName=\"letsencrypt-nginx\"\nclusterIssuerTemplate=\"cluster-issuer.yml\"\n\n# AKS Cluster\naksClusterName=\"CoralAks\"\naksResourceGroupName=\"CoralRG\"\n\n# Sample Application\nnamespace=\"magic8ball\"\nserviceAccountName=\"magic8ball-sa\"\ndeploymentTemplate=\"deployment.yml\"\nserviceTemplate=\"service.yml\"\nconfigMapTemplate=\"configMap.yml\"\nsecretTemplate=\"secret.yml\"\n\n# Ingress and DNS\ningressTemplate=\"ingress.yml\"\ningressName=\"magic8ball-ingress\"\ndnsZoneName=\"babosbird.com\"\ndnsZoneResourceGroupName=\"DnsResourceGroup\"\nsubdomain=\"magic8ball\"\nhost=\"$subdomain.$dnsZoneName\"\n
\n
Upload Docker container image to Azure Container Registry (ACR)
\n
You can push the Docker container image to Azure Container Registry (ACR) using the03-push-docker-image.shscript in thescriptsfolder.
\n
\n#!/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Login to ACR\naz acr login --name $acrName \n\n# Retrieve ACR login server. Each container image needs to be tagged with the loginServer name of the registry. \nloginServer=$(az acr show --name $acrName --query loginServer --output tsv)\n\n# Tag the local image with the loginServer of ACR\ndocker tag ${imageName,,}:$tag $loginServer/${imageName,,}:$tag\n\n# Push latest container image to ACR\ndocker push $loginServer/${imageName,,}:$tag\n
\n
Deployment Scripts
\n
If you deployed the Azure infrastructure using the Bicep modules provided with this sample, you only need to deploy the application using the following scripts and YAML templates in thescriptsfolder.
\n
\n
\n
09-deploy-app.sh
\n
10-create-ingress.sh
\n
11-configure-dns.sh
\n
configMap.yml
\n
deployment.yml
\n
ingress.yml
\n
service.yml
\n
\n
\n
If you instead want to deploy the application in your AKS cluster, you can use the following scripts to configure your environment.
\n
\n
04-create-nginx-ingress-controller.sh
\n
The scriptinstalls theNGINX Ingress Controllerusing Helm.
\n
\n#!/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Use Helm to deploy an NGINX ingress controller\nresult=$(helm list -n $nginxNamespace | grep $nginxReleaseName | awk '{print $1}')\n\nif [[ -n $result ]]; then\n echo \"[$nginxReleaseName] ingress controller already exists in the [$nginxNamespace] namespace\"\nelse\n # Check if the ingress-nginx repository is not already added\n result=$(helm repo list | grep $nginxRepoName | awk '{print $1}')\n\n if [[ -n $result ]]; then\n echo \"[$nginxRepoName] Helm repo already exists\"\n else\n # Add the ingress-nginx repository\n echo \"Adding [$nginxRepoName] Helm repo...\"\n helm repo add $nginxRepoName $nginxRepoUrl\n fi\n\n # Update your local Helm chart repository cache\n echo 'Updating Helm repos...'\n helm repo update\n\n # Deploy NGINX ingress controller\n echo \"Deploying [$nginxReleaseName] NGINX ingress controller to the [$nginxNamespace] namespace...\"\n helm install $nginxReleaseName $nginxRepoName/$nginxChartName \\\n --create-namespace \\\n --namespace $nginxNamespace \\\n --set controller.config.enable-modsecurity=true \\\n --set controller.config.enable-owasp-modsecurity-crs=true \\\n --set controller.config.modsecurity-snippet=\\\n'SecRuleEngine On\nSecRequestBodyAccess On\nSecAuditLog /dev/stdout\nSecAuditLogFormat JSON\nSecAuditEngine RelevantOnly\nSecRule REMOTE_ADDR \"@ipMatch 127.0.0.1\" \"id:87,phase:1,pass,nolog,ctl:ruleEngine=Off\"' \\\n --set controller.metrics.enabled=true \\\n --set controller.metrics.serviceMonitor.enabled=true \\\n --set controller.metrics.serviceMonitor.additionalLabels.release=\"prometheus\" \\\n --set controller.nodeSelector.\"kubernetes\\.io/os\"=linux \\\n --set controller.replicaCount=$replicaCount \\\n --set defaultBackend.nodeSelector.\"kubernetes\\.io/os\"=linux \\\n --set controller.service.annotations.\"service\\.beta\\.kubernetes\\.io/azure-load-balancer-health-probe-request-path\"=/healthz\nfi\n
\n
05-install-cert-manager.sh This script installs thecert-managerusing Helm.
\n
\n#/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Check if the ingress-nginx repository is not already added\nresult=$(helm repo list | grep $cmRepoName | awk '{print $1}')\n\nif [[ -n $result ]]; then\n echo \"[$cmRepoName] Helm repo already exists\"\nelse\n # Add the Jetstack Helm repository\n echo \"Adding [$cmRepoName] Helm repo...\"\n helm repo add $cmRepoName $cmRepoUrl\nfi\n\n# Update your local Helm chart repository cache\necho 'Updating Helm repos...'\nhelm repo update\n\n# Install cert-manager Helm chart\nresult=$(helm list -n $cmNamespace | grep $cmReleaseName | awk '{print $1}')\n\nif [[ -n $result ]]; then\n echo \"[$cmReleaseName] cert-manager already exists in the $cmNamespace namespace\"\nelse\n # Install the cert-manager Helm chart\n echo \"Deploying [$cmReleaseName] cert-manager to the $cmNamespace namespace...\"\n helm install $cmReleaseName $cmRepoName/$cmChartName \\\n --create-namespace \\\n --namespace $cmNamespace \\\n --set installCRDs=true \\\n --set nodeSelector.\"kubernetes\\.io/os\"=linux\nfi\n
\n
06-create-cluster-issuer.sh
\n
This script creates a cluster issuer for theNGINX Ingress Controllerbased on theLet's EncryptACME certificate issuer.
This script creates the managed identity used by themagic8ballchatbot and assigns it theCognitive Services Userrole on the Azure OpenAI Service.
\n
\n#!/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Check if the user-assigned managed identity already exists\necho \"Checking if [$managedIdentityName] user-assigned managed identity actually exists in the [$aksResourceGroupName] resource group...\"\n\naz identity show \\\n --name $managedIdentityName \\\n --resource-group $aksResourceGroupName &>/dev/null\n\nif [[ $? != 0 ]]; then\n echo \"No [$managedIdentityName] user-assigned managed identity actually exists in the [$aksResourceGroupName] resource group\"\n echo \"Creating [$managedIdentityName] user-assigned managed identity in the [$aksResourceGroupName] resource group...\"\n\n # Create the user-assigned managed identity\n az identity create \\\n --name $managedIdentityName \\\n --resource-group $aksResourceGroupName \\\n --location $location \\\n --subscription $subscriptionId 1>/dev/null\n\n if [[ $? == 0 ]]; then\n echo \"[$managedIdentityName] user-assigned managed identity successfully created in the [$aksResourceGroupName] resource group\"\n else\n echo \"Failed to create [$managedIdentityName] user-assigned managed identity in the [$aksResourceGroupName] resource group\"\n exit\n fi\nelse\n echo \"[$managedIdentityName] user-assigned managed identity already exists in the [$aksResourceGroupName] resource group\"\nfi\n\n# Retrieve the clientId of the user-assigned managed identity\necho \"Retrieving clientId for [$managedIdentityName] managed identity...\"\nclientId=$(az identity show \\\n --name $managedIdentityName \\\n --resource-group $aksResourceGroupName \\\n --query clientId \\\n --output tsv)\n\nif [[ -n $clientId ]]; then\n echo \"[$clientId] clientId for the [$managedIdentityName] managed identity successfully retrieved\"\nelse\n echo \"Failed to retrieve clientId for the [$managedIdentityName] managed identity\"\n exit\nfi\n\n# Retrieve the principalId of the user-assigned managed identity\necho \"Retrieving principalId for [$managedIdentityName] managed identity...\"\nprincipalId=$(az identity show \\\n --name $managedIdentityName \\\n --resource-group $aksResourceGroupName \\\n --query principalId \\\n --output tsv)\n\nif [[ -n $principalId ]]; then\n echo \"[$principalId] principalId for the [$managedIdentityName] managed identity successfully retrieved\"\nelse\n echo \"Failed to retrieve principalId for the [$managedIdentityName] managed identity\"\n exit\nfi\n\n# Get the resource id of the Azure OpenAI resource\nopenAiId=$(az cognitiveservices account show \\\n --name $openAiName \\\n --resource-group $openAiResourceGroupName \\\n --query id \\\n --output tsv)\n\nif [[ -n $openAiId ]]; then\n echo \"Resource id for the [$openAiName] Azure OpenAI resource successfully retrieved\"\nelse\n echo \"Failed to the resource id for the [$openAiName] Azure OpenAI resource\"\n exit -1\nfi\n\n# Assign the Cognitive Services User role on the Azure OpenAI resource to the managed identity\nrole=\"Cognitive Services User\"\necho \"Checking if the [$managedIdentityName] managed identity has been assigned to [$role] role with [$openAiName] Azure OpenAI resource as a scope...\"\ncurrent=$(az role assignment list \\\n --assignee $principalId \\\n --scope $openAiId \\\n --query \"[?roleDefinitionName=='$role'].roleDefinitionName\" \\\n --output tsv 2>/dev/null)\n\nif [[ $current == $role ]]; then\n echo \"[$managedIdentityName] managed identity is already assigned to the [\"$current\"] role with [$openAiName] Azure OpenAI resource as a scope\"\nelse\n echo \"[$managedIdentityName] managed identity is not assigned to the [$role] role with [$openAiName] Azure OpenAI resource as a scope\"\n echo \"Assigning the [$role] role to the [$managedIdentityName] managed identity with [$openAiName] Azure OpenAI resource as a scope...\"\n\n az role assignment create \\\n --assignee $principalId \\\n --role \"$role\" \\\n --scope $openAiId 1>/dev/null\n\n if [[ $? == 0 ]]; then\n echo \"[$managedIdentityName] managed identity successfully assigned to the [$role] role with [$openAiName] Azure OpenAI resource as a scope\"\n else\n echo \"Failed to assign the [$managedIdentityName] managed identity to the [$role] role with [$openAiName] Azure OpenAI resource as a scope\"\n exit\n fi\nfi\n
\n
08-create-service-account.sh
\n
This script creates the namespace and service account for themagic8ballchatbot and federate the service account with the user-defined managed identity created in the previous step.
\n
\n#!/bin/bash\n\n# Variables for the user-assigned managed identity\nsource ./00-variables.sh\n\n# Check if the namespace already exists\nresult=$(kubectl get namespace -o 'jsonpath={.items[?(@.metadata.name==\"'$namespace'\")].metadata.name'})\n\nif [[ -n $result ]]; then\n echo \"[$namespace] namespace already exists\"\nelse\n # Create the namespace for your ingress resources\n echo \"[$namespace] namespace does not exist\"\n echo \"Creating [$namespace] namespace...\"\n kubectl create namespace $namespace\nfi\n\n# Check if the service account already exists\nresult=$(kubectl get sa -n $namespace -o 'jsonpath={.items[?(@.metadata.name==\"'$serviceAccountName'\")].metadata.name'})\n\nif [[ -n $result ]]; then\n echo \"[$serviceAccountName] service account already exists\"\nelse\n # Retrieve the resource id of the user-assigned managed identity\n echo \"Retrieving clientId for [$managedIdentityName] managed identity...\"\n managedIdentityClientId=$(az identity show \\\n --name $managedIdentityName \\\n --resource-group $aksResourceGroupName \\\n --query clientId \\\n --output tsv)\n\n if [[ -n $managedIdentityClientId ]]; then\n echo \"[$managedIdentityClientId] clientId for the [$managedIdentityName] managed identity successfully retrieved\"\n else\n echo \"Failed to retrieve clientId for the [$managedIdentityName] managed identity\"\n exit\n fi\n\n # Create the service account\n echo \"[$serviceAccountName] service account does not exist\"\n echo \"Creating [$serviceAccountName] service account...\"\n cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n annotations:\n azure.workload.identity/client-id: $managedIdentityClientId\n azure.workload.identity/tenant-id: $tenantId\n labels:\n azure.workload.identity/use: \"true\"\n name: $serviceAccountName\n namespace: $namespace\nEOF\nfi\n\n# Show service account YAML manifest\necho \"Service Account YAML manifest\"\necho \"-----------------------------\"\nkubectl get sa $serviceAccountName -n $namespace -o yaml\n\n# Check if the federated identity credential already exists\necho \"Checking if [$federatedIdentityName] federated identity credential actually exists in the [$aksResourceGroupName] resource group...\"\n\naz identity federated-credential show \\\n --name $federatedIdentityName \\\n --resource-group $aksResourceGroupName \\\n --identity-name $managedIdentityName &>/dev/null\n\nif [[ $? != 0 ]]; then\n echo \"No [$federatedIdentityName] federated identity credential actually exists in the [$aksResourceGroupName] resource group\"\n\n # Get the OIDC Issuer URL\n aksOidcIssuerUrl=\"$(az aks show \\\n --only-show-errors \\\n --name $aksClusterName \\\n --resource-group $aksResourceGroupName \\\n --query oidcIssuerProfile.issuerUrl \\\n --output tsv)\"\n\n # Show OIDC Issuer URL\n if [[ -n $aksOidcIssuerUrl ]]; then\n echo \"The OIDC Issuer URL of the $aksClusterName cluster is $aksOidcIssuerUrl\"\n fi\n\n echo \"Creating [$federatedIdentityName] federated identity credential in the [$aksResourceGroupName] resource group...\"\n\n # Establish the federated identity credential between the managed identity, the service account issuer, and the subject.\n az identity federated-credential create \\\n --name $federatedIdentityName \\\n --identity-name $managedIdentityName \\\n --resource-group $aksResourceGroupName \\\n --issuer $aksOidcIssuerUrl \\\n --subject system:serviceaccount:$namespace:$serviceAccountName\n\n if [[ $? == 0 ]]; then\n echo \"[$federatedIdentityName] federated identity credential successfully created in the [$aksResourceGroupName] resource group\"\n else\n echo \"Failed to create [$federatedIdentityName] federated identity credential in the [$aksResourceGroupName] resource group\"\n exit\n fi\nelse\n echo \"[$federatedIdentityName] federated identity credential already exists in the [$aksResourceGroupName] resource group\"\nfi\n
\n
09-deploy-app.sh
\n
This script creates the Kubernetes config map, deployment, and service used by themagic8ballchatbot.
\n
\n#!/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Attach ACR to AKS cluster\nif [[ $attachAcr == true ]]; then\n echo \"Attaching ACR $acrName to AKS cluster $aksClusterName...\"\n az aks update \\\n --name $aksClusterName \\\n --resource-group $aksResourceGroupName \\\n --attach-acr $acrName\nfi\n\n# Check if namespace exists in the cluster\nresult=$(kubectl get namespace -o jsonpath=\"{.items[?(@.metadata.name=='$namespace')].metadata.name}\")\n\nif [[ -n $result ]]; then\n echo \"$namespace namespace already exists in the cluster\"\nelse\n echo \"$namespace namespace does not exist in the cluster\"\n echo \"creating $namespace namespace in the cluster...\"\n kubectl create namespace $namespace\nfi\n\n# Create config map\ncat $configMapTemplate |\n yq \"(.data.TITLE)|=\"\\\"\"$title\"\\\" |\n yq \"(.data.LABEL)|=\"\\\"\"$label\"\\\" |\n yq \"(.data.TEMPERATURE)|=\"\\\"\"$temperature\"\\\" |\n yq \"(.data.IMAGE_WIDTH)|=\"\\\"\"$imageWidth\"\\\" |\n yq \"(.data.AZURE_OPENAI_TYPE)|=\"\\\"\"$openAiType\"\\\" |\n yq \"(.data.AZURE_OPENAI_BASE)|=\"\\\"\"$openAiBase\"\\\" |\n yq \"(.data.AZURE_OPENAI_MODEL)|=\"\\\"\"$openAiModel\"\\\" |\n yq \"(.data.AZURE_OPENAI_DEPLOYMENT)|=\"\\\"\"$openAiDeployment\"\\\" |\n kubectl apply -n $namespace -f -\n\n# Create deployment\ncat $deploymentTemplate |\n yq \"(.spec.template.spec.containers[0].image)|=\"\\\"\"$image\"\\\" |\n yq \"(.spec.template.spec.containers[0].imagePullPolicy)|=\"\\\"\"$imagePullPolicy\"\\\" |\n yq \"(.spec.template.spec.serviceAccountName)|=\"\\\"\"$serviceAccountName\"\\\" |\n kubectl apply -n $namespace -f -\n\n# Create deployment\nkubectl apply -f $serviceTemplate -n $namespace\n
\n
10-create-ingress.sh
\n
This script creates the ingress object to expose the service via theNGINX Ingress Controller.
\n
\n#/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Create the ingress\necho \"[$ingressName] ingress does not exist\"\necho \"Creating [$ingressName] ingress...\"\ncat $ingressTemplate |\n yq \"(.spec.tls[0].hosts[0])|=\"\\\"\"$host\"\\\" |\n yq \"(.spec.rules[0].host)|=\"\\\"\"$host\"\\\" |\n kubectl apply -n $namespace -f -\n
\n
11-configure-dns.sh
\n
This script creates an A record in the Azure DNS Zone to expose the application via a given subdomain (e.g.,https://magic8ball.example.com).
\n
\n# Variables\nsource ./00-variables.sh\n\n# Retrieve the public IP address from the ingress\necho \"Retrieving the external IP address from the [$ingressName] ingress...\"\npublicIpAddress=$(kubectl get ingress $ingressName -n $namespace -o jsonpath='{.status.loadBalancer.ingress[0].ip}')\n\nif [ -n $publicIpAddress ]; then\n echo \"[$publicIpAddress] external IP address of the application gateway ingress controller successfully retrieved from the [$ingressName] ingress\"\nelse\n echo \"Failed to retrieve the external IP address of the application gateway ingress controller from the [$ingressName] ingress\"\n exit\nfi\n\n# Check if an A record for todolist subdomain exists in the DNS Zone\necho \"Retrieving the A record for the [$subdomain] subdomain from the [$dnsZoneName] DNS zone...\"\nipv4Address=$(az network dns record-set a list \\\n --zone-name $dnsZoneName \\\n --resource-group $dnsZoneResourceGroupName \\\n --query \"[?name=='$subdomain'].arecords[].ipv4Address\" \\\n --output tsv)\n\nif [[ -n $ipv4Address ]]; then\n echo \"An A record already exists in [$dnsZoneName] DNS zone for the [$subdomain] subdomain with [$ipv4Address] IP address\"\n\n if [[ $ipv4Address == $publicIpAddress ]]; then\n echo \"The [$ipv4Address] ip address of the existing A record is equal to the ip address of the [$ingressName] ingress\"\n echo \"No additional step is required\"\n exit\n else\n echo \"The [$ipv4Address] ip address of the existing A record is different than the ip address of the [$ingressName] ingress\"\n fi\n\n # Retrieving name of the record set relative to the zone\n echo \"Retrieving the name of the record set relative to the [$dnsZoneName] zone...\"\n\n recordSetName=$(az network dns record-set a list \\\n --zone-name $dnsZoneName \\\n --resource-group $dnsZoneResourceGroupName \\\n --query \"[?name=='$subdomain'].name\" \\\n --output name 2>/dev/null)\n\n if [[ -n $recordSetName ]]; then\n \"[$recordSetName] record set name successfully retrieved\"\n else\n \"Failed to retrieve the name of the record set relative to the [$dnsZoneName] zone\"\n exit\n fi\n\n # Remove the a record\n echo \"Removing the A record from the record set relative to the [$dnsZoneName] zone...\"\n\n az network dns record-set a remove-record \\\n --ipv4-address $ipv4Address \\\n --record-set-name $recordSetName \\\n --zone-name $dnsZoneName \\\n --resource-group $dnsZoneResourceGroupName\n\n if [[ $? == 0 ]]; then\n echo \"[$ipv4Address] ip address successfully removed from the [$recordSetName] record set\"\n else\n echo \"Failed to remove the [$ipv4Address] ip address from the [$recordSetName] record set\"\n exit\n fi\nfi\n\n# Create the a record\necho \"Creating an A record in [$dnsZoneName] DNS zone for the [$subdomain] subdomain with [$publicIpAddress] IP address...\"\naz network dns record-set a add-record \\\n --zone-name $dnsZoneName \\\n --resource-group $dnsZoneResourceGroupName \\\n --record-set-name $subdomain \\\n --ipv4-address $publicIpAddress 1>/dev/null\n\nif [[ $? == 0 ]]; then\n echo \"A record for the [$subdomain] subdomain with [$publicIpAddress] IP address successfully created in [$dnsZoneName] DNS zone\"\nelse\n echo \"Failed to create an A record for the $subdomain subdomain with [$publicIpAddress] IP address in [$dnsZoneName] DNS zone\"\nfi\n
\n
The scripts used to deploy the YAML template use theyqtool to customize the manifests with the value of the variables defined in the00-variables.shfile. This tool is a lightweight and portable command-line YAML, JSON and XML processor that usesjqlike syntax but works with YAML files as well as json, xml, properties, csv and tsv. It doesn't yet support everything jq does - but it does support the most common operations and functions, and more is being added continuously.
\n
\n
YAML manifests
\n
Below, you can read the YAML manifests used to deploy themagic8ballchatbot to AKS.
\n
\n
configmap.yml
\n
The configmap.yml defines a value for the environment variables passed to the application container. The config map does not define any environment variable for the OpenAI key as the container.
These are the parameters defined by the configmap:
\n
\n
\n
AZURE_OPENAI_TYPE: specifyazureif you want to let the application use the API key to authenticate against OpenAI. In this case, make sure to provide the Key in theAZURE_OPENAI_KEYenvironment variable. If you want to authenticate using an Azure AD security token, you need to specifyazure_adas a value. In this case, don't need to provide any value in theAZURE_OPENAI_KEYenvironment variable.
\n
AZURE_OPENAI_BASE: the URL of your Azure OpenAI resource. If you use the API key to authenticate against OpenAI, you can specify the regional endpoint of your Azure OpenAI Service (e.g.,https://eastus.api.cognitive.microsoft.com/). If you instead plan to use Azure AD security tokens for authentication, you need to deploy your Azure OpenAI Service with a subdomain and specify the resource-specific endpoint url (e.g.,https://myopenai.openai.azure.com/).
\n
AZURE_OPENAI_KEY: the key of your Azure OpenAI resource. If you setAZURE_OPENAI_TYPEtoazure_adyou can leave this parameter empty.
\n
AZURE_OPENAI_DEPLOYMENT: the name of the ChatGPT deployment used by your Azure OpenAI resource, for examplegpt-35-turbo.
\n
AZURE_OPENAI_MODEL: the name of the ChatGPT model used by your Azure OpenAI resource, for examplegpt-35-turbo.
\n
TITLE: the title of the Streamlit app.
\n
TEMPERATURE: the temperature used by the OpenAI API to generate the response.
\n
SYSTEM: give the model instructions about how it should behave and any context it should reference when generating a response. Used to describe the assistant's personality.
\n
\n
\n
deployment.yml Thedeployment.ymlmanifest is used create a Kubernetesdeploymentthat defines the application pods to create azure.workload.identity/use label is required in the pod template spec. Only pods with this label will be mutated by the azure-workload-identity mutating admission webhook to inject the Azure specific environment variables and the projected service account token volume.
The ingress object defines the following annotations:
\n
\n
\n
cert-manager.io/cluster-issuer: specifies the name of a cert-manager.io ClusterIssuer to acquire the certificate required for this Ingress. It does not matter which namespace your Ingress resides, as ClusterIssuers are non-namespaced resources. In this sample, the cert-manager is instructed to use theletsencrypt-nginxClusterIssuer that you can create using the06-create-cluster-issuer.shscript.
","introduction":"","coverImage":null,"coverImageProperties":{"__typename":"CoverImageProperties","style":"STANDARD","titlePosition":"BOTTOM","altText":""},"currentRevision":{"__ref":"Revision:revision:3834619_5"},"latestVersion":{"__typename":"FriendlyVersion","major":"5","minor":"0"},"metrics":{"__typename":"MessageMetrics","views":38129},"visibilityScope":"PUBLIC","canonicalUrl":null,"seoTitle":null,"seoDescription":null,"placeholder":false,"originalMessageForPlaceholder":null,"contributors":{"__typename":"UserConnection","edges":[]},"nonCoAuthorContributors":{"__typename":"UserConnection","edges":[]},"coAuthors":{"__typename":"UserConnection","edges":[]},"blogMessagePolicies":{"__typename":"BlogMessagePolicies","canDoAuthoringActionsOnBlog":{"__typename":"PolicyResult","failureReason":{"__typename":"FailureReason","message":"error.lithium.policies.blog.action_can_do_authoring_action.accessDenied","key":"error.lithium.policies.blog.action_can_do_authoring_action.accessDenied","args":[]}}},"archivalData":null,"replies":{"__typename":"MessageConnection","edges":[{"__typename":"MessageEdge","cursor":"MjUuM3wyLjF8aXwxMHwxMzI6MHxpbnQsMzg0MzIwMCwzODQzMjAw","node":{"__ref":"BlogReplyMessage:message:3843200"}},{"__typename":"MessageEdge","cursor":"MjUuM3wyLjF8aXwxMHwxMzI6MHxpbnQsMzg0MzIwMCwzODQzMTky","node":{"__ref":"BlogReplyMessage:message:3843192"}}],"pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null}},"customFields":[],"revisions({\"constraints\":{\"isPublished\":{\"eq\":true}},\"first\":1})":{"__typename":"RevisionConnection","totalCount":5}},"Conversation:conversation:3834619":{"__typename":"Conversation","id":"conversation:3834619","solved":false,"topic":{"__ref":"BlogTopicMessage:message:3834619"},"lastPostingActivityTime":"2023-06-08T08:43:03.341-07:00","lastPostTime":"2023-06-08T08:39:05.690-07:00","unreadReplyCount":2,"isSubscribed":false},"ModerationData:moderation_data:3834619":{"__typename":"ModerationData","id":"moderation_data:3834619","status":"APPROVED","rejectReason":null,"isReportedAbuse":false,"rejectUser":null,"rejectTime":null,"rejectActorType":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODM0NjE5LTQ3NTE1MGkxRERBRTk5ODgyOTUwNThB?revision=5\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODM0NjE5LTQ3NTE1MGkxRERBRTk5ODgyOTUwNThB?revision=5","title":"architecture.png","associationType":"TEASER","width":690,"height":758,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODM0NjE5LTQ3NTEzM2lBQUQ1RTk5RjkyRjFBQzhF?revision=5\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODM0NjE5LTQ3NTEzM2lBQUQ1RTk5RjkyRjFBQzhF?revision=5","title":"architecture.png","associationType":"BODY","width":690,"height":758,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODM0NjE5LTQ3NTM5MWk1NzM2QzYyNDgzRDY3MzlC?revision=5\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODM0NjE5LTQ3NTM5MWk1NzM2QzYyNDgzRDY3MzlC?revision=5","title":"openai.png","associationType":"BODY","width":1012,"height":587,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODM0NjE5LTQ3NTM5Mmk2RkVGQ0QyN0E0Nzk4MzA1?revision=5\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODM0NjE5LTQ3NTM5Mmk2RkVGQ0QyN0E0Nzk4MzA1?revision=5","title":"federatedidentitycredentials.png","associationType":"BODY","width":876,"height":673,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODM0NjE5LTQ3NTE1MWkwM0ZEMjI3NThDNDQyRDVC?revision=5\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODM0NjE5LTQ3NTE1MWkwM0ZEMjI3NThDNDQyRDVC?revision=5","title":"magic8ball.png","associationType":"BODY","width":754,"height":587,"altText":null},"Revision:revision:3834619_5":{"__typename":"Revision","id":"revision:3834619_5","lastEditTime":"2023-06-08T08:43:03.341-07:00"},"CachedAsset:theme:customTheme1-1746563245856":{"__typename":"CachedAsset","id":"theme:customTheme1-1746563245856","value":{"id":"customTheme1","animation":{"fast":"150ms","normal":"250ms","slow":"500ms","slowest":"750ms","function":"cubic-bezier(0.07, 0.91, 0.51, 1)","__typename":"AnimationThemeSettings"},"avatar":{"borderRadius":"50%","collections":["default"],"__typename":"AvatarThemeSettings"},"basics":{"browserIcon":{"imageAssetName":"favicon-1730836283320.png","imageLastModified":"1730836286415","__typename":"ThemeAsset"},"customerLogo":{"imageAssetName":"favicon-1730836271365.png","imageLastModified":"1730836274203","__typename":"ThemeAsset"},"maximumWidthOfPageContent":"1300px","oneColumnNarrowWidth":"800px","gridGutterWidthMd":"30px","gridGutterWidthXs":"10px","pageWidthStyle":"WIDTH_OF_BROWSER","__typename":"BasicsThemeSettings"},"buttons":{"borderRadiusSm":"3px","borderRadius":"3px","borderRadiusLg":"5px","paddingY":"5px","paddingYLg":"7px","paddingYHero":"var(--lia-bs-btn-padding-y-lg)","paddingX":"12px","paddingXLg":"16px","paddingXHero":"60px","fontStyle":"NORMAL","fontWeight":"700","textTransform":"NONE","disabledOpacity":0.5,"primaryTextColor":"var(--lia-bs-white)","primaryTextHoverColor":"var(--lia-bs-white)","primaryTextActiveColor":"var(--lia-bs-white)","primaryBgColor":"var(--lia-bs-primary)","primaryBgHoverColor":"hsl(var(--lia-bs-primary-h), var(--lia-bs-primary-s), calc(var(--lia-bs-primary-l) * 0.85))","primaryBgActiveColor":"hsl(var(--lia-bs-primary-h), var(--lia-bs-primary-s), calc(var(--lia-bs-primary-l) * 0.7))","primaryBorder":"1px solid transparent","primaryBorderHover":"1px solid transparent","primaryBorderActive":"1px solid transparent","primaryBorderFocus":"1px solid var(--lia-bs-white)","primaryBoxShadowFocus":"0 0 0 1px var(--lia-bs-primary), 0 0 0 4px hsla(var(--lia-bs-primary-h), var(--lia-bs-primary-s), var(--lia-bs-primary-l), 0.2)","secondaryTextColor":"var(--lia-bs-gray-900)","secondaryTextHoverColor":"hsl(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), calc(var(--lia-bs-gray-900-l) * 0.95))","secondaryTextActiveColor":"hsl(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), calc(var(--lia-bs-gray-900-l) * 0.9))","secondaryBgColor":"var(--lia-bs-gray-200)","secondaryBgHoverColor":"hsl(var(--lia-bs-gray-200-h), var(--lia-bs-gray-200-s), calc(var(--lia-bs-gray-200-l) * 0.96))","secondaryBgActiveColor":"hsl(var(--lia-bs-gray-200-h), var(--lia-bs-gray-200-s), calc(var(--lia-bs-gray-200-l) * 0.92))","secondaryBorder":"1px solid transparent","secondaryBorderHover":"1px solid transparent","secondaryBorderActive":"1px solid transparent","secondaryBorderFocus":"1px solid transparent","secondaryBoxShadowFocus":"0 0 0 1px var(--lia-bs-primary), 0 0 0 4px hsla(var(--lia-bs-primary-h), var(--lia-bs-primary-s), var(--lia-bs-primary-l), 0.2)","tertiaryTextColor":"var(--lia-bs-gray-900)","tertiaryTextHoverColor":"hsl(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), calc(var(--lia-bs-gray-900-l) * 0.95))","tertiaryTextActiveColor":"hsl(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), calc(var(--lia-bs-gray-900-l) * 0.9))","tertiaryBgColor":"transparent","tertiaryBgHoverColor":"transparent","tertiaryBgActiveColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.04)","tertiaryBorder":"1px solid transparent","tertiaryBorderHover":"1px solid hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.08)","tertiaryBorderActive":"1px solid transparent","tertiaryBorderFocus":"1px solid transparent","tertiaryBoxShadowFocus":"0 0 0 1px var(--lia-bs-primary), 0 0 0 4px hsla(var(--lia-bs-primary-h), var(--lia-bs-primary-s), var(--lia-bs-primary-l), 0.2)","destructiveTextColor":"var(--lia-bs-danger)","destructiveTextHoverColor":"hsl(var(--lia-bs-danger-h), var(--lia-bs-danger-s), calc(var(--lia-bs-danger-l) * 0.95))","destructiveTextActiveColor":"hsl(var(--lia-bs-danger-h), var(--lia-bs-danger-s), calc(var(--lia-bs-danger-l) * 0.9))","destructiveBgColor":"var(--lia-bs-gray-200)","destructiveBgHoverColor":"hsl(var(--lia-bs-gray-200-h), var(--lia-bs-gray-200-s), calc(var(--lia-bs-gray-200-l) * 0.96))","destructiveBgActiveColor":"hsl(var(--lia-bs-gray-200-h), var(--lia-bs-gray-200-s), calc(var(--lia-bs-gray-200-l) * 0.92))","destructiveBorder":"1px solid transparent","destructiveBorderHover":"1px solid transparent","destructiveBorderActive":"1px solid transparent","destructiveBorderFocus":"1px solid transparent","destructiveBoxShadowFocus":"0 0 0 1px var(--lia-bs-primary), 0 0 0 4px hsla(var(--lia-bs-primary-h), var(--lia-bs-primary-s), var(--lia-bs-primary-l), 0.2)","__typename":"ButtonsThemeSettings"},"border":{"color":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.08)","mainContent":"NONE","sideContent":"LIGHT","radiusSm":"3px","radius":"5px","radiusLg":"9px","radius50":"100vw","__typename":"BorderThemeSettings"},"boxShadow":{"xs":"0 0 0 1px hsla(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), var(--lia-bs-gray-900-l), 0.08), 0 3px 0 -1px hsla(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), var(--lia-bs-gray-900-l), 0.16)","sm":"0 2px 4px hsla(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), var(--lia-bs-gray-900-l), 0.12)","md":"0 5px 15px hsla(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), var(--lia-bs-gray-900-l), 0.3)","lg":"0 10px 30px hsla(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), var(--lia-bs-gray-900-l), 0.3)","__typename":"BoxShadowThemeSettings"},"cards":{"bgColor":"var(--lia-panel-bg-color)","borderRadius":"var(--lia-panel-border-radius)","boxShadow":"var(--lia-box-shadow-xs)","__typename":"CardsThemeSettings"},"chip":{"maxWidth":"300px","height":"30px","__typename":"ChipThemeSettings"},"coreTypes":{"defaultMessageLinkColor":"var(--lia-bs-link-color)","defaultMessageLinkDecoration":"none","defaultMessageLinkFontStyle":"NORMAL","defaultMessageLinkFontWeight":"400","defaultMessageFontStyle":"NORMAL","defaultMessageFontWeight":"400","defaultMessageFontFamily":"var(--lia-bs-font-family-base)","forumColor":"#4099E2","forumFontFamily":"var(--lia-bs-font-family-base)","forumFontWeight":"var(--lia-default-message-font-weight)","forumLineHeight":"var(--lia-bs-line-height-base)","forumFontStyle":"var(--lia-default-message-font-style)","forumMessageLinkColor":"var(--lia-default-message-link-color)","forumMessageLinkDecoration":"var(--lia-default-message-link-decoration)","forumMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","forumMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","forumSolvedColor":"#148563","blogColor":"#1CBAA0","blogFontFamily":"var(--lia-bs-font-family-base)","blogFontWeight":"var(--lia-default-message-font-weight)","blogLineHeight":"1.75","blogFontStyle":"var(--lia-default-message-font-style)","blogMessageLinkColor":"var(--lia-default-message-link-color)","blogMessageLinkDecoration":"var(--lia-default-message-link-decoration)","blogMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","blogMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","tkbColor":"#4C6B90","tkbFontFamily":"var(--lia-bs-font-family-base)","tkbFontWeight":"var(--lia-default-message-font-weight)","tkbLineHeight":"1.75","tkbFontStyle":"var(--lia-default-message-font-style)","tkbMessageLinkColor":"var(--lia-default-message-link-color)","tkbMessageLinkDecoration":"var(--lia-default-message-link-decoration)","tkbMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","tkbMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","qandaColor":"#4099E2","qandaFontFamily":"var(--lia-bs-font-family-base)","qandaFontWeight":"var(--lia-default-message-font-weight)","qandaLineHeight":"var(--lia-bs-line-height-base)","qandaFontStyle":"var(--lia-default-message-link-font-style)","qandaMessageLinkColor":"var(--lia-default-message-link-color)","qandaMessageLinkDecoration":"var(--lia-default-message-link-decoration)","qandaMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","qandaMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","qandaSolvedColor":"#3FA023","ideaColor":"#FF8000","ideaFontFamily":"var(--lia-bs-font-family-base)","ideaFontWeight":"var(--lia-default-message-font-weight)","ideaLineHeight":"var(--lia-bs-line-height-base)","ideaFontStyle":"var(--lia-default-message-font-style)","ideaMessageLinkColor":"var(--lia-default-message-link-color)","ideaMessageLinkDecoration":"var(--lia-default-message-link-decoration)","ideaMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","ideaMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","contestColor":"#FCC845","contestFontFamily":"var(--lia-bs-font-family-base)","contestFontWeight":"var(--lia-default-message-font-weight)","contestLineHeight":"var(--lia-bs-line-height-base)","contestFontStyle":"var(--lia-default-message-link-font-style)","contestMessageLinkColor":"var(--lia-default-message-link-color)","contestMessageLinkDecoration":"var(--lia-default-message-link-decoration)","contestMessageLinkFontStyle":"ITALIC","contestMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","occasionColor":"#D13A1F","occasionFontFamily":"var(--lia-bs-font-family-base)","occasionFontWeight":"var(--lia-default-message-font-weight)","occasionLineHeight":"var(--lia-bs-line-height-base)","occasionFontStyle":"var(--lia-default-message-font-style)","occasionMessageLinkColor":"var(--lia-default-message-link-color)","occasionMessageLinkDecoration":"var(--lia-default-message-link-decoration)","occasionMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","occasionMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","grouphubColor":"#333333","categoryColor":"#949494","communityColor":"#FFFFFF","productColor":"#949494","__typename":"CoreTypesThemeSettings"},"colors":{"black":"#000000","white":"#FFFFFF","gray100":"#F7F7F7","gray200":"#F7F7F7","gray300":"#E8E8E8","gray400":"#D9D9D9","gray500":"#CCCCCC","gray600":"#717171","gray700":"#707070","gray800":"#545454","gray900":"#333333","dark":"#545454","light":"#F7F7F7","primary":"#0069D4","secondary":"#333333","bodyText":"#1E1E1E","bodyBg":"#FFFFFF","info":"#409AE2","success":"#41C5AE","warning":"#FCC844","danger":"#BC341B","alertSystem":"#FF6600","textMuted":"#707070","highlight":"#FFFCAD","outline":"var(--lia-bs-primary)","custom":["#D3F5A4","#243A5E"],"__typename":"ColorsThemeSettings"},"divider":{"size":"3px","marginLeft":"4px","marginRight":"4px","borderRadius":"50%","bgColor":"var(--lia-bs-gray-600)","bgColorActive":"var(--lia-bs-gray-600)","__typename":"DividerThemeSettings"},"dropdown":{"fontSize":"var(--lia-bs-font-size-sm)","borderColor":"var(--lia-bs-border-color)","borderRadius":"var(--lia-bs-border-radius-sm)","dividerBg":"var(--lia-bs-gray-300)","itemPaddingY":"5px","itemPaddingX":"20px","headerColor":"var(--lia-bs-gray-700)","__typename":"DropdownThemeSettings"},"email":{"link":{"color":"#0069D4","hoverColor":"#0061c2","decoration":"none","hoverDecoration":"underline","__typename":"EmailLinkSettings"},"border":{"color":"#e4e4e4","__typename":"EmailBorderSettings"},"buttons":{"borderRadiusLg":"5px","paddingXLg":"16px","paddingYLg":"7px","fontWeight":"700","primaryTextColor":"#ffffff","primaryTextHoverColor":"#ffffff","primaryBgColor":"#0069D4","primaryBgHoverColor":"#005cb8","primaryBorder":"1px solid transparent","primaryBorderHover":"1px solid transparent","__typename":"EmailButtonsSettings"},"panel":{"borderRadius":"5px","borderColor":"#e4e4e4","__typename":"EmailPanelSettings"},"__typename":"EmailThemeSettings"},"emoji":{"skinToneDefault":"#ffcd43","skinToneLight":"#fae3c5","skinToneMediumLight":"#e2cfa5","skinToneMedium":"#daa478","skinToneMediumDark":"#a78058","skinToneDark":"#5e4d43","__typename":"EmojiThemeSettings"},"heading":{"color":"var(--lia-bs-body-color)","fontFamily":"Segoe UI","fontStyle":"NORMAL","fontWeight":"400","h1FontSize":"34px","h2FontSize":"32px","h3FontSize":"28px","h4FontSize":"24px","h5FontSize":"20px","h6FontSize":"16px","lineHeight":"1.3","subHeaderFontSize":"11px","subHeaderFontWeight":"500","h1LetterSpacing":"normal","h2LetterSpacing":"normal","h3LetterSpacing":"normal","h4LetterSpacing":"normal","h5LetterSpacing":"normal","h6LetterSpacing":"normal","subHeaderLetterSpacing":"2px","h1FontWeight":"var(--lia-bs-headings-font-weight)","h2FontWeight":"var(--lia-bs-headings-font-weight)","h3FontWeight":"var(--lia-bs-headings-font-weight)","h4FontWeight":"var(--lia-bs-headings-font-weight)","h5FontWeight":"var(--lia-bs-headings-font-weight)","h6FontWeight":"var(--lia-bs-headings-font-weight)","__typename":"HeadingThemeSettings"},"icons":{"size10":"10px","size12":"12px","size14":"14px","size16":"16px","size20":"20px","size24":"24px","size30":"30px","size40":"40px","size50":"50px","size60":"60px","size80":"80px","size120":"120px","size160":"160px","__typename":"IconsThemeSettings"},"imagePreview":{"bgColor":"var(--lia-bs-gray-900)","titleColor":"var(--lia-bs-white)","controlColor":"var(--lia-bs-white)","controlBgColor":"var(--lia-bs-gray-800)","__typename":"ImagePreviewThemeSettings"},"input":{"borderColor":"var(--lia-bs-gray-600)","disabledColor":"var(--lia-bs-gray-600)","focusBorderColor":"var(--lia-bs-primary)","labelMarginBottom":"10px","btnFontSize":"var(--lia-bs-font-size-sm)","focusBoxShadow":"0 0 0 3px hsla(var(--lia-bs-primary-h), var(--lia-bs-primary-s), var(--lia-bs-primary-l), 0.2)","checkLabelMarginBottom":"2px","checkboxBorderRadius":"3px","borderRadiusSm":"var(--lia-bs-border-radius-sm)","borderRadius":"var(--lia-bs-border-radius)","borderRadiusLg":"var(--lia-bs-border-radius-lg)","formTextMarginTop":"4px","textAreaBorderRadius":"var(--lia-bs-border-radius)","activeFillColor":"var(--lia-bs-primary)","__typename":"InputThemeSettings"},"loading":{"dotDarkColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.2)","dotLightColor":"hsla(var(--lia-bs-white-h), var(--lia-bs-white-s), var(--lia-bs-white-l), 0.5)","barDarkColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.06)","barLightColor":"hsla(var(--lia-bs-white-h), var(--lia-bs-white-s), var(--lia-bs-white-l), 0.4)","__typename":"LoadingThemeSettings"},"link":{"color":"var(--lia-bs-primary)","hoverColor":"hsl(var(--lia-bs-primary-h), var(--lia-bs-primary-s), calc(var(--lia-bs-primary-l) - 10%))","decoration":"none","hoverDecoration":"underline","__typename":"LinkThemeSettings"},"listGroup":{"itemPaddingY":"15px","itemPaddingX":"15px","borderColor":"var(--lia-bs-gray-300)","__typename":"ListGroupThemeSettings"},"modal":{"contentTextColor":"var(--lia-bs-body-color)","contentBg":"var(--lia-bs-white)","backgroundBg":"var(--lia-bs-black)","smSize":"440px","mdSize":"760px","lgSize":"1080px","backdropOpacity":0.3,"contentBoxShadowXs":"var(--lia-bs-box-shadow-sm)","contentBoxShadow":"var(--lia-bs-box-shadow)","headerFontWeight":"700","__typename":"ModalThemeSettings"},"navbar":{"position":"FIXED","background":{"attachment":null,"clip":null,"color":"var(--lia-bs-white)","imageAssetName":"","imageLastModified":"0","origin":null,"position":"CENTER_CENTER","repeat":"NO_REPEAT","size":"COVER","__typename":"BackgroundProps"},"backgroundOpacity":0.8,"paddingTop":"15px","paddingBottom":"15px","borderBottom":"1px solid var(--lia-bs-border-color)","boxShadow":"var(--lia-bs-box-shadow-sm)","brandMarginRight":"30px","brandMarginRightSm":"10px","brandLogoHeight":"30px","linkGap":"10px","linkJustifyContent":"flex-start","linkPaddingY":"5px","linkPaddingX":"10px","linkDropdownPaddingY":"9px","linkDropdownPaddingX":"var(--lia-nav-link-px)","linkColor":"var(--lia-bs-body-color)","linkHoverColor":"var(--lia-bs-primary)","linkFontSize":"var(--lia-bs-font-size-sm)","linkFontStyle":"NORMAL","linkFontWeight":"400","linkTextTransform":"NONE","linkLetterSpacing":"normal","linkBorderRadius":"var(--lia-bs-border-radius-sm)","linkBgColor":"transparent","linkBgHoverColor":"transparent","linkBorder":"none","linkBorderHover":"none","linkBoxShadow":"none","linkBoxShadowHover":"none","linkTextBorderBottom":"none","linkTextBorderBottomHover":"none","dropdownPaddingTop":"10px","dropdownPaddingBottom":"15px","dropdownPaddingX":"10px","dropdownMenuOffset":"2px","dropdownDividerMarginTop":"10px","dropdownDividerMarginBottom":"10px","dropdownBorderColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.08)","controllerBgHoverColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.1)","controllerIconColor":"var(--lia-bs-body-color)","controllerIconHoverColor":"var(--lia-bs-body-color)","controllerTextColor":"var(--lia-nav-controller-icon-color)","controllerTextHoverColor":"var(--lia-nav-controller-icon-hover-color)","controllerHighlightColor":"hsla(30, 100%, 50%)","controllerHighlightTextColor":"var(--lia-yiq-light)","controllerBorderRadius":"var(--lia-border-radius-50)","hamburgerColor":"var(--lia-nav-controller-icon-color)","hamburgerHoverColor":"var(--lia-nav-controller-icon-color)","hamburgerBgColor":"transparent","hamburgerBgHoverColor":"transparent","hamburgerBorder":"none","hamburgerBorderHover":"none","collapseMenuMarginLeft":"20px","collapseMenuDividerBg":"var(--lia-nav-link-color)","collapseMenuDividerOpacity":0.16,"__typename":"NavbarThemeSettings"},"pager":{"textColor":"var(--lia-bs-link-color)","textFontWeight":"var(--lia-font-weight-md)","textFontSize":"var(--lia-bs-font-size-sm)","__typename":"PagerThemeSettings"},"panel":{"bgColor":"var(--lia-bs-white)","borderRadius":"var(--lia-bs-border-radius)","borderColor":"var(--lia-bs-border-color)","boxShadow":"none","__typename":"PanelThemeSettings"},"popover":{"arrowHeight":"8px","arrowWidth":"16px","maxWidth":"300px","minWidth":"100px","headerBg":"var(--lia-bs-white)","borderColor":"var(--lia-bs-border-color)","borderRadius":"var(--lia-bs-border-radius)","boxShadow":"0 0.5rem 1rem hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.15)","__typename":"PopoverThemeSettings"},"prism":{"color":"#000000","bgColor":"#f5f2f0","fontFamily":"var(--font-family-monospace)","fontSize":"var(--lia-bs-font-size-base)","fontWeightBold":"var(--lia-bs-font-weight-bold)","fontStyleItalic":"italic","tabSize":2,"highlightColor":"#b3d4fc","commentColor":"#62707e","punctuationColor":"#6f6f6f","namespaceOpacity":"0.7","propColor":"#990055","selectorColor":"#517a00","operatorColor":"#906736","operatorBgColor":"hsla(0, 0%, 100%, 0.5)","keywordColor":"#0076a9","functionColor":"#d3284b","variableColor":"#c14700","__typename":"PrismThemeSettings"},"rte":{"bgColor":"var(--lia-bs-white)","borderRadius":"var(--lia-panel-border-radius)","boxShadow":" var(--lia-panel-box-shadow)","customColor1":"#bfedd2","customColor2":"#fbeeb8","customColor3":"#f8cac6","customColor4":"#eccafa","customColor5":"#c2e0f4","customColor6":"#2dc26b","customColor7":"#f1c40f","customColor8":"#e03e2d","customColor9":"#b96ad9","customColor10":"#3598db","customColor11":"#169179","customColor12":"#e67e23","customColor13":"#ba372a","customColor14":"#843fa1","customColor15":"#236fa1","customColor16":"#ecf0f1","customColor17":"#ced4d9","customColor18":"#95a5a6","customColor19":"#7e8c8d","customColor20":"#34495e","customColor21":"#000000","customColor22":"#ffffff","defaultMessageHeaderMarginTop":"40px","defaultMessageHeaderMarginBottom":"20px","defaultMessageItemMarginTop":"0","defaultMessageItemMarginBottom":"10px","diffAddedColor":"hsla(170, 53%, 51%, 0.4)","diffChangedColor":"hsla(43, 97%, 63%, 0.4)","diffNoneColor":"hsla(0, 0%, 80%, 0.4)","diffRemovedColor":"hsla(9, 74%, 47%, 0.4)","specialMessageHeaderMarginTop":"40px","specialMessageHeaderMarginBottom":"20px","specialMessageItemMarginTop":"0","specialMessageItemMarginBottom":"10px","__typename":"RteThemeSettings"},"tags":{"bgColor":"var(--lia-bs-gray-200)","bgHoverColor":"var(--lia-bs-gray-400)","borderRadius":"var(--lia-bs-border-radius-sm)","color":"var(--lia-bs-body-color)","hoverColor":"var(--lia-bs-body-color)","fontWeight":"var(--lia-font-weight-md)","fontSize":"var(--lia-font-size-xxs)","textTransform":"UPPERCASE","letterSpacing":"0.5px","__typename":"TagsThemeSettings"},"toasts":{"borderRadius":"var(--lia-bs-border-radius)","paddingX":"12px","__typename":"ToastsThemeSettings"},"typography":{"fontFamilyBase":"Segoe UI","fontStyleBase":"NORMAL","fontWeightBase":"400","fontWeightLight":"300","fontWeightNormal":"400","fontWeightMd":"500","fontWeightBold":"700","letterSpacingSm":"normal","letterSpacingXs":"normal","lineHeightBase":"1.5","fontSizeBase":"16px","fontSizeXxs":"11px","fontSizeXs":"12px","fontSizeSm":"14px","fontSizeLg":"20px","fontSizeXl":"24px","smallFontSize":"14px","customFonts":[{"source":"SERVER","name":"Segoe UI","styles":[{"style":"NORMAL","weight":"400","__typename":"FontStyleData"},{"style":"NORMAL","weight":"300","__typename":"FontStyleData"},{"style":"NORMAL","weight":"600","__typename":"FontStyleData"},{"style":"NORMAL","weight":"700","__typename":"FontStyleData"},{"style":"ITALIC","weight":"400","__typename":"FontStyleData"}],"assetNames":["SegoeUI-normal-400.woff2","SegoeUI-normal-300.woff2","SegoeUI-normal-600.woff2","SegoeUI-normal-700.woff2","SegoeUI-italic-400.woff2"],"__typename":"CustomFont"},{"source":"SERVER","name":"MWF Fluent Icons","styles":[{"style":"NORMAL","weight":"400","__typename":"FontStyleData"}],"assetNames":["MWFFluentIcons-normal-400.woff2"],"__typename":"CustomFont"}],"__typename":"TypographyThemeSettings"},"unstyledListItem":{"marginBottomSm":"5px","marginBottomMd":"10px","marginBottomLg":"15px","marginBottomXl":"20px","marginBottomXxl":"25px","__typename":"UnstyledListItemThemeSettings"},"yiq":{"light":"#ffffff","dark":"#000000","__typename":"YiqThemeSettings"},"colorLightness":{"primaryDark":0.36,"primaryLight":0.74,"primaryLighter":0.89,"primaryLightest":0.95,"infoDark":0.39,"infoLight":0.72,"infoLighter":0.85,"infoLightest":0.93,"successDark":0.24,"successLight":0.62,"successLighter":0.8,"successLightest":0.91,"warningDark":0.39,"warningLight":0.68,"warningLighter":0.84,"warningLightest":0.93,"dangerDark":0.41,"dangerLight":0.72,"dangerLighter":0.89,"dangerLightest":0.95,"__typename":"ColorLightnessThemeSettings"},"localOverride":false,"__typename":"Theme"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/common/Loading/LoadingDot-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/common/Loading/LoadingDot-1745505307000","value":{"title":"Loading..."},"localOverride":false},"CachedAsset:quilt:o365.prod:pages/blogs/BlogMessagePage:board:FastTrackforAzureBlog-1746563244026":{"__typename":"CachedAsset","id":"quilt:o365.prod:pages/blogs/BlogMessagePage:board:FastTrackforAzureBlog-1746563244026","value":{"id":"BlogMessagePage","container":{"id":"Common","headerProps":{"backgroundImageProps":null,"backgroundColor":null,"addComponents":null,"removeComponents":["community.widget.bannerWidget"],"componentOrder":null,"__typename":"QuiltContainerSectionProps"},"headerComponentProps":{"community.widget.breadcrumbWidget":{"disableLastCrumbForDesktop":false}},"footerProps":null,"footerComponentProps":null,"items":[{"id":"blog-article","layout":"ONE_COLUMN","bgColor":null,"showTitle":null,"showDescription":null,"textPosition":null,"textColor":null,"sectionEditLevel":"LOCKED","bgImage":null,"disableSpacing":null,"edgeToEdgeDisplay":null,"fullHeight":null,"showBorder":null,"__typename":"OneColumnQuiltSection","columnMap":{"main":[{"id":"blogs.widget.blogArticleWidget","className":"lia-blog-container","props":null,"__typename":"QuiltComponent"}],"__typename":"OneSectionColumns"}},{"id":"section-1729184836777","layout":"MAIN_SIDE","bgColor":"transparent","showTitle":false,"showDescription":false,"textPosition":"CENTER","textColor":"var(--lia-bs-body-color)","sectionEditLevel":null,"bgImage":null,"disableSpacing":null,"edgeToEdgeDisplay":null,"fullHeight":null,"showBorder":null,"__typename":"MainSideQuiltSection","columnMap":{"main":[],"side":[],"__typename":"MainSideSectionColumns"}}],"__typename":"QuiltContainer"},"__typename":"Quilt","localOverride":false},"localOverride":false},"CachedAsset:text:en_US-components/common/EmailVerification-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/common/EmailVerification-1745505307000","value":{"email.verification.title":"Email Verification Required","email.verification.message.update.email":"To participate in the community, you must first verify your email address. The verification email was sent to {email}. To change your email, visit My Settings.","email.verification.message.resend.email":"To participate in the community, you must first verify your email address. The verification email was sent to {email}. Resend email."},"localOverride":false},"CachedAsset:text:en_US-pages/blogs/BlogMessagePage-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-pages/blogs/BlogMessagePage-1745505307000","value":{"title":"{contextMessageSubject} | {communityTitle}","errorMissing":"This blog post cannot be found","name":"Blog Message Page","section.blog-article.title":"Blog Post","archivedMessageTitle":"This Content Has Been Archived","section.section-1729184836777.title":"","section.section-1729184836777.description":"","section.CncIde.title":"Blog Post","section.tifEmD.description":"","section.tifEmD.title":""},"localOverride":false},"CachedAsset:quiltWrapper:o365.prod:Common:1746563184305":{"__typename":"CachedAsset","id":"quiltWrapper:o365.prod:Common:1746563184305","value":{"id":"Common","header":{"backgroundImageProps":{"assetName":null,"backgroundSize":"COVER","backgroundRepeat":"NO_REPEAT","backgroundPosition":"CENTER_CENTER","lastModified":null,"__typename":"BackgroundImageProps"},"backgroundColor":"transparent","items":[{"id":"community.widget.navbarWidget","props":{"showUserName":true,"showRegisterLink":true,"useIconLanguagePicker":true,"useLabelLanguagePicker":true,"className":"QuiltComponent_lia-component-edit-mode__0nCcm","links":{"sideLinks":[],"mainLinks":[{"children":[],"linkType":"INTERNAL","id":"gxcuf89792","params":{},"routeName":"CommunityPage"},{"children":[],"linkType":"EXTERNAL","id":"external-link","url":"/Directory","target":"SELF"},{"children":[{"linkType":"INTERNAL","id":"microsoft365","params":{"categoryId":"microsoft365"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"windows","params":{"categoryId":"Windows"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"Common-microsoft365-copilot-link","params":{"categoryId":"Microsoft365Copilot"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"microsoft-teams","params":{"categoryId":"MicrosoftTeams"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"microsoft-securityand-compliance","params":{"categoryId":"microsoft-security"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"azure","params":{"categoryId":"Azure"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"Common-content_management-link","params":{"categoryId":"Content_Management"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"exchange","params":{"categoryId":"Exchange"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"windows-server","params":{"categoryId":"Windows-Server"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"outlook","params":{"categoryId":"Outlook"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"microsoft-endpoint-manager","params":{"categoryId":"microsoftintune"},"routeName":"CategoryPage"},{"linkType":"EXTERNAL","id":"external-link-2","url":"/Directory","target":"SELF"}],"linkType":"EXTERNAL","id":"communities","url":"/","target":"BLANK"},{"children":[{"linkType":"INTERNAL","id":"a-i","params":{"categoryId":"AI"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"education-sector","params":{"categoryId":"EducationSector"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"partner-community","params":{"categoryId":"PartnerCommunity"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"i-t-ops-talk","params":{"categoryId":"ITOpsTalk"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"healthcare-and-life-sciences","params":{"categoryId":"HealthcareAndLifeSciences"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"microsoft-mechanics","params":{"categoryId":"MicrosoftMechanics"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"public-sector","params":{"categoryId":"PublicSector"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"s-m-b","params":{"categoryId":"MicrosoftforNonprofits"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"io-t","params":{"categoryId":"IoT"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"startupsat-microsoft","params":{"categoryId":"StartupsatMicrosoft"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"driving-adoption","params":{"categoryId":"DrivingAdoption"},"routeName":"CategoryPage"},{"linkType":"EXTERNAL","id":"external-link-1","url":"/Directory","target":"SELF"}],"linkType":"EXTERNAL","id":"communities-1","url":"/","target":"SELF"},{"children":[],"linkType":"EXTERNAL","id":"external","url":"/Blogs","target":"SELF"},{"children":[],"linkType":"EXTERNAL","id":"external-1","url":"/Events","target":"SELF"},{"children":[{"linkType":"INTERNAL","id":"microsoft-learn-1","params":{"categoryId":"MicrosoftLearn"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"microsoft-learn-blog","params":{"boardId":"MicrosoftLearnBlog","categoryId":"MicrosoftLearn"},"routeName":"BlogBoardPage"},{"linkType":"EXTERNAL","id":"external-10","url":"https://learningroomdirectory.microsoft.com/","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-3","url":"https://docs.microsoft.com/learn/dynamics365/?WT.mc_id=techcom_header-webpage-m365","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-4","url":"https://docs.microsoft.com/learn/m365/?wt.mc_id=techcom_header-webpage-m365","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-5","url":"https://docs.microsoft.com/learn/topics/sci/?wt.mc_id=techcom_header-webpage-m365","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-6","url":"https://docs.microsoft.com/learn/powerplatform/?wt.mc_id=techcom_header-webpage-powerplatform","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-7","url":"https://docs.microsoft.com/learn/github/?wt.mc_id=techcom_header-webpage-github","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-8","url":"https://docs.microsoft.com/learn/teams/?wt.mc_id=techcom_header-webpage-teams","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-9","url":"https://docs.microsoft.com/learn/dotnet/?wt.mc_id=techcom_header-webpage-dotnet","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-2","url":"https://docs.microsoft.com/learn/azure/?WT.mc_id=techcom_header-webpage-m365","target":"BLANK"}],"linkType":"INTERNAL","id":"microsoft-learn","params":{"categoryId":"MicrosoftLearn"},"routeName":"CategoryPage"},{"children":[],"linkType":"INTERNAL","id":"community-info-center","params":{"categoryId":"Community-Info-Center"},"routeName":"CategoryPage"}]},"style":{"boxShadow":"var(--lia-bs-box-shadow-sm)","controllerHighlightColor":"hsla(30, 100%, 50%)","linkFontWeight":"400","dropdownDividerMarginBottom":"10px","hamburgerBorderHover":"none","linkBoxShadowHover":"none","linkFontSize":"14px","backgroundOpacity":0.8,"controllerBorderRadius":"var(--lia-border-radius-50)","hamburgerBgColor":"transparent","hamburgerColor":"var(--lia-nav-controller-icon-color)","linkTextBorderBottom":"none","brandLogoHeight":"30px","linkBgHoverColor":"transparent","linkLetterSpacing":"normal","collapseMenuDividerOpacity":0.16,"dropdownPaddingBottom":"15px","paddingBottom":"15px","dropdownMenuOffset":"2px","hamburgerBgHoverColor":"transparent","borderBottom":"1px solid var(--lia-bs-border-color)","hamburgerBorder":"none","dropdownPaddingX":"10px","brandMarginRightSm":"10px","linkBoxShadow":"none","collapseMenuDividerBg":"var(--lia-nav-link-color)","linkColor":"var(--lia-bs-body-color)","linkJustifyContent":"flex-start","dropdownPaddingTop":"10px","controllerHighlightTextColor":"var(--lia-yiq-dark)","controllerTextColor":"var(--lia-nav-controller-icon-color)","background":{"imageAssetName":"","color":"var(--lia-bs-white)","size":"COVER","repeat":"NO_REPEAT","position":"CENTER_CENTER","imageLastModified":""},"linkBorderRadius":"var(--lia-bs-border-radius-sm)","linkHoverColor":"var(--lia-bs-body-color)","position":"FIXED","linkBorder":"none","linkTextBorderBottomHover":"2px solid var(--lia-bs-body-color)","brandMarginRight":"30px","hamburgerHoverColor":"var(--lia-nav-controller-icon-color)","linkBorderHover":"none","collapseMenuMarginLeft":"20px","linkFontStyle":"NORMAL","controllerTextHoverColor":"var(--lia-nav-controller-icon-hover-color)","linkPaddingX":"10px","linkPaddingY":"5px","paddingTop":"15px","linkTextTransform":"NONE","dropdownBorderColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.08)","controllerBgHoverColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.1)","linkBgColor":"transparent","linkDropdownPaddingX":"var(--lia-nav-link-px)","linkDropdownPaddingY":"9px","controllerIconColor":"var(--lia-bs-body-color)","dropdownDividerMarginTop":"10px","linkGap":"10px","controllerIconHoverColor":"var(--lia-bs-body-color)"},"showSearchIcon":false,"languagePickerStyle":"iconAndLabel"},"__typename":"QuiltComponent"},{"id":"community.widget.breadcrumbWidget","props":{"backgroundColor":"transparent","linkHighlightColor":"var(--lia-bs-primary)","visualEffects":{"showBottomBorder":true},"linkTextColor":"var(--lia-bs-gray-700)"},"__typename":"QuiltComponent"},{"id":"custom.widget.community_banner","props":{"widgetVisibility":"signedInOrAnonymous","useTitle":true,"usePageWidth":false,"useBackground":false,"title":"","lazyLoad":false},"__typename":"QuiltComponent"},{"id":"custom.widget.HeroBanner","props":{"widgetVisibility":"signedInOrAnonymous","usePageWidth":false,"useTitle":true,"cMax_items":3,"useBackground":false,"title":"","lazyLoad":false,"widgetChooser":"custom.widget.HeroBanner"},"__typename":"QuiltComponent"}],"__typename":"QuiltWrapperSection"},"footer":{"backgroundImageProps":{"assetName":null,"backgroundSize":"COVER","backgroundRepeat":"NO_REPEAT","backgroundPosition":"CENTER_CENTER","lastModified":null,"__typename":"BackgroundImageProps"},"backgroundColor":"transparent","items":[{"id":"custom.widget.MicrosoftFooter","props":{"widgetVisibility":"signedInOrAnonymous","useTitle":true,"useBackground":false,"title":"","lazyLoad":false},"__typename":"QuiltComponent"}],"__typename":"QuiltWrapperSection"},"__typename":"QuiltWrapper","localOverride":false},"localOverride":false},"CachedAsset:text:en_US-components/common/ActionFeedback-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/common/ActionFeedback-1745505307000","value":{"joinedGroupHub.title":"Welcome","joinedGroupHub.message":"You are now a member of this group and are subscribed to updates.","groupHubInviteNotFound.title":"Invitation Not Found","groupHubInviteNotFound.message":"Sorry, we could not find your invitation to the group. The owner may have canceled the invite.","groupHubNotFound.title":"Group Not Found","groupHubNotFound.message":"The grouphub you tried to join does not exist. It may have been deleted.","existingGroupHubMember.title":"Already Joined","existingGroupHubMember.message":"You are already a member of this group.","accountLocked.title":"Account Locked","accountLocked.message":"Your account has been locked due to multiple failed attempts. Try again in {lockoutTime} minutes.","editedGroupHub.title":"Changes Saved","editedGroupHub.message":"Your group has been updated.","leftGroupHub.title":"Goodbye","leftGroupHub.message":"You are no longer a member of this group and will not receive future updates.","deletedGroupHub.title":"Deleted","deletedGroupHub.message":"The group has been deleted.","groupHubCreated.title":"Group Created","groupHubCreated.message":"{groupHubName} is ready to use","accountClosed.title":"Account Closed","accountClosed.message":"The account has been closed and you will now be redirected to the homepage","resetTokenExpired.title":"Reset Password Link has Expired","resetTokenExpired.message":"Try resetting your password again","invalidUrl.title":"Invalid URL","invalidUrl.message":"The URL you're using is not recognized. Verify your URL and try again.","accountClosedForUser.title":"Account Closed","accountClosedForUser.message":"{userName}'s account is closed","inviteTokenInvalid.title":"Invitation Invalid","inviteTokenInvalid.message":"Your invitation to the community has been canceled or expired.","inviteTokenError.title":"Invitation Verification Failed","inviteTokenError.message":"The url you are utilizing is not recognized. Verify your URL and try again","pageNotFound.title":"Access Denied","pageNotFound.message":"You do not have access to this area of the community or it doesn't exist","eventAttending.title":"Responded as Attending","eventAttending.message":"You'll be notified when there's new activity and reminded as the event approaches","eventInterested.title":"Responded as Interested","eventInterested.message":"You'll be notified when there's new activity and reminded as the event approaches","eventNotFound.title":"Event Not Found","eventNotFound.message":"The event you tried to respond to does not exist.","redirectToRelatedPage.title":"Showing Related Content","redirectToRelatedPageForBaseUsers.title":"Showing Related Content","redirectToRelatedPageForBaseUsers.message":"The content you are trying to access is archived","redirectToRelatedPage.message":"The content you are trying to access is archived","relatedUrl.archivalLink.flyoutMessage":"The content you are trying to access is archived View Archived Content"},"localOverride":false},"QueryVariables:TopicReplyList:message:3834619:5":{"__typename":"QueryVariables","id":"TopicReplyList:message:3834619:5","value":{"id":"message:3834619","first":10,"sorts":{"postTime":{"direction":"DESC"}},"repliesFirst":3,"repliesFirstDepthThree":1,"repliesSorts":{"postTime":{"direction":"DESC"}},"useAvatar":true,"useAuthorLogin":true,"useAuthorRank":true,"useBody":true,"useKudosCount":true,"useTimeToRead":false,"useMedia":false,"useReadOnlyIcon":false,"useRepliesCount":true,"useSearchSnippet":false,"useAcceptedSolutionButton":false,"useSolvedBadge":false,"useAttachments":false,"attachmentsFirst":5,"useTags":true,"useNodeAncestors":false,"useUserHoverCard":false,"useNodeHoverCard":false,"useModerationStatus":true,"usePreviewSubjectModal":false,"useMessageStatus":true}},"ROOT_MUTATION":{"__typename":"Mutation"},"CachedAsset:component:custom.widget.community_banner-en-us-1746563280879":{"__typename":"CachedAsset","id":"component:custom.widget.community_banner-en-us-1746563280879","value":{"component":{"id":"custom.widget.community_banner","template":{"id":"community_banner","markupLanguage":"HANDLEBARS","style":".community-banner {\n a.top-bar.btn {\n top: 0px;\n width: 100%;\n z-index: 999;\n text-align: center;\n left: 0px;\n background: #0068b8;\n color: white;\n padding: 10px 0px;\n display: block;\n box-shadow: none !important;\n border: none !important;\n border-radius: none !important;\n margin: 0px !important;\n font-size: 14px;\n }\n}\n","texts":{},"defaults":{"config":{"applicablePages":[],"description":"community announcement text","fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[],"__typename":"ComponentProperties"},"components":[{"id":"custom.widget.community_banner","form":null,"config":null,"props":[],"__typename":"Component"}],"grouping":"CUSTOM","__typename":"ComponentTemplate"},"properties":{"config":{"applicablePages":[],"description":"community announcement text","fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[],"__typename":"ComponentProperties"},"form":null,"__typename":"Component","localOverride":false},"globalCss":{"css":".custom_widget_community_banner_community-banner_1x9u2_1 {\n a.custom_widget_community_banner_top-bar_1x9u2_2.custom_widget_community_banner_btn_1x9u2_2 {\n top: 0;\n width: 100%;\n z-index: 999;\n text-align: center;\n left: 0;\n background: #0068b8;\n color: white;\n padding: 0.625rem 0;\n display: block;\n box-shadow: none !important;\n border: none !important;\n border-radius: none !important;\n margin: 0 !important;\n font-size: 0.875rem;\n }\n}\n","tokens":{"community-banner":"custom_widget_community_banner_community-banner_1x9u2_1","top-bar":"custom_widget_community_banner_top-bar_1x9u2_2","btn":"custom_widget_community_banner_btn_1x9u2_2"}},"form":null},"localOverride":false},"CachedAsset:component:custom.widget.HeroBanner-en-us-1746563280879":{"__typename":"CachedAsset","id":"component:custom.widget.HeroBanner-en-us-1746563280879","value":{"component":{"id":"custom.widget.HeroBanner","template":{"id":"HeroBanner","markupLanguage":"REACT","style":null,"texts":{"searchPlaceholderText":"Search this community","followActionText":"Follow","unfollowActionText":"Following","searchOnHoverText":"Please enter your search term(s) and then press return key to complete a search.","blogs.sidebar.pagetitle":"Latest Blogs | Microsoft Tech Community","followThisNode":"Follow this node","unfollowThisNode":"Unfollow this node"},"defaults":{"config":{"applicablePages":[],"description":null,"fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[{"id":"max_items","dataType":"NUMBER","list":false,"defaultValue":"3","label":"Max Items","description":"The maximum number of items to display in the carousel","possibleValues":null,"control":"INPUT","__typename":"PropDefinition"}],"__typename":"ComponentProperties"},"components":[{"id":"custom.widget.HeroBanner","form":{"fields":[{"id":"widgetChooser","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"title","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useTitle","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useBackground","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"widgetVisibility","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"moreOptions","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"cMax_items","validation":null,"noValidation":null,"dataType":"NUMBER","list":false,"control":"INPUT","defaultValue":"3","label":"Max Items","description":"The maximum number of items to display in the carousel","possibleValues":null,"__typename":"FormField"}],"layout":{"rows":[{"id":"widgetChooserGroup","type":"fieldset","as":null,"items":[{"id":"widgetChooser","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"titleGroup","type":"fieldset","as":null,"items":[{"id":"title","className":null,"__typename":"FormFieldRef"},{"id":"useTitle","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"useBackground","type":"fieldset","as":null,"items":[{"id":"useBackground","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"widgetVisibility","type":"fieldset","as":null,"items":[{"id":"widgetVisibility","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"moreOptionsGroup","type":"fieldset","as":null,"items":[{"id":"moreOptions","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"componentPropsGroup","type":"fieldset","as":null,"items":[{"id":"cMax_items","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"}],"actionButtons":null,"className":"custom_widget_HeroBanner_form","formGroupFieldSeparator":"divider","__typename":"FormLayout"},"__typename":"Form"},"config":null,"props":[],"__typename":"Component"}],"grouping":"CUSTOM","__typename":"ComponentTemplate"},"properties":{"config":{"applicablePages":[],"description":null,"fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[{"id":"max_items","dataType":"NUMBER","list":false,"defaultValue":"3","label":"Max Items","description":"The maximum number of items to display in the carousel","possibleValues":null,"control":"INPUT","__typename":"PropDefinition"}],"__typename":"ComponentProperties"},"form":{"fields":[{"id":"widgetChooser","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"title","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useTitle","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useBackground","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"widgetVisibility","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"moreOptions","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"cMax_items","validation":null,"noValidation":null,"dataType":"NUMBER","list":false,"control":"INPUT","defaultValue":"3","label":"Max Items","description":"The maximum number of items to display in the carousel","possibleValues":null,"__typename":"FormField"}],"layout":{"rows":[{"id":"widgetChooserGroup","type":"fieldset","as":null,"items":[{"id":"widgetChooser","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"titleGroup","type":"fieldset","as":null,"items":[{"id":"title","className":null,"__typename":"FormFieldRef"},{"id":"useTitle","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"useBackground","type":"fieldset","as":null,"items":[{"id":"useBackground","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"widgetVisibility","type":"fieldset","as":null,"items":[{"id":"widgetVisibility","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"moreOptionsGroup","type":"fieldset","as":null,"items":[{"id":"moreOptions","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"componentPropsGroup","type":"fieldset","as":null,"items":[{"id":"cMax_items","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"}],"actionButtons":null,"className":"custom_widget_HeroBanner_form","formGroupFieldSeparator":"divider","__typename":"FormLayout"},"__typename":"Form"},"__typename":"Component","localOverride":false},"globalCss":null,"form":{"fields":[{"id":"widgetChooser","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"title","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useTitle","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useBackground","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"widgetVisibility","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"moreOptions","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"cMax_items","validation":null,"noValidation":null,"dataType":"NUMBER","list":false,"control":"INPUT","defaultValue":"3","label":"Max Items","description":"The maximum number of items to display in the carousel","possibleValues":null,"__typename":"FormField"}],"layout":{"rows":[{"id":"widgetChooserGroup","type":"fieldset","as":null,"items":[{"id":"widgetChooser","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"titleGroup","type":"fieldset","as":null,"items":[{"id":"title","className":null,"__typename":"FormFieldRef"},{"id":"useTitle","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"useBackground","type":"fieldset","as":null,"items":[{"id":"useBackground","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"widgetVisibility","type":"fieldset","as":null,"items":[{"id":"widgetVisibility","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"moreOptionsGroup","type":"fieldset","as":null,"items":[{"id":"moreOptions","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"componentPropsGroup","type":"fieldset","as":null,"items":[{"id":"cMax_items","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"}],"actionButtons":null,"className":"custom_widget_HeroBanner_form","formGroupFieldSeparator":"divider","__typename":"FormLayout"},"__typename":"Form"}},"localOverride":false},"CachedAsset:component:custom.widget.MicrosoftFooter-en-us-1746563280879":{"__typename":"CachedAsset","id":"component:custom.widget.MicrosoftFooter-en-us-1746563280879","value":{"component":{"id":"custom.widget.MicrosoftFooter","template":{"id":"MicrosoftFooter","markupLanguage":"HANDLEBARS","style":".context-uhf {\n min-width: 280px;\n font-size: 15px;\n box-sizing: border-box;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n & *,\n & *:before,\n & *:after {\n box-sizing: inherit;\n }\n a.c-uhff-link {\n color: #616161;\n word-break: break-word;\n text-decoration: none;\n }\n &a:link,\n &a:focus,\n &a:hover,\n &a:active,\n &a:visited {\n text-decoration: none;\n color: inherit;\n }\n & div {\n font-family: 'Segoe UI', SegoeUI, 'Helvetica Neue', Helvetica, Arial, sans-serif;\n }\n}\n.c-uhff {\n background: #f2f2f2;\n margin: -1.5625;\n width: auto;\n height: auto;\n}\n.c-uhff-nav {\n margin: 0 auto;\n max-width: calc(1600px + 10%);\n padding: 0 5%;\n box-sizing: inherit;\n &:before,\n &:after {\n content: ' ';\n display: table;\n clear: left;\n }\n @media only screen and (max-width: 1083px) {\n padding-left: 12px;\n }\n .c-heading-4 {\n color: #616161;\n word-break: break-word;\n font-size: 15px;\n line-height: 20px;\n padding: 36px 0 4px;\n font-weight: 600;\n }\n .c-uhff-nav-row {\n .c-uhff-nav-group {\n display: block;\n float: left;\n min-height: 1px;\n vertical-align: text-top;\n padding: 0 12px;\n width: 100%;\n zoom: 1;\n &:first-child {\n padding-left: 0;\n @media only screen and (max-width: 1083px) {\n padding-left: 12px;\n }\n }\n @media only screen and (min-width: 540px) and (max-width: 1082px) {\n width: 33.33333%;\n }\n @media only screen and (min-width: 1083px) {\n width: 16.6666666667%;\n }\n ul.c-list.f-bare {\n font-size: 11px;\n line-height: 16px;\n margin-top: 0;\n margin-bottom: 0;\n padding-left: 0;\n list-style-type: none;\n li {\n word-break: break-word;\n padding: 8px 0;\n margin: 0;\n }\n }\n }\n }\n}\n.c-uhff-base {\n background: #f2f2f2;\n margin: 0 auto;\n max-width: calc(1600px + 10%);\n padding: 30px 5% 16px;\n &:before,\n &:after {\n content: ' ';\n display: table;\n }\n &:after {\n clear: both;\n }\n a.c-uhff-ccpa {\n font-size: 11px;\n line-height: 16px;\n float: left;\n margin: 3px 0;\n }\n a.c-uhff-ccpa:hover {\n text-decoration: underline;\n }\n ul.c-list {\n font-size: 11px;\n line-height: 16px;\n float: right;\n margin: 3px 0;\n color: #616161;\n li {\n padding: 0 24px 4px 0;\n display: inline-block;\n }\n }\n .c-list.f-bare {\n padding-left: 0;\n list-style-type: none;\n }\n @media only screen and (max-width: 1083px) {\n display: flex;\n flex-wrap: wrap;\n padding: 30px 24px 16px;\n }\n}\n\n.social-share {\n position: fixed;\n top: 60%;\n transform: translateY(-50%);\n left: 0;\n z-index: 1000;\n}\n\n.sharing-options {\n list-style: none;\n padding: 0;\n margin: 0;\n display: block;\n flex-direction: column;\n background-color: white;\n width: 43px;\n border-radius: 0px 7px 7px 0px;\n}\n.linkedin-icon {\n border-top-right-radius: 7px;\n}\n.linkedin-icon:hover {\n border-radius: 0;\n}\n.social-share-rss-image {\n border-bottom-right-radius: 7px;\n}\n.social-share-rss-image:hover {\n border-radius: 0;\n}\n\n.social-link-footer {\n position: relative;\n display: block;\n margin: -2px 0;\n transition: all 0.2s ease;\n}\n.social-link-footer:hover .linkedin-icon {\n border-radius: 0;\n}\n.social-link-footer:hover .social-share-rss-image {\n border-radius: 0;\n}\n\n.social-link-footer img {\n width: 40px;\n height: auto;\n transition: filter 0.3s ease;\n}\n\n.social-share-list {\n width: 40px;\n}\n.social-share-rss-image {\n width: 40px;\n}\n\n.share-icon {\n border: 2px solid transparent;\n display: inline-block;\n position: relative;\n}\n\n.share-icon:hover {\n opacity: 1;\n border: 2px solid white;\n box-sizing: border-box;\n}\n\n.share-icon:hover .label {\n opacity: 1;\n visibility: visible;\n border: 2px solid white;\n box-sizing: border-box;\n border-left: none;\n}\n\n.label {\n position: absolute;\n left: 100%;\n white-space: nowrap;\n opacity: 0;\n visibility: hidden;\n transition: all 0.2s ease;\n color: white;\n border-radius: 0 10 0 10px;\n top: 50%;\n transform: translateY(-50%);\n height: 40px;\n border-radius: 0 6px 6px 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px 5px 20px 8px;\n margin-left: -1px;\n}\n.linkedin {\n background-color: #0474b4;\n}\n.facebook {\n background-color: #3c5c9c;\n}\n.twitter {\n background-color: white;\n color: black;\n}\n.reddit {\n background-color: #fc4404;\n}\n.mail {\n background-color: #848484;\n}\n.bluesky {\n background-color: white;\n color: black;\n}\n.rss {\n background-color: #ec7b1c;\n}\n#RSS {\n width: 40px;\n height: 40px;\n}\n\n@media (max-width: 991px) {\n .social-share {\n display: none;\n }\n}\n","texts":{"New tab":"What's New","New 1":"Surface Laptop Studio 2","New 2":"Surface Laptop Go 3","New 3":"Surface Pro 9","New 4":"Surface Laptop 5","New 5":"Surface Studio 2+","New 6":"Copilot in Windows","New 7":"Microsoft 365","New 8":"Windows 11 apps","Store tab":"Microsoft Store","Store 1":"Account Profile","Store 2":"Download Center","Store 3":"Microsoft Store Support","Store 4":"Returns","Store 5":"Order tracking","Store 6":"Certified Refurbished","Store 7":"Microsoft Store Promise","Store 8":"Flexible Payments","Education tab":"Education","Edu 1":"Microsoft in education","Edu 2":"Devices for education","Edu 3":"Microsoft Teams for Education","Edu 4":"Microsoft 365 Education","Edu 5":"How to buy for your school","Edu 6":"Educator Training and development","Edu 7":"Deals for students and parents","Edu 8":"Azure for students","Business tab":"Business","Bus 1":"Microsoft Cloud","Bus 2":"Microsoft Security","Bus 3":"Dynamics 365","Bus 4":"Microsoft 365","Bus 5":"Microsoft Power Platform","Bus 6":"Microsoft Teams","Bus 7":"Microsoft Industry","Bus 8":"Small Business","Developer tab":"Developer & IT","Dev 1":"Azure","Dev 2":"Developer Center","Dev 3":"Documentation","Dev 4":"Microsoft Learn","Dev 5":"Microsoft Tech Community","Dev 6":"Azure Marketplace","Dev 7":"AppSource","Dev 8":"Visual Studio","Company tab":"Company","Com 1":"Careers","Com 2":"About Microsoft","Com 3":"Company News","Com 4":"Privacy at Microsoft","Com 5":"Investors","Com 6":"Diversity and inclusion","Com 7":"Accessiblity","Com 8":"Sustainibility"},"defaults":{"config":{"applicablePages":[],"description":"The Microsoft Footer","fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[],"__typename":"ComponentProperties"},"components":[{"id":"custom.widget.MicrosoftFooter","form":null,"config":null,"props":[],"__typename":"Component"}],"grouping":"CUSTOM","__typename":"ComponentTemplate"},"properties":{"config":{"applicablePages":[],"description":"The Microsoft Footer","fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[],"__typename":"ComponentProperties"},"form":null,"__typename":"Component","localOverride":false},"globalCss":{"css":".custom_widget_MicrosoftFooter_context-uhf_105bp_1 {\n min-width: 17.5rem;\n font-size: 0.9375rem;\n box-sizing: border-box;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n & *,\n & *:before,\n & *:after {\n box-sizing: inherit;\n }\n a.custom_widget_MicrosoftFooter_c-uhff-link_105bp_12 {\n color: #616161;\n word-break: break-word;\n text-decoration: none;\n }\n &a:link,\n &a:focus,\n &a:hover,\n &a:active,\n &a:visited {\n text-decoration: none;\n color: inherit;\n }\n & div {\n font-family: 'Segoe UI', SegoeUI, 'Helvetica Neue', Helvetica, Arial, sans-serif;\n }\n}\n.custom_widget_MicrosoftFooter_c-uhff_105bp_12 {\n background: #f2f2f2;\n margin: -1.5625;\n width: auto;\n height: auto;\n}\n.custom_widget_MicrosoftFooter_c-uhff-nav_105bp_35 {\n margin: 0 auto;\n max-width: calc(100rem + 10%);\n padding: 0 5%;\n box-sizing: inherit;\n &:before,\n &:after {\n content: ' ';\n display: table;\n clear: left;\n }\n @media only screen and (max-width: 1083px) {\n padding-left: 0.75rem;\n }\n .custom_widget_MicrosoftFooter_c-heading-4_105bp_49 {\n color: #616161;\n word-break: break-word;\n font-size: 0.9375rem;\n line-height: 1.25rem;\n padding: 2.25rem 0 0.25rem;\n font-weight: 600;\n }\n .custom_widget_MicrosoftFooter_c-uhff-nav-row_105bp_57 {\n .custom_widget_MicrosoftFooter_c-uhff-nav-group_105bp_58 {\n display: block;\n float: left;\n min-height: 0.0625rem;\n vertical-align: text-top;\n padding: 0 0.75rem;\n width: 100%;\n zoom: 1;\n &:first-child {\n padding-left: 0;\n @media only screen and (max-width: 1083px) {\n padding-left: 0.75rem;\n }\n }\n @media only screen and (min-width: 540px) and (max-width: 1082px) {\n width: 33.33333%;\n }\n @media only screen and (min-width: 1083px) {\n width: 16.6666666667%;\n }\n ul.custom_widget_MicrosoftFooter_c-list_105bp_78.custom_widget_MicrosoftFooter_f-bare_105bp_78 {\n font-size: 0.6875rem;\n line-height: 1rem;\n margin-top: 0;\n margin-bottom: 0;\n padding-left: 0;\n list-style-type: none;\n li {\n word-break: break-word;\n padding: 0.5rem 0;\n margin: 0;\n }\n }\n }\n }\n}\n.custom_widget_MicrosoftFooter_c-uhff-base_105bp_94 {\n background: #f2f2f2;\n margin: 0 auto;\n max-width: calc(100rem + 10%);\n padding: 1.875rem 5% 1rem;\n &:before,\n &:after {\n content: ' ';\n display: table;\n }\n &:after {\n clear: both;\n }\n a.custom_widget_MicrosoftFooter_c-uhff-ccpa_105bp_107 {\n font-size: 0.6875rem;\n line-height: 1rem;\n float: left;\n margin: 0.1875rem 0;\n }\n a.custom_widget_MicrosoftFooter_c-uhff-ccpa_105bp_107:hover {\n text-decoration: underline;\n }\n ul.custom_widget_MicrosoftFooter_c-list_105bp_78 {\n font-size: 0.6875rem;\n line-height: 1rem;\n float: right;\n margin: 0.1875rem 0;\n color: #616161;\n li {\n padding: 0 1.5rem 0.25rem 0;\n display: inline-block;\n }\n }\n .custom_widget_MicrosoftFooter_c-list_105bp_78.custom_widget_MicrosoftFooter_f-bare_105bp_78 {\n padding-left: 0;\n list-style-type: none;\n }\n @media only screen and (max-width: 1083px) {\n display: flex;\n flex-wrap: wrap;\n padding: 1.875rem 1.5rem 1rem;\n }\n}\n.custom_widget_MicrosoftFooter_social-share_105bp_138 {\n position: fixed;\n top: 60%;\n transform: translateY(-50%);\n left: 0;\n z-index: 1000;\n}\n.custom_widget_MicrosoftFooter_sharing-options_105bp_146 {\n list-style: none;\n padding: 0;\n margin: 0;\n display: block;\n flex-direction: column;\n background-color: white;\n width: 2.6875rem;\n border-radius: 0 0.4375rem 0.4375rem 0;\n}\n.custom_widget_MicrosoftFooter_linkedin-icon_105bp_156 {\n border-top-right-radius: 7px;\n}\n.custom_widget_MicrosoftFooter_linkedin-icon_105bp_156:hover {\n border-radius: 0;\n}\n.custom_widget_MicrosoftFooter_social-share-rss-image_105bp_162 {\n border-bottom-right-radius: 7px;\n}\n.custom_widget_MicrosoftFooter_social-share-rss-image_105bp_162:hover {\n border-radius: 0;\n}\n.custom_widget_MicrosoftFooter_social-link-footer_105bp_169 {\n position: relative;\n display: block;\n margin: -0.125rem 0;\n transition: all 0.2s ease;\n}\n.custom_widget_MicrosoftFooter_social-link-footer_105bp_169:hover .custom_widget_MicrosoftFooter_linkedin-icon_105bp_156 {\n border-radius: 0;\n}\n.custom_widget_MicrosoftFooter_social-link-footer_105bp_169:hover .custom_widget_MicrosoftFooter_social-share-rss-image_105bp_162 {\n border-radius: 0;\n}\n.custom_widget_MicrosoftFooter_social-link-footer_105bp_169 img {\n width: 2.5rem;\n height: auto;\n transition: filter 0.3s ease;\n}\n.custom_widget_MicrosoftFooter_social-share-list_105bp_188 {\n width: 2.5rem;\n}\n.custom_widget_MicrosoftFooter_social-share-rss-image_105bp_162 {\n width: 2.5rem;\n}\n.custom_widget_MicrosoftFooter_share-icon_105bp_195 {\n border: 2px solid transparent;\n display: inline-block;\n position: relative;\n}\n.custom_widget_MicrosoftFooter_share-icon_105bp_195:hover {\n opacity: 1;\n border: 2px solid white;\n box-sizing: border-box;\n}\n.custom_widget_MicrosoftFooter_share-icon_105bp_195:hover .custom_widget_MicrosoftFooter_label_105bp_207 {\n opacity: 1;\n visibility: visible;\n border: 2px solid white;\n box-sizing: border-box;\n border-left: none;\n}\n.custom_widget_MicrosoftFooter_label_105bp_207 {\n position: absolute;\n left: 100%;\n white-space: nowrap;\n opacity: 0;\n visibility: hidden;\n transition: all 0.2s ease;\n color: white;\n border-radius: 0 10 0 0.625rem;\n top: 50%;\n transform: translateY(-50%);\n height: 2.5rem;\n border-radius: 0 0.375rem 0.375rem 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 1.25rem 0.3125rem 1.25rem 0.5rem;\n margin-left: -0.0625rem;\n}\n.custom_widget_MicrosoftFooter_linkedin_105bp_156 {\n background-color: #0474b4;\n}\n.custom_widget_MicrosoftFooter_facebook_105bp_237 {\n background-color: #3c5c9c;\n}\n.custom_widget_MicrosoftFooter_twitter_105bp_240 {\n background-color: white;\n color: black;\n}\n.custom_widget_MicrosoftFooter_reddit_105bp_244 {\n background-color: #fc4404;\n}\n.custom_widget_MicrosoftFooter_mail_105bp_247 {\n background-color: #848484;\n}\n.custom_widget_MicrosoftFooter_bluesky_105bp_250 {\n background-color: white;\n color: black;\n}\n.custom_widget_MicrosoftFooter_rss_105bp_254 {\n background-color: #ec7b1c;\n}\n#custom_widget_MicrosoftFooter_RSS_105bp_1 {\n width: 2.5rem;\n height: 2.5rem;\n}\n@media (max-width: 991px) {\n .custom_widget_MicrosoftFooter_social-share_105bp_138 {\n display: none;\n }\n}\n","tokens":{"context-uhf":"custom_widget_MicrosoftFooter_context-uhf_105bp_1","c-uhff-link":"custom_widget_MicrosoftFooter_c-uhff-link_105bp_12","c-uhff":"custom_widget_MicrosoftFooter_c-uhff_105bp_12","c-uhff-nav":"custom_widget_MicrosoftFooter_c-uhff-nav_105bp_35","c-heading-4":"custom_widget_MicrosoftFooter_c-heading-4_105bp_49","c-uhff-nav-row":"custom_widget_MicrosoftFooter_c-uhff-nav-row_105bp_57","c-uhff-nav-group":"custom_widget_MicrosoftFooter_c-uhff-nav-group_105bp_58","c-list":"custom_widget_MicrosoftFooter_c-list_105bp_78","f-bare":"custom_widget_MicrosoftFooter_f-bare_105bp_78","c-uhff-base":"custom_widget_MicrosoftFooter_c-uhff-base_105bp_94","c-uhff-ccpa":"custom_widget_MicrosoftFooter_c-uhff-ccpa_105bp_107","social-share":"custom_widget_MicrosoftFooter_social-share_105bp_138","sharing-options":"custom_widget_MicrosoftFooter_sharing-options_105bp_146","linkedin-icon":"custom_widget_MicrosoftFooter_linkedin-icon_105bp_156","social-share-rss-image":"custom_widget_MicrosoftFooter_social-share-rss-image_105bp_162","social-link-footer":"custom_widget_MicrosoftFooter_social-link-footer_105bp_169","social-share-list":"custom_widget_MicrosoftFooter_social-share-list_105bp_188","share-icon":"custom_widget_MicrosoftFooter_share-icon_105bp_195","label":"custom_widget_MicrosoftFooter_label_105bp_207","linkedin":"custom_widget_MicrosoftFooter_linkedin_105bp_156","facebook":"custom_widget_MicrosoftFooter_facebook_105bp_237","twitter":"custom_widget_MicrosoftFooter_twitter_105bp_240","reddit":"custom_widget_MicrosoftFooter_reddit_105bp_244","mail":"custom_widget_MicrosoftFooter_mail_105bp_247","bluesky":"custom_widget_MicrosoftFooter_bluesky_105bp_250","rss":"custom_widget_MicrosoftFooter_rss_105bp_254","RSS":"custom_widget_MicrosoftFooter_RSS_105bp_1"}},"form":null},"localOverride":false},"CachedAsset:text:en_US-components/community/Breadcrumb-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/community/Breadcrumb-1745505307000","value":{"navLabel":"Breadcrumbs","dropdown":"Additional parent page navigation"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageBanner-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageBanner-1745505307000","value":{"messageMarkedAsSpam":"This post has been marked as spam","messageMarkedAsSpam@board:TKB":"This article has been marked as spam","messageMarkedAsSpam@board:BLOG":"This post has been marked as spam","messageMarkedAsSpam@board:FORUM":"This discussion has been marked as spam","messageMarkedAsSpam@board:OCCASION":"This event has been marked as spam","messageMarkedAsSpam@board:IDEA":"This idea has been marked as spam","manageSpam":"Manage Spam","messageMarkedAsAbuse":"This post has been marked as abuse","messageMarkedAsAbuse@board:TKB":"This article has been marked as abuse","messageMarkedAsAbuse@board:BLOG":"This post has been marked as abuse","messageMarkedAsAbuse@board:FORUM":"This discussion has been marked as abuse","messageMarkedAsAbuse@board:OCCASION":"This event has been marked as abuse","messageMarkedAsAbuse@board:IDEA":"This idea has been marked as abuse","preModCommentAuthorText":"This comment will be published as soon as it is approved","preModCommentModeratorText":"This comment is awaiting moderation","messageMarkedAsOther":"This post has been rejected due to other reasons","messageMarkedAsOther@board:TKB":"This article has been rejected due to other reasons","messageMarkedAsOther@board:BLOG":"This post has been rejected due to other reasons","messageMarkedAsOther@board:FORUM":"This discussion has been rejected due to other reasons","messageMarkedAsOther@board:OCCASION":"This event has been rejected due to other reasons","messageMarkedAsOther@board:IDEA":"This idea has been rejected due to other reasons","messageArchived":"This post was archived on {date}","relatedUrl":"View Related Content","relatedContentText":"Showing related content","archivedContentLink":"View Archived Content"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageView/MessageViewStandard-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageView/MessageViewStandard-1745505307000","value":{"anonymous":"Anonymous","author":"{messageAuthorLogin}","authorBy":"{messageAuthorLogin}","board":"{messageBoardTitle}","replyToUser":" to {parentAuthor}","showMoreReplies":"Show More","replyText":"Reply","repliesText":"Replies","markedAsSolved":"Marked as Solution","movedMessagePlaceholder.BLOG":"{count, plural, =0 {This comment has been} other {These comments have been} }","movedMessagePlaceholder.TKB":"{count, plural, =0 {This comment has been} other {These comments have been} }","movedMessagePlaceholder.FORUM":"{count, plural, =0 {This reply has been} other {These replies have been} }","movedMessagePlaceholder.IDEA":"{count, plural, =0 {This comment has been} other {These comments have been} }","movedMessagePlaceholder.OCCASION":"{count, plural, =0 {This comment has been} other {These comments have been} }","movedMessagePlaceholderUrlText":"moved.","messageStatus":"Status: ","statusChanged":"Status changed: {previousStatus} to {currentStatus}","statusAdded":"Status added: {status}","statusRemoved":"Status removed: {status}","labelExpand":"expand replies","labelCollapse":"collapse replies","unhelpfulReason.reason1":"Content is outdated","unhelpfulReason.reason2":"Article is missing information","unhelpfulReason.reason3":"Content is for a different Product","unhelpfulReason.reason4":"Doesn't match what I was searching for"},"localOverride":false},"CachedAsset:text:en_US-components/messages/ThreadedReplyList-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/ThreadedReplyList-1745505307000","value":{"title":"{count, plural, one{# Reply} other{# Replies}}","title@board:BLOG":"{count, plural, one{# Comment} other{# Comments}}","title@board:TKB":"{count, plural, one{# Comment} other{# Comments}}","title@board:IDEA":"{count, plural, one{# Comment} other{# Comments}}","title@board:OCCASION":"{count, plural, one{# Comment} other{# Comments}}","noRepliesTitle":"No Replies","noRepliesTitle@board:BLOG":"No Comments","noRepliesTitle@board:TKB":"No Comments","noRepliesTitle@board:IDEA":"No Comments","noRepliesTitle@board:OCCASION":"No Comments","noRepliesDescription":"Be the first to reply","noRepliesDescription@board:BLOG":"Be the first to comment","noRepliesDescription@board:TKB":"Be the first to comment","noRepliesDescription@board:IDEA":"Be the first to comment","noRepliesDescription@board:OCCASION":"Be the first to comment","messageReadOnlyAlert:BLOG":"Comments have been turned off for this post","messageReadOnlyAlert:TKB":"Comments have been turned off for this article","messageReadOnlyAlert:IDEA":"Comments have been turned off for this idea","messageReadOnlyAlert:FORUM":"Replies have been turned off for this discussion","messageReadOnlyAlert:OCCASION":"Comments have been turned off for this event"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageReplyCallToAction-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageReplyCallToAction-1745505307000","value":{"leaveReply":"Leave a reply...","leaveReply@board:BLOG@message:root":"Leave a comment...","leaveReply@board:TKB@message:root":"Leave a comment...","leaveReply@board:IDEA@message:root":"Leave a comment...","leaveReply@board:OCCASION@message:root":"Leave a comment...","repliesTurnedOff.FORUM":"Replies are turned off for this topic","repliesTurnedOff.BLOG":"Comments are turned off for this topic","repliesTurnedOff.TKB":"Comments are turned off for this topic","repliesTurnedOff.IDEA":"Comments are turned off for this topic","repliesTurnedOff.OCCASION":"Comments are turned off for this topic","infoText":"Stop poking me!"},"localOverride":false},"Category:category:Exchange":{"__typename":"Category","id":"category:Exchange","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Outlook":{"__typename":"Category","id":"category:Outlook","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Community-Info-Center":{"__typename":"Category","id":"category:Community-Info-Center","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:EducationSector":{"__typename":"Category","id":"category:EducationSector","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:DrivingAdoption":{"__typename":"Category","id":"category:DrivingAdoption","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Azure":{"__typename":"Category","id":"category:Azure","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Windows-Server":{"__typename":"Category","id":"category:Windows-Server","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:MicrosoftTeams":{"__typename":"Category","id":"category:MicrosoftTeams","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:PublicSector":{"__typename":"Category","id":"category:PublicSector","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:microsoft365":{"__typename":"Category","id":"category:microsoft365","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:IoT":{"__typename":"Category","id":"category:IoT","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:HealthcareAndLifeSciences":{"__typename":"Category","id":"category:HealthcareAndLifeSciences","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:ITOpsTalk":{"__typename":"Category","id":"category:ITOpsTalk","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:MicrosoftLearn":{"__typename":"Category","id":"category:MicrosoftLearn","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Blog:board:MicrosoftLearnBlog":{"__typename":"Blog","id":"board:MicrosoftLearnBlog","blogPolicies":{"__typename":"BlogPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}},"boardPolicies":{"__typename":"BoardPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:AI":{"__typename":"Category","id":"category:AI","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:MicrosoftMechanics":{"__typename":"Category","id":"category:MicrosoftMechanics","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:MicrosoftforNonprofits":{"__typename":"Category","id":"category:MicrosoftforNonprofits","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:StartupsatMicrosoft":{"__typename":"Category","id":"category:StartupsatMicrosoft","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:PartnerCommunity":{"__typename":"Category","id":"category:PartnerCommunity","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Microsoft365Copilot":{"__typename":"Category","id":"category:Microsoft365Copilot","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Windows":{"__typename":"Category","id":"category:Windows","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Content_Management":{"__typename":"Category","id":"category:Content_Management","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:microsoft-security":{"__typename":"Category","id":"category:microsoft-security","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:microsoftintune":{"__typename":"Category","id":"category:microsoftintune","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"ModerationData:moderation_data:3843200":{"__typename":"ModerationData","id":"moderation_data:3843200","status":"APPROVED","rejectReason":null,"isReportedAbuse":false,"rejectUser":null,"rejectTime":null,"rejectActorType":null},"BlogReplyMessage:message:3843200":{"__typename":"BlogReplyMessage","author":{"__ref":"User:user:988334"},"id":"message:3843200","revisionNum":1,"uid":3843200,"depth":1,"hasGivenKudo":false,"subscribed":false,"board":{"__ref":"Blog:board:FastTrackforAzureBlog"},"parent":{"__ref":"BlogTopicMessage:message:3834619"},"conversation":{"__ref":"Conversation:conversation:3834619"},"subject":"Re: Deploy and run a Azure OpenAI/ChatGPT application on AKS via Bicep","moderationData":{"__ref":"ModerationData:moderation_data:3843200"},"body":"
","body@stripHtml({\"removeProcessingText\":false,\"removeSpoilerMarkup\":false,\"removeTocMarkup\":false,\"truncateLength\":200})@stringLength":"57","kudosSumWeight":0,"repliesCount":0,"postTime":"2023-06-08T08:39:05.690-07:00","lastPublishTime":"2023-06-08T08:39:05.690-07:00","metrics":{"__typename":"MessageMetrics","views":8531},"visibilityScope":"PUBLIC","placeholder":false,"originalMessageForPlaceholder":null,"entityType":"BLOG_REPLY","eventPath":"category:FastTrack/category:products-services/category:communities/community:gxcuf89792board:FastTrackforAzureBlog/message:3834619/message:3843200","replies":{"__typename":"MessageConnection","pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null},"edges":[]},"customFields":[],"attachments":{"__typename":"AttachmentConnection","edges":[],"pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null}}},"Rank:rank:37":{"__typename":"Rank","id":"rank:37","position":18,"name":"Copper Contributor","color":"333333","icon":null,"rankStyle":"TEXT"},"User:user:1894018":{"__typename":"User","id":"user:1894018","uid":1894018,"login":"waelkdouh","biography":null,"registrationData":{"__typename":"RegistrationData","status":null,"registrationTime":"2023-06-08T08:31:18.197-07:00"},"deleted":false,"email":"","avatar":{"__typename":"UserAvatar","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/m_assets/avatars/default/avatar-8.svg?time=0"},"rank":{"__ref":"Rank:rank:37"},"entityType":"USER","eventPath":"community:gxcuf89792/user:1894018"},"ModerationData:moderation_data:3843192":{"__typename":"ModerationData","id":"moderation_data:3843192","status":"APPROVED","rejectReason":null,"isReportedAbuse":false,"rejectUser":null,"rejectTime":null,"rejectActorType":null},"BlogReplyMessage:message:3843192":{"__typename":"BlogReplyMessage","author":{"__ref":"User:user:1894018"},"id":"message:3843192","revisionNum":1,"uid":3843192,"depth":1,"hasGivenKudo":false,"subscribed":false,"board":{"__ref":"Blog:board:FastTrackforAzureBlog"},"parent":{"__ref":"BlogTopicMessage:message:3834619"},"conversation":{"__ref":"Conversation:conversation:3834619"},"subject":"Re: Deploy and run a Azure OpenAI/ChatGPT application on AKS via Bicep","moderationData":{"__ref":"ModerationData:moderation_data:3843192"},"body":"
I think you have a typo. The magic of copy and paste 🙂
","body@stripHtml({\"removeProcessingText\":false,\"removeSpoilerMarkup\":false,\"removeTocMarkup\":false,\"truncateLength\":200})@stringLength":"213","kudosSumWeight":0,"repliesCount":0,"postTime":"2023-06-08T08:32:54.464-07:00","lastPublishTime":"2023-06-08T08:32:54.464-07:00","metrics":{"__typename":"MessageMetrics","views":8527},"visibilityScope":"PUBLIC","placeholder":false,"originalMessageForPlaceholder":null,"entityType":"BLOG_REPLY","eventPath":"category:FastTrack/category:products-services/category:communities/community:gxcuf89792board:FastTrackforAzureBlog/message:3834619/message:3843192","replies":{"__typename":"MessageConnection","pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null},"edges":[]},"customFields":[],"attachments":{"__typename":"AttachmentConnection","edges":[],"pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null}}},"CachedAsset:text:en_US-components/community/Navbar-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/community/Navbar-1745505307000","value":{"community":"Community Home","inbox":"Inbox","manageContent":"Manage Content","tos":"Terms of Service","forgotPassword":"Forgot Password","themeEditor":"Theme Editor","edit":"Edit Navigation Bar","skipContent":"Skip to content","gxcuf89792":"Tech Community","external-1":"Events","s-m-b":"Nonprofit Community","windows-server":"Windows Server","education-sector":"Education Sector","driving-adoption":"Driving Adoption","Common-content_management-link":"Content Management","microsoft-learn":"Microsoft Learn","s-q-l-server":"Content Management","partner-community":"Microsoft Partner Community","microsoft365":"Microsoft 365","external-9":".NET","external-8":"Teams","external-7":"Github","products-services":"Products","external-6":"Power Platform","communities-1":"Topics","external-5":"Microsoft Security","planner":"Outlook","external-4":"Microsoft 365","external-3":"Dynamics 365","azure":"Azure","healthcare-and-life-sciences":"Healthcare and Life Sciences","external-2":"Azure","microsoft-mechanics":"Microsoft Mechanics","microsoft-learn-1":"Community","external-10":"Learning Room Directory","microsoft-learn-blog":"Blog","windows":"Windows","i-t-ops-talk":"ITOps Talk","external-link-1":"View All","microsoft-securityand-compliance":"Microsoft Security","public-sector":"Public Sector","community-info-center":"Lounge","external-link-2":"View All","microsoft-teams":"Microsoft Teams","external":"Blogs","microsoft-endpoint-manager":"Microsoft Intune","startupsat-microsoft":"Startups at Microsoft","exchange":"Exchange","a-i":"AI and Machine Learning","io-t":"Internet of Things (IoT)","Common-microsoft365-copilot-link":"Microsoft 365 Copilot","outlook":"Microsoft 365 Copilot","external-link":"Community Hubs","communities":"Products"},"localOverride":false},"CachedAsset:text:en_US-components/community/NavbarHamburgerDropdown-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/community/NavbarHamburgerDropdown-1745505307000","value":{"hamburgerLabel":"Side Menu"},"localOverride":false},"CachedAsset:text:en_US-components/community/BrandLogo-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/community/BrandLogo-1745505307000","value":{"logoAlt":"Khoros","themeLogoAlt":"Brand Logo"},"localOverride":false},"CachedAsset:text:en_US-components/community/NavbarTextLinks-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/community/NavbarTextLinks-1745505307000","value":{"more":"More"},"localOverride":false},"CachedAsset:text:en_US-components/authentication/AuthenticationLink-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/authentication/AuthenticationLink-1745505307000","value":{"title.login":"Sign In","title.registration":"Register","title.forgotPassword":"Forgot Password","title.multiAuthLogin":"Sign In"},"localOverride":false},"CachedAsset:text:en_US-components/nodes/NodeLink-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/nodes/NodeLink-1745505307000","value":{"place":"Place {name}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageCoverImage-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageCoverImage-1745505307000","value":{"coverImageTitle":"Cover Image"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/nodes/NodeTitle-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/nodes/NodeTitle-1745505307000","value":{"nodeTitle":"{nodeTitle, select, community {Community} other {{nodeTitle}}} "},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageTimeToRead-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageTimeToRead-1745505307000","value":{"minReadText":"{min} MIN READ"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageSubject-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageSubject-1745505307000","value":{"noSubject":"(no subject)"},"localOverride":false},"CachedAsset:text:en_US-components/users/UserLink-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/users/UserLink-1745505307000","value":{"authorName":"View Profile: {author}","anonymous":"Anonymous"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/users/UserRank-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/users/UserRank-1745505307000","value":{"rankName":"{rankName}","userRank":"Author rank {rankName}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageTime-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageTime-1745505307000","value":{"postTime":"Published: {time}","lastPublishTime":"Last Update: {time}","conversation.lastPostingActivityTime":"Last posting activity time: {time}","conversation.lastPostTime":"Last post time: {time}","moderationData.rejectTime":"Rejected time: {time}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageBody-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageBody-1745505307000","value":{"showMessageBody":"Show More","mentionsErrorTitle":"{mentionsType, select, board {Board} user {User} message {Message} other {}} No Longer Available","mentionsErrorMessage":"The {mentionsType} you are trying to view has been removed from the community.","videoProcessing":"Video is being processed. Please try again in a few minutes.","bannerTitle":"Video provider requires cookies to play the video. Accept to continue or {url} it directly on the provider's site.","buttonTitle":"Accept","urlText":"watch"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageCustomFields-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageCustomFields-1745505307000","value":{"CustomField.default.label":"Value of {name}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageRevision-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageRevision-1745505307000","value":{"lastUpdatedDatePublished":"{publishCount, plural, one{Published} other{Updated}} {date}","lastUpdatedDateDraft":"Created {date}","version":"Version {major}.{minor}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/common/QueryHandler-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/common/QueryHandler-1745505307000","value":{"title":"Query Handler"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageReplyButton-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageReplyButton-1745505307000","value":{"repliesCount":"{count}","title":"Reply","title@board:BLOG@message:root":"Comment","title@board:TKB@message:root":"Comment","title@board:IDEA@message:root":"Comment","title@board:OCCASION@message:root":"Comment"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageAuthorBio-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageAuthorBio-1745505307000","value":{"sendMessage":"Send Message","actionMessage":"Follow this blog board to get notified when there's new activity","coAuthor":"CO-PUBLISHER","contributor":"CONTRIBUTOR","userProfile":"View Profile","iconlink":"Go to {name} {type}"},"localOverride":false},"CachedAsset:text:en_US-components/community/NavbarDropdownToggle-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/community/NavbarDropdownToggle-1745505307000","value":{"ariaLabelClosed":"Press the down arrow to open the menu"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/users/UserAvatar-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/users/UserAvatar-1745505307000","value":{"altText":"{login}'s avatar","altTextGeneric":"User's avatar"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/ranks/UserRankLabel-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/ranks/UserRankLabel-1745505307000","value":{"altTitle":"Icon for {rankName} rank"},"localOverride":false},"CachedAsset:text:en_US-components/tags/TagView/TagViewChip-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/tags/TagView/TagViewChip-1745505307000","value":{"tagLabelName":"Tag name {tagName}"},"localOverride":false},"CachedAsset:text:en_US-components/users/UserRegistrationDate-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/users/UserRegistrationDate-1745505307000","value":{"noPrefix":"{date}","withPrefix":"Joined {date}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/nodes/NodeAvatar-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/nodes/NodeAvatar-1745505307000","value":{"altTitle":"Node avatar for {nodeTitle}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/nodes/NodeDescription-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/nodes/NodeDescription-1745505307000","value":{"description":"{description}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageListMenu-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageListMenu-1745505307000","value":{"postTimeAsc":"Oldest","postTimeDesc":"Newest","kudosSumWeightAsc":"Least Liked","kudosSumWeightDesc":"Most Liked","sortTitle":"Sort By","sortedBy.item":" { itemName, select, postTimeAsc {Oldest} postTimeDesc {Newest} kudosSumWeightAsc {Least Liked} kudosSumWeightDesc {Most Liked} other {}}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/nodes/NodeIcon-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/nodes/NodeIcon-1745505307000","value":{"contentType":"Content Type {style, select, FORUM {Forum} BLOG {Blog} TKB {Knowledge Base} IDEA {Ideas} OCCASION {Events} other {}} icon"},"localOverride":false}}}},"page":"/blogs/BlogMessagePage/BlogMessagePage","query":{"boardId":"fasttrackforazureblog","messageSubject":"deploy-and-run-a-azure-openaichatgpt-application-on-aks-via-bicep","messageId":"3834619"},"buildId":"-gVUpXaWnPcjlrLJZ92B7","runtimeConfig":{"buildInformationVisible":false,"logLevelApp":"info","logLevelMetrics":"info","openTelemetryClientEnabled":false,"openTelemetryConfigName":"o365","openTelemetryServiceVersion":"25.3.0","openTelemetryUniverse":"prod","openTelemetryCollector":"http://localhost:4318","openTelemetryRouteChangeAllowedTime":"5000","apolloDevToolsEnabled":false,"inboxMuteWipFeatureEnabled":false},"isFallback":false,"isExperimentalCompile":false,"dynamicIds":["./components/community/Navbar/NavbarWidget.tsx","./components/community/Breadcrumb/BreadcrumbWidget.tsx","./components/customComponent/CustomComponent/CustomComponent.tsx","./components/blogs/BlogArticleWidget/BlogArticleWidget.tsx","./components/messages/MessageView/MessageViewStandard/MessageViewStandard.tsx","./components/messages/ThreadedReplyList/ThreadedReplyList.tsx","./components/external/components/ExternalComponent.tsx","./components/customComponent/CustomComponentContent/TemplateContent.tsx","../shared/client/components/common/List/UnwrappedList/UnwrappedList.tsx","./components/tags/TagView/TagView.tsx","./components/tags/TagView/TagViewChip/TagViewChip.tsx","../shared/client/components/common/List/UnstyledList/UnstyledList.tsx","./components/messages/MessageView/MessageView.tsx"],"appGip":true,"scriptLoader":[{"id":"analytics","src":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/pagescripts/1730819800000/analytics.js?page.id=BlogMessagePage&entity.id=board%3Afasttrackforazureblog&entity.id=message%3A3834619","strategy":"afterInteractive"}]}