Skip to navigation

Migrate DocumentAI Service from Environment Variables to Azure App Configuration

The DocumentAI service currently uses the following .env configuration:  

APPLICATION_PORT=4200  
DB_SQL_MANAGED_IDENTITY=true  
DB_SQL_HOST=dev-eus2-adc-sql-002.database.windows.net  
DB_SQL_DATABASE=DocumentAI  
AZURE_AD_TENANT=4aaa468e-93ba-4ee3-ab9f-6a247aa3ade0  
AZURE_AD_CLIENT_ID=f338c4a6-dbb1-4e22-9919-1f4071928cce  
AZURE_OPENAI_API_VERSION=2024-08-01-preview  
AZURE_OPENAI_DEPLOYMENT_ID=gpt-4o-ncus  
AZURE_OPENAI_ENDPOINT=https://da-dev-dc01-openai-oai1.openai.azure.com/  
AZURE_OPENAI_API_KEY=3497ead01da84aad92e8e27ebbbdc9e9  
AZURE_FR_ENDPOINT=https://ap-ai-di01-w2szuopsr4mgc.cognitiveservices.azure.com/  
AZURE_FR_KEY=EoeEdhCJvmP82OMmaNZPJUQA7TBEK9mB0yy5XEkCvVvmoxvBqJVBJQQJ99BJACHYHv6XJ3w3AAALACOG5LST  
AZURE_STORAGE_CONNECTION_STRING=https://deveus2adcinst001.blob.core.windows.net/  
AZURE_STORAGE_CONNECTION_STRING_DOCAI=https://deveus2adcdocst001.blob.core.windows.net/  
FLUXX_UPLOAD_ENDPOINT_URL=http://fluxx-file-intake.fluxx.svc.cluster.local/v1/upload/33333333-3333-4333-8333-333333333333  

Issues with Current Approach

  1. Configuration values hardcoded in deployment files or environment-specific .env files

  2. Secrets exposed in plain text in environment variables

  3. No audit trail for configuration changes

  4. Configuration changes require redeployment

  5. Potential for configuration drift across environments

Azure App Configuration Keys

All configuration will be stored in Azure App Configuration with the DOCAI_ prefix to prevent collisions:

DOCAI_APPLICATION_PORT
DOCAI_DB_SQL_MANAGED_IDENTITY
DOCAI_DB_SQL_HOST
DOCAI_DB_SQL_DATABASE
DOCAI_AZURE_AD_TENANT
DOCAI_AZURE_AD_CLIENT_ID
DOCAI_AZURE_OPENAI_API_VERSION
DOCAI_AZURE_OPENAI_DEPLOYMENT_ID
DOCAI_AZURE_OPENAI_ENDPOINT
DOCAI_AZURE_OPENAI_API_KEY (Key Vault Reference)
DOCAI_AZURE_FR_ENDPOINT
DOCAI_AZURE_FR_KEY (Key Vault Reference)
DOCAI_AZURE_STORAGE_CONNECTION_STRING
DOCAI_AZURE_STORAGE_CONNECTION_STRING_DOCAI
DOCAI_FLUXX_UPLOAD_ENDPOINT_URL

Design

Architecture Pattern

Follow the pattern established in the fluxx-file-intake repository:

1. App Configuration Initialization

Use the framework-nodejs-appconfig and framework-nodejs-core packages to initialize App Config with environment variable fallback support.

2. Configuration Constants

Define all configuration keys in a central constants file (constants/configuration.ts).

3. Lookup Abstraction

Use an app.lookup() abstraction layer that:

  • Resolves values from Azure App Configuration

  • Falls back to environment variables during local development

  • Handles type conversions (string → boolean, string → number)

  • Supports package.json property resolution

4. Dependency Injection

Configuration values are looked up asynchronously and injected where needed.

Component Architecture

┌─────────────────────────────────────────────────────────────┐
│                    DocumentAI Service                        │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  ┌──────────────────────────────────────────────────────┐   │
│  │              app.lookup(ENV_KEYS.*)                  │   │
│  │              Unified Configuration API                │   │
│  └────────────────────┬─────────────────────────────────┘   │
│                       │                                      │
│       ┌───────────────┴────────────────┐                    │
│       │                                │                    │
│  ┌────▼─────────────┐    ┌────────────▼──────────────┐     │
│  │  Azure App       │    │  Environment Variables    │     │
│  │  Configuration   │    │  (Dev/Local Fallback)     │     │
│  └────┬─────────────┘    └───────────────────────────┘     │
│       │                                                      │
│  ┌────▼─────────────┐                                       │
│  │  Azure Key Vault │                                       │
│  │  (Secrets Only)  │                                       │
│  └──────────────────┘                                       │
└─────────────────────────────────────────────────────────────┘

Implementation Steps

Phase 1: Setup Dependencies

Task 1.1: Install Required NPM Packages

npm install framework-nodejs-core@2.0.1
npm install framework-nodejs-appconfig@2.0.1
npm install framework-nodejs-extensions@1.2.2
npm install framework-nodejs-logging@2.0.1
npm install @azure/identity@4.13.0
npm install dotenv@16.6.1

Task 1.2: Update package.json

Add the following to package.json:

{
  "type": "module",
  "dependencies": {
    "@azure/identity": "4.13.0",
    "dotenv": "16.6.1",
    "framework-nodejs-appconfig": "2.0.1",
    "framework-nodejs-core": "2.0.1",
    "framework-nodejs-extensions": "1.2.2",
    "framework-nodejs-logging": "2.0.1"
  }
}

Phase 2: Create Core Infrastructure

Task 2.1: Create Logger Type Definition

File: src/internal/Logger.d.ts

export interface Logger {
    trace: (msg: string, ...args: unknown[]) => void;
    debug: (msg: string, ...args: unknown[]) => void;
    info: (msg: string, ...args: unknown[]) => void;
    warn: (msg: string, ...args: unknown[]) => void;
    error: (msg: string, ...args: unknown[]) => void;
    fatal: (msg: string, ...args: unknown[]) => void;
}

Task 2.2: Create App Container

File: src/internal/app.ts

import { Logger } from './Logger.d.js';
import { missingLogger } from './frameworkAdapter/missingLogger.js';
import { missingLookup } from './frameworkAdapter/missingLookup.js';
import { prepareLookup } from './frameworkAdapter/prepareLookup.js';
import { registerLogger } from './frameworkAdapter/registerLogger.js';
// A holder for all the important parts of the app that are attached to the
// frameworkApp in a DI container or as a property such as the config and logger
export const app: App = {
    log: missingLogger,
    lookup: missingLookup,
    prepareLookup,
    registerLogger,
};
interface App {
    log: Logger;
    lookup: <T>(key: string) => Promise<T>
    prepareLookup: typeof prepareLookup;
    registerLogger: typeof registerLogger;
}

Task 2.3: Create Configuration Constants

File: src/constants/configuration.ts

export const ENV_KEYS = {
    // Application Configuration
    APPLICATION_PORT: 'DOCAI_APPLICATION_PORT',
   
    // Database Configuration
    DB_SQL_MANAGED_IDENTITY: 'DOCAI_DB_SQL_MANAGED_IDENTITY',
    DB_SQL_HOST: 'DOCAI_DB_SQL_HOST',
    DB_SQL_DATABASE: 'DOCAI_DB_SQL_DATABASE',
   
    // Azure AD Configuration
    AZURE_AD_TENANT: 'DOCAI_AZURE_AD_TENANT',
    AZURE_AD_CLIENT_ID: 'DOCAI_AZURE_AD_CLIENT_ID',
   
    // Azure OpenAI Configuration
    AZURE_OPENAI_API_VERSION: 'DOCAI_AZURE_OPENAI_API_VERSION',
    AZURE_OPENAI_DEPLOYMENT_ID: 'DOCAI_AZURE_OPENAI_DEPLOYMENT_ID',
    AZURE_OPENAI_ENDPOINT: 'DOCAI_AZURE_OPENAI_ENDPOINT',
    AZURE_OPENAI_API_KEY: 'DOCAI_AZURE_OPENAI_API_KEY',
   
    // Azure Form Recognizer Configuration
    AZURE_FR_ENDPOINT: 'DOCAI_AZURE_FR_ENDPOINT',
    AZURE_FR_KEY: 'DOCAI_AZURE_FR_KEY',
   
    // Azure Storage Configuration
    AZURE_STORAGE_CONNECTION_STRING: 'DOCAI_AZURE_STORAGE_CONNECTION_STRING',
    AZURE_STORAGE_CONNECTION_STRING_DOCAI: 'DOCAI_AZURE_STORAGE_CONNECTION_STRING_DOCAI',
   
    // External Service Configuration
    FLUXX_UPLOAD_ENDPOINT_URL: 'DOCAI_FLUXX_UPLOAD_ENDPOINT_URL',
} as const;

Phase 3: Create Framework Adapters

Task 3.1: Missing Logger Placeholder

File: src/internal/frameworkAdapter/missingLogger.ts

import { Logger } from '../Logger.d.js';
export const missingLogger: Logger = {
    trace: createMissingLoggerMethod('trace'),
    debug: createMissingLoggerMethod('debug'),
    info: createMissingLoggerMethod('info'),
    warn: createMissingLoggerMethod('warn'),
    error: createMissingLoggerMethod('error'),
    fatal: createMissingLoggerMethod('fatal'),
};
function createMissingLoggerMethod(level: string) {
    return (msg: string, ...args: unknown[]) => {
        throw new Error(`Logger not initialized. Attempted to log ${level}: ${msg}`);
    };
}

Task 3.2: Missing Lookup Placeholder

File: src/internal/frameworkAdapter/missingLookup.ts

export const missingLookup = async <T>(key: string): Promise<T> => {
    throw new Error(`App config lookup not initialized. Attempted to lookup key: ${key}`);
};

Task 3.3: Register Logger Function

File: src/internal/frameworkAdapter/registerLogger.ts

import { DevFramework } from 'framework-nodejs-core';
import { app } from '../app.js';
export const registerLogger = (frameworkApp: DevFramework): void => {
    app.log = frameworkApp.server.log;
};

Task 3.4: Provide App Config

File: src/internal/frameworkAdapter/provideAppConfig.ts

import { ConfigClient } from 'framework-nodejs-extensions';
export const provideAppConfig = (frameworkApp: FrameworkAppWithAppConfig): ConfigClient => {
    if (!frameworkApp.server) {
        throw Error('Attempted to resolve the AppConfig in an app with no "server" assigned');
    }
    if (!frameworkApp.server.diContainer) {
        throw Error('Attempted to resolve the AppConfig in an app & server with no "diContainer" assigned');
    }
    return frameworkApp.server.diContainer.resolve<ConfigClient>('appConfig');
};
interface FrameworkAppWithAppConfig {
    server?: { diContainer?: { resolve: <T>(key: string) => T } }
}

Task 3.5: Create Lookup Function with Type Conversion

File: src/internal/frameworkAdapter/createLookup.ts

import { ConfigClient } from 'framework-nodejs-extensions';
const KEYS_FOR_BOOLEAN_VALUES = [
    'DOCAI_DB_SQL_MANAGED_IDENTITY',
];
const KEYS_FOR_NUMBER_VALUES = [
    'DOCAI_APPLICATION_PORT',
];
export const createLookup = (appConfig: ConfigClient) =>
    async <T>(key: string): Promise<T> => {
        const value = await appConfig.get<unknown>(key);
        // Handle boolean conversion
        if (KEYS_FOR_BOOLEAN_VALUES.includes(key)) {
            if (typeof value === 'string' && value.toLowerCase() === 'false') {
                return false as T;
            }
            return Boolean(value) as T;
        }
        // Handle number conversion
        if (KEYS_FOR_NUMBER_VALUES.includes(key)) {
            if (typeof value === 'string') {
                return Number(value) as T;
            }
        }
        return value as T;
    };

Task 3.6: Prepare Lookup Function

File: src/internal/frameworkAdapter/prepareLookup.ts

import { provideAppConfig } from './provideAppConfig.js';
import { app } from '../app.js';
import { createLookup } from './createLookup.js';
import { DevFramework } from 'framework-nodejs-core';
export const prepareLookup = async (frameworkApp: DevFramework): Promise<void> => {
    const appConfig = provideAppConfig(frameworkApp);
    app.lookup = createLookup(appConfig);
    return undefined;
};

Phase 4: Update Application Entry Point

Task 4.1: Modify index.ts

File: src/index.ts

import 'dotenv/config';
import 'framework-nodejs-appconfig';
import { DevFramework } from 'framework-nodejs-core';
import { app } from './internal/app.js';
import { ENV_KEYS } from './constants/configuration.js';
// Initialize the framework
const frameworkApp = await DevFramework.create();
// Initialize the appConfig with environment variable fallback
await frameworkApp.initAppConfig({
    useEnvironmentVariables: true, // Enables fallback to .env during local dev
});
// Prepare the lookup function
await app.prepareLookup(frameworkApp);
// Register the logger
app.registerLogger(frameworkApp);
// Example: Get application port from App Config
const applicationPort: number = await app.lookup(ENV_KEYS.APPLICATION_PORT);
app.log.info(`Starting DocumentAI service on port ${applicationPort}`);
// Initialize your application here
// ...

Phase 5: Migrate Service Code

Task 5.1: Update Configuration Usage Pattern

Before (using direct environment variables):

const openAiEndpoint = process.env.AZURE_OPENAI_ENDPOINT;
const openAiKey = process.env.AZURE_OPENAI_API_KEY;
const port = parseInt(process.env.APPLICATION_PORT || '4200');

After (using app.lookup):

import { app } from './internal/app.js';
import { ENV_KEYS } from './constants/configuration.js';
const openAiEndpoint: string = await app.lookup(ENV_KEYS.AZURE_OPENAI_ENDPOINT);
const openAiKey: string = await app.lookup(ENV_KEYS.AZURE_OPENAI_API_KEY);
const port: number = await app.lookup(ENV_KEYS.APPLICATION_PORT);

Task 5.2: Update Database Configuration

import { app } from './internal/app.js';
import { ENV_KEYS } from './constants/configuration.js';
export const getDatabaseConfig = async () => {
    return {
        host: await app.lookup(ENV_KEYS.DB_SQL_HOST),
        database: await app.lookup(ENV_KEYS.DB_SQL_DATABASE),
        useManagedIdentity: await app.lookup(ENV_KEYS.DB_SQL_MANAGED_IDENTITY),
    };
};

Task 5.3: Update OpenAI Service

import { app } from '../internal/app.js';
import { ENV_KEYS } from '../constants/configuration.js';
export class OpenAIService {
    private endpoint: string;
    private apiKey: string;
    private apiVersion: string;
    private deploymentId: string;
    async initialize() {
        this.endpoint = await app.lookup(ENV_KEYS.AZURE_OPENAI_ENDPOINT);
        this.apiKey = await app.lookup(ENV_KEYS.AZURE_OPENAI_API_KEY);
        this.apiVersion = await app.lookup(ENV_KEYS.AZURE_OPENAI_API_VERSION);
        this.deploymentId = await app.lookup(ENV_KEYS.AZURE_OPENAI_DEPLOYMENT_ID);
    }
    // Service methods...
}

Task 5.4: Update Form Recognizer Service

import { app } from '../internal/app.js';
import { ENV_KEYS } from '../constants/configuration.js';
export class FormRecognizerService {
    private endpoint: string;
    private apiKey: string;
    async initialize() {
        this.endpoint = await app.lookup(ENV_KEYS.AZURE_FR_ENDPOINT);
        this.apiKey = await app.lookup(ENV_KEYS.AZURE_FR_KEY);
    }
    // Service methods...
}

Phase 6: Azure Infrastructure Setup

Task 6.1: Create App Configuration Keys in Azure Portal

For each environment (dev, qa, uat, prd), create the following keys in Azure App Configuration:

# Development Environment
DOCAI_APPLICATION_PORT=4200
DOCAI_DB_SQL_MANAGED_IDENTITY=true
DOCAI_DB_SQL_HOST=dev-eus2-adc-sql-002.database.windows.net
DOCAI_DB_SQL_DATABASE=DocumentAI
DOCAI_AZURE_AD_TENANT=4aaa468e-93ba-4ee3-ab9f-6a247aa3ade0
DOCAI_AZURE_AD_CLIENT_ID=f338c4a6-dbb1-4e22-9919-1f4071928cce
DOCAI_AZURE_OPENAI_API_VERSION=2024-08-01-preview
DOCAI_AZURE_OPENAI_DEPLOYMENT_ID=gpt-4o-ncus
DOCAI_AZURE_OPENAI_ENDPOINT=https://da-dev-dc01-openai-oai1.openai.azure.com/
DOCAI_AZURE_FR_ENDPOINT=https://ap-ai-di01-w2szuopsr4mgc.cognitiveservices.azure.com/
DOCAI_AZURE_STORAGE_CONNECTION_STRING=https://deveus2adcinst001.blob.core.windows.net/
DOCAI_AZURE_STORAGE_CONNECTION_STRING_DOCAI=https://deveus2adcdocst001.blob.core.windows.net/
DOCAI_FLUXX_UPLOAD_ENDPOINT_URL=http://fluxx-file-intake.fluxx.svc.cluster.local/v1/upload/33333333-3333-4333-8333-333333333333

Task 6.2: Migrate Secrets to Key Vault References

Store sensitive values in Azure Key Vault and reference them in App Config:

Create Key Vault secrets:

az keyvault secret set --vault-name <vault-name> --name DOCAI-AZURE-OPENAI-API-KEY --value "3497ead01da84aad92e8e27ebbbdc9e9"
az keyvault secret set --vault-name <vault-name> --name DOCAI-AZURE-FR-KEY --value "EoeEdhCJvmP82OMmaNZPJUQA7TBEK9mB0yy5XEkCvVvmoxvBqJVBJQQJ99BJACHYHv6XJ3w3AAALACOG5LST"

Update App Config with Key Vault references:

{
  "key": "DOCAI_AZURE_OPENAI_API_KEY",
  "value": "{\"uri\":\"https://<vault-name>.vault.azure.net/secrets/DOCAI-AZURE-OPENAI-API-KEY\"}"
}
{
  "key": "DOCAI_AZURE_FR_KEY",
  "value": "{\"uri\":\"https://<vault-name>.vault.azure.net/secrets/DOCAI-AZURE-FR-KEY\"}"
}

Task 6.3: Configure Managed Identity Access

Grant the DocumentAI service’s Managed Identity access to:

  1. App Configuration: App Configuration Data Reader role

  2. Key Vault: Key Vault Secrets User role

# Get the service principal ID
PRINCIPAL_ID=$(az webapp identity show --name <app-name> --resource-group <rg-name> --query principalId -o tsv)
# Grant App Config access
az role assignment create --assignee $PRINCIPAL_ID --role "App Configuration Data Reader" --scope /subscriptions/<subscription-id>/resourceGroups/<rg-name>/providers/Microsoft.AppConfiguration/configurationStores/<appconfig-name>
# Grant Key Vault access
az keyvault set-policy --name <vault-name> --object-id $PRINCIPAL_ID --secret-permissions get list

Phase 7: Update Deployment Configuration

Task 7.1: Update Kubernetes Deployment YAML

File: k8s/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: documentai-service
spec:
  template:
    spec:
      containers:
      - name: documentai
        image: <your-registry>/documentai:latest
        env:
        - name: AZURE_APP_CONFIGURATION_CONNECTION_STRING
          valueFrom:
            secretKeyRef:
              name: appconfig-secret
              key: connection-string
        # Keep fallback environment variables for local development
        - name: DOCAI_APPLICATION_PORT
          value: "4200"

Task 7.2: Create Local Development .env File

File: .env.example

# Azure App Configuration (leave empty to use environment variables)
AZURE_APP_CONFIGURATION_CONNECTION_STRING=
# Local Development Fallback Values (prefixed with DOCAI_)
DOCAI_APPLICATION_PORT=4200
DOCAI_DB_SQL_MANAGED_IDENTITY=false
DOCAI_DB_SQL_HOST=localhost
DOCAI_DB_SQL_DATABASE=DocumentAI
DOCAI_AZURE_AD_TENANT=4aaa468e-93ba-4ee3-ab9f-6a247aa3ade0
DOCAI_AZURE_AD_CLIENT_ID=f338c4a6-dbb1-4e22-9919-1f4071928cce
DOCAI_AZURE_OPENAI_API_VERSION=2024-08-01-preview
DOCAI_AZURE_OPENAI_DEPLOYMENT_ID=gpt-4o-ncus
DOCAI_AZURE_OPENAI_ENDPOINT=https://da-dev-dc01-openai-oai1.openai.azure.com/
DOCAI_AZURE_OPENAI_API_KEY=local-dev-key
DOCAI_AZURE_FR_ENDPOINT=https://ap-ai-di01-w2szuopsr4mgc.cognitiveservices.azure.com/
DOCAI_AZURE_FR_KEY=local-dev-key
DOCAI_AZURE_STORAGE_CONNECTION_STRING=https://deveus2adcinst001.blob.core.windows.net/
DOCAI_AZURE_STORAGE_CONNECTION_STRING_DOCAI=https://deveus2adcdocst001.blob.core.windows.net/
DOCAI_FLUXX_UPLOAD_ENDPOINT_URL=http://localhost:3000/v1/upload/33333333-3333-4333-8333-333333333333

Testing Strategy

Unit Tests

Test 1: Configuration Lookup

File: test/unit/internal/frameworkAdapter/createLookup.test.ts

import { describe, it, expect, vi } from 'vitest';
import { createLookup } from '../../../../src/internal/frameworkAdapter/createLookup.js';
import { ConfigClient } from 'framework-nodejs-extensions';
describe('createLookup', () => {
    it('should return string values as-is', async () => {
        const mockAppConfig = {
            get: vi.fn().mockResolvedValue('test-value'),
        } as unknown as ConfigClient;
        const lookup = createLookup(mockAppConfig);
        const result = await lookup<string>('DOCAI_AZURE_OPENAI_ENDPOINT');
        expect(result).toBe('test-value');
        expect(mockAppConfig.get).toHaveBeenCalledWith('DOCAI_AZURE_OPENAI_ENDPOINT');
    });
    it('should convert string "true" to boolean true', async () => {
        const mockAppConfig = {
            get: vi.fn().mockResolvedValue('true'),
        } as unknown as ConfigClient;
        const lookup = createLookup(mockAppConfig);
        const result = await lookup<boolean>('DOCAI_DB_SQL_MANAGED_IDENTITY');
        expect(result).toBe(true);
    });
    it('should convert string "false" to boolean false', async () => {
        const mockAppConfig = {
            get: vi.fn().mockResolvedValue('false'),
        } as unknown as ConfigClient;
        const lookup = createLookup(mockAppConfig);
        const result = await lookup<boolean>('DOCAI_DB_SQL_MANAGED_IDENTITY');
        expect(result).toBe(false);
    });
    it('should convert string numbers to numbers', async () => {
        const mockAppConfig = {
            get: vi.fn().mockResolvedValue('4200'),
        } as unknown as ConfigClient;
        const lookup = createLookup(mockAppConfig);
        const result = await lookup<number>('DOCAI_APPLICATION_PORT');
        expect(result).toBe(4200);
        expect(typeof result).toBe('number');
    });
});

Test 2: Missing Configuration Handling

File: test/unit/internal/frameworkAdapter/missingLookup.test.ts

import { describe, it, expect } from 'vitest';
import { missingLookup } from '../../../../src/internal/frameworkAdapter/missingLookup.js';
describe('missingLookup', () => {
    it('should throw an error when lookup is not initialized', async () => {
        await expect(missingLookup('TEST_KEY')).rejects.toThrow(
            'App config lookup not initialized. Attempted to lookup key: TEST_KEY'
        );
    });
});

Test 3: Environment Variable Fallback

File: test/unit/config-fallback.test.ts

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { DevFramework } from 'framework-nodejs-core';
import { app } from '../../src/internal/app.js';
import { ENV_KEYS } from '../../src/constants/configuration.js';
describe('Configuration Fallback', () => {
    let frameworkApp: DevFramework;
    beforeEach(async () => {
        // Set environment variable
        process.env.DOCAI_APPLICATION_PORT = '5000';
       
        frameworkApp = await DevFramework.create();
        await frameworkApp.initAppConfig({ useEnvironmentVariables: true });
        await app.prepareLookup(frameworkApp);
    });
    afterEach(() => {
        delete process.env.DOCAI_APPLICATION_PORT;
    });
    it('should fall back to environment variable when App Config is not available', async () => {
        const port = await app.lookup<number>(ENV_KEYS.APPLICATION_PORT);
        expect(port).toBe(5000);
    });
});

Integration Tests

Test 4: App Configuration Connection

File: test/integration/appconfig-connection.test.ts

import { describe, it, expect, beforeAll } from 'vitest';
import { DevFramework } from 'framework-nodejs-core';
import { app } from '../../src/internal/app.js';
import { ENV_KEYS } from '../../src/constants/configuration.js';
describe('Azure App Configuration Integration', () => {
    beforeAll(async () => {
        const frameworkApp = await DevFramework.create();
        await frameworkApp.initAppConfig({ useEnvironmentVariables: true });
        await app.prepareLookup(frameworkApp);
    });
    it('should successfully retrieve configuration from App Config', async () => {
        const port = await app.lookup<number>(ENV_KEYS.APPLICATION_PORT);
        expect(typeof port).toBe('number');
        expect(port).toBeGreaterThan(0);
    });
    it('should retrieve all required configuration keys', async () => {
        const configs = await Promise.all([
            app.lookup<number>(ENV_KEYS.APPLICATION_PORT),
            app.lookup<boolean>(ENV_KEYS.DB_SQL_MANAGED_IDENTITY),
            app.lookup<string>(ENV_KEYS.DB_SQL_HOST),
            app.lookup<string>(ENV_KEYS.AZURE_OPENAI_ENDPOINT),
        ]);
        expect(configs[0]).toBeDefined();
        expect(configs[1]).toBeDefined();
        expect(configs[2]).toBeDefined();
        expect(configs[3]).toBeDefined();
    });
});

Test 5: Key Vault Secret Resolution

File: test/integration/keyvault-secrets.test.ts

import { describe, it, expect, beforeAll } from 'vitest';
import { DevFramework } from 'framework-nodejs-core';
import { app } from '../../src/internal/app.js';
import { ENV_KEYS } from '../../src/constants/configuration.js';
describe('Key Vault Secret Resolution', () => {
    beforeAll(async () => {
        const frameworkApp = await DevFramework.create();
        await frameworkApp.initAppConfig({ useEnvironmentVariables: true });
        await app.prepareLookup(frameworkApp);
    });
    it('should resolve Key Vault reference for OpenAI API key', async () => {
        const apiKey = await app.lookup<string>(ENV_KEYS.AZURE_OPENAI_API_KEY);
       
        expect(apiKey).toBeDefined();
        expect(typeof apiKey).toBe('string');
        expect(apiKey.length).toBeGreaterThan(0);
        // Should not be the Key Vault reference JSON
        expect(apiKey).not.toContain('vault.azure.net');
    });
    it('should resolve Key Vault reference for Form Recognizer key', async () => {
        const frKey = await app.lookup<string>(ENV_KEYS.AZURE_FR_KEY);
       
        expect(frKey).toBeDefined();
        expect(typeof frKey).toBe('string');
        expect(frKey.length).toBeGreaterThan(0);
    });
});

Contract Tests

Test 6: Configuration Schema Validation

File: test/contract/config-schema.test.ts

import { describe, it, expect, beforeAll } from 'vitest';
import { app } from '../../src/internal/app.js';
import { ENV_KEYS } from '../../src/constants/configuration.js';
describe('Configuration Schema Validation', () => {
    beforeAll(async () => {
        // Initialize app config
    });
    it('should validate APPLICATION_PORT is a valid port number', async () => {
        const port = await app.lookup<number>(ENV_KEYS.APPLICATION_PORT);
        expect(port).toBeGreaterThan(0);
        expect(port).toBeLessThan(65536);
    });
    it('should validate DB_SQL_MANAGED_IDENTITY is boolean', async () => {
        const useManagedIdentity = await app.lookup<boolean>(ENV_KEYS.DB_SQL_MANAGED_IDENTITY);
        expect(typeof useManagedIdentity).toBe('boolean');
    });
    it('should validate endpoints are valid URLs', async () => {
        const openAiEndpoint = await app.lookup<string>(ENV_KEYS.AZURE_OPENAI_ENDPOINT);
        const frEndpoint = await app.lookup<string>(ENV_KEYS.AZURE_FR_ENDPOINT);
       
        expect(() => new URL(openAiEndpoint)).not.toThrow();
        expect(() => new URL(frEndpoint)).not.toThrow();
    });
});

Acceptance Criteria

Functional Requirements

  • All configuration values are retrieved from Azure App Configuration in deployed environments

  • Local development works with .env file fallback

  • Secrets (API keys) are stored in Key Vault and referenced through App Config

  • All configuration keys use the DOCAI_ prefix

  • Boolean values (DB_SQL_MANAGED_IDENTITY) are correctly typed as booleans

  • Numeric values (APPLICATION_PORT) are correctly typed as numbers

  • String values are retrieved without type conversion

Technical Requirements

  • app.lookup() abstraction is used throughout the codebase

  • No direct process.env access for configuration values

  • Configuration constants defined in src/constants/configuration.ts

  • Framework initialization happens before any configuration lookup

  • Proper error handling for missing configuration

  • TypeScript type safety maintained for all configuration values

Testing Requirements

  • Unit tests cover configuration lookup logic

  • Unit tests cover type conversion (string → boolean, string → number)

  • Integration tests validate Azure App Config connectivity

  • Integration tests validate Key Vault secret resolution

  • Contract tests validate configuration schema

  • All tests pass in CI/CD pipeline

  • Test coverage > 80% for configuration modules

Security Requirements

  • Sensitive values removed from .env files in version control

  • .env contains only non-sensitive local development values

  • Secrets stored in Azure Key Vault

  • Managed Identity used for authentication (no connection strings in code)

  • App Configuration connection string stored as Kubernetes secret

  • No secrets logged or exposed in error messages

Documentation Requirements

  • README updated with App Config setup instructions

  • .env.example file created with all required keys

  • Migration guide documented for team members

  • Architecture diagram updated

  • Runbook created for App Config management

Deployment Requirements

  • App Configuration provisioned in all environments (dev, qa, uat, prd)

  • All configuration keys created with appropriate labels/environments

  • Managed Identity configured with correct RBAC roles

  • Helm charts updated with App Config connection string

  • Deployment pipeline validates configuration availability

  • Rollback plan documented


Non-Functional Requirements

Performance

  • Configuration lookup should add < 100ms to application startup time

  • Cached configuration values should not require repeated App Config calls

  • Application should start successfully within 30 seconds

Reliability

  • Application should fail fast with clear error messages if configuration is missing

  • Fallback to environment variables should work seamlessly in local development

  • Service should handle App Config temporary unavailability gracefully

Maintainability

  • Configuration keys should follow consistent naming convention (DOCAI_*)

  • Type conversions should be centralized in createLookup.ts

  • Adding new configuration should require updates in only 2 files (ENV_KEYS and App Config)


Risks and Mitigation

Risk 1: App Configuration Unavailability

Impact: High - Service cannot start

Mitigation:

  • Implement caching at application level

  • Use environment variable fallback in deployment YAML as backup

  • Monitor App Config availability with alerts

Risk 2: Key Vault Access Issues

Impact: High - Cannot retrieve secrets

Mitigation:

  • Validate Managed Identity permissions during deployment

  • Implement retry logic with exponential backoff

  • Monitor Key Vault access patterns

Risk 3: Configuration Drift Between Environments

Impact: Medium - Inconsistent behavior across environments

Mitigation:

  • Use Infrastructure as Code (Terraform/Bicep) to manage App Config

  • Implement configuration validation tests in CI/CD

  • Document configuration schema

Risk 4: Breaking Changes During Migration

Impact: High - Service downtime

Mitigation:

  • Implement feature flag to toggle between old and new config methods

  • Deploy to non-production environments first

  • Maintain backward compatibility during transition period


Rollout Plan

Phase 1: Development Environment (Week 1)

  1. Implement configuration infrastructure code

  2. Deploy to development environment

  3. Run integration tests

  4. Validate logging and monitoring

Phase 2: QA Environment (Week 2)

  1. Deploy to QA environment

  2. Run full test suite

  3. Performance testing

  4. Security validation

Phase 3: UAT Environment (Week 3)

  1. Deploy to UAT environment

  2. Stakeholder validation

  3. Load testing

  4. Documentation review

Phase 4: Production Environment (Week 4)

  1. Deploy during maintenance window

  2. Monitor application health

  3. Validate all services

  4. Conduct post-deployment review


Success Metrics

Technical Metrics

  • Zero configuration-related incidents post-migration

  • Application startup time < 30 seconds

  • Configuration lookup latency < 50ms (p95)

  • 100% of secrets stored in Key Vault

  • Zero secrets in version control

Operational Metrics

  • Configuration change time reduced from 30 minutes to 5 minutes

  • Zero environment-specific configuration files in repository

  • 100% configuration audit trail in App Config

  • Mean time to update configuration < 2 minutes

Team Metrics

  • All team members trained on App Config management

  • Zero deployment failures due to missing configuration

  • Configuration documentation maintained and up-to-date



Story Points: 13

Complexity: High

Effort: 2-3 weeks

Dependencies: Azure App Configuration, Azure Key Vault, Managed Identity setup


Definition of Done

  • All acceptance criteria met

  • Code reviewed and approved

  • All tests passing (unit, integration, contract)

  • Deployed to all environments

  • Documentation updated

  • Team training completed

  • Monitoring and alerts configured

  • Post-deployment validation successful

  • Stakeholder sign-off received