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 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7af05c8
make deployable
aprilk-ms Mar 31, 2025
b399a51
minimal updates
aprilk-ms Apr 10, 2025
0f456b4
Merge branch 'main' into aprilk/add-exp
aprilk-ms Apr 10, 2025
bb08f51
remove vscode settings
aprilk-ms Apr 10, 2025
371b4c3
add to .gitignore
aprilk-ms Apr 10, 2025
c2b3b63
Work in progress
aprilk-ms Apr 10, 2025
19abd4c
Configure Azure Developer Pipeline
aprilk-ms Apr 10, 2025
ba41c7c
add empty feature flag file
aprilk-ms Apr 10, 2025
c96b5f3
Update workflow name
aprilk-ms Apr 10, 2025
3fe9f12
start a feature flag
aprilk-ms Apr 10, 2025
e4e1de1
minor updates
aprilk-ms Apr 13, 2025
f8bb98c
Add variant to message title
aprilk-ms Apr 14, 2025
0616a36
Added new thread button
aprilk-ms Apr 15, 2025
7fe4ce1
update icon and use thread id for randomization
aprilk-ms Apr 15, 2025
cd2e117
Some cleanup
aprilk-ms Apr 15, 2025
bc4981b
clean up for PR
aprilk-ms Apr 15, 2025
7466a59
Minor fixes
aprilk-ms Apr 15, 2025
dc5f9ec
minor fixes
aprilk-ms Apr 15, 2025
7f3c217
Configure Azure Developer Pipeline
aprilk-ms Apr 18, 2025
8e10baa
update deploy workflow
aprilk-ms Apr 18, 2025
968db30
Update title
aprilk-ms Apr 18, 2025
b34f52c
back to short name
aprilk-ms Apr 18, 2025
4d8c058
fixed bicep
aprilk-ms Apr 18, 2025
c880263
update feature flags
aprilk-ms Apr 18, 2025
d78639d
minor fixes to app config initialize
aprilk-ms Apr 18, 2025
f32ea57
fix bicep and address some feedbacks
aprilk-ms Apr 18, 2025
27d3a59
addressed feedback and minor updates
aprilk-ms Apr 18, 2025
94d9ffe
update
aprilk-ms Apr 18, 2025
5c847fd
update to async
aprilk-ms Apr 18, 2025
dc4e590
Merge branch 'main' into aprilk/add-exp
aprilk-ms Apr 29, 2025
027181d
update with new UX
aprilk-ms Apr 29, 2025
a989dbc
Merge with new UX updates
aprilk-ms Apr 29, 2025
5d26938
Revert registry changes
aprilk-ms Apr 29, 2025
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
}
}
]
}
}
27 changes: 27 additions & 0 deletions .github/workflows/azure-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ permissions:

jobs:
build:
name: Deploy
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 +76,28 @@ jobs:

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

update-experiments:
name: Update Experiments
needs: build
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
30 changes: 30 additions & 0 deletions .github/workflows/experiment-validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Validate Experiments
on:
workflow_dispatch:

# 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
5 changes: 5 additions & 0 deletions infra/api.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ param searchServiceEndpoint string
param agentName string
param agentID string
param projectName string
param appConfigurationEndpoint string

resource apiIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: identityName
Expand Down Expand Up @@ -65,6 +66,10 @@ var env = [
name: 'AZURE_AI_SEARCH_ENDPOINT'
value: searchServiceEndpoint
}
{
name: 'APP_CONFIGURATION_ENDPOINT'
value: appConfigurationEndpoint
}
]


Expand Down
34 changes: 23 additions & 11 deletions infra/core/config/configstore.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ param name string
@description('The Azure region/location for the Azure App Configuration store')
param location string = resourceGroup().location

@description('The SKU for the Azure App Configuration store')
param sku string

@description('Custom tags to apply to the Azure App Configuration store')
param tags object = {}

Expand All @@ -15,16 +18,29 @@ param keyValueNames array = []
@description('Specifies the values of the key-value resources.')
param keyValueValues array = []

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

resource configStore 'Microsoft.AppConfiguration/configurationStores@2023-03-01' = {
resource configStore 'Microsoft.AppConfiguration/configurationStores@2023-09-01-preview' = {
name: name
location: location
sku: {
name: 'standard'
name: sku
}
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 @@ -36,13 +52,9 @@ resource configStoreKeyValue 'Microsoft.AppConfiguration/configurationStores/key
}
}]

module configStoreAccess '../security/configstore-access.bicep' = {
name: 'app-configuration-access'
params: {
configStoreName: name
principalId: principalId
}
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
21 changes: 0 additions & 21 deletions infra/core/security/configstore-access.bicep

This file was deleted.

47 changes: 46 additions & 1 deletion infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,25 @@ param embedDeploymentSku string = 'Standard'
// https://learn.microsoft.com/azure/ai-services/openai/quotas-limits
param embedDeploymentCapacity int = 30

@description('Whether to use Azure Application Insights')
param useApplicationInsights bool = true

@description('Whether to use Azure App Configuration')
param useAppConfiguration bool = true

@description('Sku for the App Configuration')
// Recommend to upgrade for production use. See pricing plan:
// https://azure.microsoft.com/en-us/pricing/details/app-configuration/
param appConfigurationSku string = 'free'

@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
Collaborator

Choose a reason for hiding this comment

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

If you remove newGUID, it might break AI Search resource creation.


var abbrs = loadJsonContent('./abbreviations.json')
var resourceToken = toLower(uniqueString(subscription().id, environmentName, location, seed))
Expand Down Expand Up @@ -264,6 +277,19 @@ module containerApps 'core/host/container-apps.bicep' = {
}
}

// App Configuration
module configStore 'core/config/configstore.bicep' = if (useApplicationInsights && useAppConfiguration) {
name: 'config-store'
scope: rg
params: {
location: location
sku: appConfigurationSku
name: '${abbrs.appConfigurationStores}${resourceToken}'
tags: tags
appInsightsName: ai.outputs.applicationInsightsName
}
}

// API app
module api 'api.bicep' = {
name: 'api'
Expand All @@ -284,6 +310,25 @@ module api 'api.bicep' = {
agentName: agentName
agentID: agentID
projectName: projectName
appConfigurationEndpoint: configStore.outputs.endpoint
}
}

module userRoleConfigStoreDataOwner 'core/security/role.bicep' = {
name: 'user-role-config-store-data-owner'
scope: rg
params: {
principalId: principalId
roleDefinitionId: '5ae67dd6-50cb-40e7-96ff-dc2bfa4b606b'
}
}

module backendRoleConfigStoreDataOwner 'core/security/role.bicep' = {
name: 'backend-role-config-store-data-reader'
scope: rg
params: {
principalId: api.outputs.SERVICE_API_IDENTITY_PRINCIPAL_ID
roleDefinitionId: '516239f1-63e1-4d78-a4de-a74fb236a071'
}
}

Expand Down
6 changes: 6 additions & 0 deletions infra/main.parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@
"useSearchService": {
"value": "${USE_AZURE_AI_SEARCH_SERVICE=false}"
},
"useAppConfiguration": {
"value": "${USE_APP_CONFIGURATION=true}"
},
"appConfigurationSku": {
"value": "${APP_CONFIGURATION_SKU=free}"
},
"agentName": {
"value": "${AZURE_AI_AGENT_NAME=agent-template-assistant}"
},
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_EXISTING_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
27 changes: 25 additions & 2 deletions src/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,31 @@ 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_endpoint = os.getenv("APP_CONFIGURATION_ENDPOINT")
if app_config_endpoint:
try:
from azure.appconfiguration.provider.aio import load
from featuremanagement.aio import FeatureManager
from featuremanagement.azuremonitor import publish_telemetry
app_config = await load(
endpoint=app_config_endpoint,
credential=DefaultAzureCredential(),
feature_flag_enabled=True,
feature_flag_refresh_enabled=True,
refresh_interval=300 # no refresh within 5 mins to avoid throttling
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: update to 2 mins

)
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 agent_id:
try:
Expand Down
Loading