Skip to content

Add app config and related feature flag capabilities #87

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .config/feature-flags.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"schemaVersion": "2.0.0",
"feature_management": {
"feature_flags": [
]
}
}
40 changes: 40 additions & 0 deletions .config/feature-flags.json.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"schemaVersion": "2.0.0",
"feature_management": {
"feature_flags": [
{
"id": "my-agent",
"enabled": true,
"variants": [
{
"name": "agent_v1",
"configuration_value": "<agent-id-1-placeholder>"
},
{
"name": "agent_v2",
"configuration_value": "<agent-id-2-placeholder>"
}
],
"allocation": {
"percentile": [
{
"variant": "agent_v1",
"from": 0,
"to": 50
},
{
"variant": "agent_v2",
"from": 50,
"to": 100
}
],
"default_when_enabled": "agent_v1",
"default_when_disabled": "agent_v1"
},
"telemetry": {
"enabled": true
}
}
]
}
}
25 changes: 25 additions & 0 deletions .github/workflows/azure-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ permissions:
jobs:
build:
runs-on: ubuntu-latest
if: ${{ vars.AZURE_CLIENT_ID != '' }}
env:
AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
Expand Down Expand Up @@ -74,3 +75,27 @@ jobs:

- name: Deploy Application
run: azd deploy --no-prompt

deploy-experiments:
name: Deploy Experiments
runs-on: ubuntu-latest
env:
APP_CONFIGURATION_FILE: .config/feature-flags.json
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Azure login using Federated Credentials
uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
enable-AzPSSession: true

- name: Deploy App Config feature flags
uses: azure/app-configuration-deploy-feature-flags@v1-beta
with:
path: ${{ env.APP_CONFIGURATION_FILE }}
app-configuration-endpoint: ${{ vars.APP_CONFIGURATION_ENDPOINT }}
strict: false
31 changes: 31 additions & 0 deletions .github/workflows/experiment-validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Validate Experiments
on:
workflow_dispatch:
push:

# GitHub Actions workflow to deploy to Azure using azd
# To configure required secrets for connecting to Azure, simply run `azd pipeline config`

# Set up permissions for deploying with secretless Azure federated credentials
# https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication
permissions:
id-token: write
contents: read

env:
APP_CONFIGURATION_FILE: .config/feature-flags.json

jobs:
validate-feature-flags:
name: Validate Feature Flags
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Validate App Config feature flags
uses: azure/app-configuration-deploy-feature-flags@v1-beta
with:
path: ${{ env.APP_CONFIGURATION_FILE }}
operation: validate

1 change: 1 addition & 0 deletions .github/workflows/template-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@ jobs:
AZURE_AI_EMBED_MODEL_FORMAT: ${{ vars.AZURE_AI_EMBED_MODEL_FORMAT }}
AZURE_AI_EMBED_MODEL_VERSION: ${{ vars.AZURE_AI_EMBED_MODEL_VERSION }}
AZURE_EXISTING_AIPROJECT_CONNECTION_STRING: ${{ vars.AZURE_EXISTING_AIPROJECT_CONNECTION_STRING }}
APP_CONFIGURATION_ENDPOINT: ${{ vars.APP_CONFIGURATION_ENDPOINT }}
- name: print result
run: cat ${{ steps.validation.outputs.resultFile }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ ENV/
env.bak/
venv.bak/
.azure
.vscode/
3 changes: 2 additions & 1 deletion azure.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ pipeline:
- AZURE_AI_EMBED_MODEL_VERSION
- AZURE_AI_EMBED_DIMENSIONS
- AZURE_AI_SEARCH_INDEX_NAME
- AZURE_EXISTING_AIPROJECT_CONNECTION_STRING
- AZURE_EXISTING_AIPROJECT_CONNECTION_STRING
- APP_CONFIGURATION_ENDPOINT
34 changes: 30 additions & 4 deletions infra/core/config/configstore.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,35 @@ param keyValueNames array = []
param keyValueValues array = []

@description('The principal ID to grant access to the Azure App Configuration store')
param principalId string
param appPrincipalId string

resource configStore 'Microsoft.AppConfiguration/configurationStores@2023-03-01' = {
@description('The principal ID to grant access to the Azure App Configuration store')
param userPrincipalId string


@description('The Application Insights ID linked to the Azure App Configuration store')
param appInsightsName string

resource configStore 'Microsoft.AppConfiguration/configurationStores@2023-09-01-preview' = {
name: name
location: location
sku: {
name: 'standard'
name: 'Standard'
}
tags: tags
properties: {
encryption: {}
disableLocalAuth: true
enablePurgeProtection: false
experimentation:{}
dataPlaneProxy:{
authenticationMode: 'Pass-through'
privateLinkDelegation: 'Disabled'
}
telemetry: {
resourceId: appInsights.id
}
}
}

resource configStoreKeyValue 'Microsoft.AppConfiguration/configurationStores/keyValues@2023-03-01' = [for (item, i) in keyValueNames: {
Expand All @@ -40,9 +60,15 @@ module configStoreAccess '../security/configstore-access.bicep' = {
name: 'app-configuration-access'
params: {
configStoreName: name
principalId: principalId
appPrincipalId: appPrincipalId
userPrincipalId: userPrincipalId
}
dependsOn: [configStore]
}

resource appInsights 'Microsoft.Insights/components@2020-02-02-preview' existing = {
name: appInsightsName
}

output endpoint string = configStore.properties.endpoint
output name string = configStore.name
2 changes: 1 addition & 1 deletion infra/core/host/container-app.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = {
serviceBinds: !empty(serviceBinds) ? serviceBinds : null
containers: [
{
image: 'azdtemplate.azurecr.io/get-start-with-ai-agents:latest'
image: 'azdtemplate.azurecr.io/get-start-with-ai-agents:latest' // Got an error on this image during azd up
name: containerName
env: env
resources: {
Expand Down
22 changes: 17 additions & 5 deletions infra/core/security/configstore-access.bicep
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
@description('Name of Azure App Configuration store')
param configStoreName string

@description('The principal ID of the service principal to assign the role to')
param principalId string
@description('The principal ID of the application that needs read access to the Azure App Configuration store')
param appPrincipalId string

@description('The principal ID of the service principal that needs to manage the Azure App Configuration store')
param userPrincipalId string

resource configStore 'Microsoft.AppConfiguration/configurationStores@2023-03-01' existing = {
name: configStoreName
}

var configStoreDataReaderRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '516239f1-63e1-4d78-a4de-a74fb236a071')
var configStoreDataOwnerRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5ae67dd6-50cb-40e7-96ff-dc2bfa4b606b')

resource configStoreDataReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(subscription().id, resourceGroup().id, principalId, configStoreDataReaderRole)
name: guid(subscription().id, resourceGroup().id, appPrincipalId, configStoreDataReaderRole)
scope: configStore
properties: {
roleDefinitionId: configStoreDataReaderRole
principalId: principalId
principalType: 'ServicePrincipal'
principalId: appPrincipalId
}
}

resource configStoreDataOwnerRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(resourceGroup().id, userPrincipalId, configStoreDataOwnerRole)
scope: configStore
properties: {
roleDefinitionId: configStoreDataOwnerRole
principalId: userPrincipalId
}
}
20 changes: 19 additions & 1 deletion infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,11 @@ param useApplicationInsights bool = true
@description('Do we want to use the Azure AI Search')
param useSearchService bool = false

@description('Id of the user or app to assign application roles')
param principalId string = ''

@description('Random seed to be used during generation of new resources suffixes.')
param seed string = newGuid()
param seed string = newGuid() // why do we need this?
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we want this? Sometimes when there is an issue, I do azd up several times and it surprised me that new set of resources got created.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, this is strange. I created an issue in this repo about it

#89


var abbrs = loadJsonContent('./abbreviations.json')
var resourceToken = toLower(uniqueString(subscription().id, environmentName, location, seed))
Expand Down Expand Up @@ -388,6 +391,20 @@ module backendRoleAzureAIDeveloperRG 'core/security/role.bicep' = {
}
}

// App Configuration
module configStore 'core/config/configstore.bicep' = {
name: 'config-store'
scope: rg
params: {
location: location
name: '${abbrs.appConfigurationStores}${resourceToken}'
tags: tags
appPrincipalId: api.outputs.SERVICE_API_IDENTITY_PRINCIPAL_ID
userPrincipalId: principalId
appInsightsName: ai.outputs.applicationInsightsName
}
}

output AZURE_RESOURCE_GROUP string = rg.name

// Outputs required for local development server
Expand All @@ -401,6 +418,7 @@ output AZURE_AI_SEARCH_ENDPOINT string = searchServiceEndpoint
output AZURE_AI_EMBED_DIMENSIONS string = embeddingDeploymentDimensions
output AZURE_AI_AGENT_NAME string = agentName
output AZURE_AI_AGENT_ID string = agentID
output APP_CONFIGURATION_ENDPOINT string = configStore.outputs.endpoint

// Outputs required by azd for ACA
output AZURE_CONTAINER_ENVIRONMENT_NAME string = containerApps.outputs.environmentName
Expand Down
2 changes: 2 additions & 0 deletions scripts/write_env.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ $azureAIEmbedDimensions = azd env get-value AZURE_AI_EMBED_DIMENSIONS
$azureAISearchIndexName = azd env get-value AZURE_AI_SEARCH_INDEX_NAME
$azureAISearchEndpoint = azd env get-value AZURE_AI_SEARCH_ENDPOINT
$serviceAPIUri = azd env get-value SERVICE_API_URI
$appConfigurationEndpoint = azd env get-value APP_CONFIGURATION_ENDPOINT

Add-Content -Path $envFilePath -Value "AZURE_AIPROJECT_CONNECTION_STRING=$azureAiProjectConnectionString"
Add-Content -Path $envFilePath -Value "AZURE_AI_AGENT_DEPLOYMENT_NAME=$azureAiagentDeploymentName"
Expand All @@ -28,6 +29,7 @@ Add-Content -Path $envFilePath -Value "AZURE_AI_SEARCH_INDEX_NAME=$azureAISearch
Add-Content -Path $envFilePath -Value "AZURE_AI_SEARCH_ENDPOINT=$azureAISearchEndpoint"
Add-Content -Path $envFilePath -Value "AZURE_AI_AGENT_NAME=$azureAiAgentName"
Add-Content -Path $envFilePath -Value "AZURE_TENANT_ID=$azureTenantId"
Add-Content -Path $envFilePath -Value "APP_CONFIGURATION_ENDPOINT=$appConfigurationEndpoint"

Write-Host "Web app URL:"
Write-Host $serviceAPIUri -ForegroundColor Cyan
1 change: 1 addition & 0 deletions scripts/write_env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ echo "AZURE_AI_SEARCH_INDEX_NAME=$(azd env get-value AZURE_AI_SEARCH_INDEX_NAME)
echo "AZURE_AI_SEARCH_ENDPOINT=$(azd env get-value AZURE_AI_SEARCH_ENDPOINT)" >> $ENV_FILE_PATH
echo "AZURE_AI_AGENT_NAME=$(azd env get-value AZURE_AI_AGENT_NAME)" >> $ENV_FILE_PATH
echo "AZURE_TENANT_ID=$(azd env get-value AZURE_TENANT_ID)" >> $ENV_FILE_PATH
echo "APP_CONFIGURATION_ENDPOINT=$(azd env get-value APP_CONFIGURATION_ENDPOINT)" >> $ENV_FILE_PATH

echo "Web app URL:"
echo -e "\033[0;36m $(azd env get-value SERVICE_API_URI)"
Expand Down
3 changes: 3 additions & 0 deletions src/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ AZURE_AI_SEARCH_INDEX_NAME="" # required for index search. Example: "index_samp
# Highly recommended. Example: "asst_AbCdEfGhIjKlMnOpQrStUvWxYz". If not specified, the agent name will be used to find the agent ID. Agent ID can be found by following https://learn.microsoft.com/en-us/azure/ai-services/agents/quickstart?pivots=ai-foundry-portal
AZURE_AI_AGENT_ID=""

# Recommended. Enable tracing for debugging and testing feature flag capabilities.
ENABLE_AZURE_MONITOR_TRACING=false
AZURE_TRACING_GEN_AI_CONTENT_RECORDING_ENABLED=false
26 changes: 24 additions & 2 deletions src/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,30 @@ async def lifespan(app: fastapi.FastAPI):
else:
from azure.monitor.opentelemetry import configure_azure_monitor
configure_azure_monitor(connection_string=application_insights_connection_string)
# Do not instrument the code yet, before trace fix is available.
#ai_client.telemetry.enable()
ai_client.telemetry.enable()
logger.info("Configured Application Insights for tracing.")

app_config_conn_str = os.getenv("APP_CONFIGURATION_ENDPOINT")
if app_config_conn_str:
try:
from azure.appconfiguration.provider import load
from featuremanagement import FeatureManager
from featuremanagement.azuremonitor import publish_telemetry
app_config = load(
endpoint=app_config_conn_str,
credential=DefaultAzureCredential(),
feature_flag_enabled=True,
feature_flag_refresh_enabled=True
)
feature_manager = FeatureManager(app_config, on_feature_evaluated=publish_telemetry)
app.state.app_config = app_config
app.state.feature_manager = feature_manager
logger.info("Configured App Configuration with feature flag support.")
except ModuleNotFoundError:
logger.warning("Required libraries for App Configuration not installed.")
logger.warning("Please make sure azure-appconfiguration-provider and FeatureManagement are installed.")
except Exception as e:
logger.warning("Failed to setup App Configuration", exc_info=True)

if os.environ.get("AZURE_AI_AGENT_ID"):
try:
Expand Down
Loading