This solution deploys the infrastructure around a small HTTP AI application: a runtime endpoint, logs, secure configuration, and a runtime identity allowed to invoke one managed model service. It keeps the blueprint small so you can swap in your own handler code without first adopting an application framework.
Use it when you need a provider-native starting point for a production-facing AI request path, not a data or training platform. The app receives a JSON request, forwards the prompt to the selected managed model service, returns the model text, and writes platform logs for operations.
Architecture
The stack has four pieces:
- An HTTP endpoint on Azure Functions HTTP trigger.
- A runtime identity with the smallest model-service permission practical for Azure OpenAI.
- Secure configuration through Azure Key Vault references.
- Native request and application logs in Application Insights.
The generated app code is small. Replace the sample prompt handler with your application logic after the stack proves that identity, model access, logs, and endpoint routing work in your account.
Azure OpenAI notes
This variant uses Azure OpenAI as the managed model backend and keeps the application endpoint provider-native. Account-specific model access is configured outside source code.
Prerequisites
You need:
- a Pulumi account and the Pulumi CLI
- an Azure subscription with an existing Azure OpenAI endpoint and deployment, plus permission to create Functions, Storage, Key Vault, Application Insights, and role assignments
- local cloud credentials for the selected provider
Download the blueprint
Use the Download blueprint button at the top of this page to grab the Azure OpenAI zip for the language selected in the chooser. Each zip contains:
index.tsas the Pulumi entrypointcomponents/ai-app.tsas the reusable component- runtime app code under the provider-specific folder
package.jsonandtsconfig.jsonfor the Pulumi project
__main__.pyas the Pulumi entrypointcomponents/ai_app.pyas the reusable component- runtime app code under the provider-specific folder
requirements.txtfor the Pulumi project
Unzip, change into the directory, and continue with the quickstart below.
Quickstart
Install Pulumi project dependencies, configure the stack, and deploy. This solution currently ships TypeScript and Python starters.
# 1. Install Pulumi project dependencies
npm install
# 2. Initialize and configure the stack
pulumi stack init dev
pulumi config set azure-native:location eastus
pulumi config set azureOpenAiEndpoint <azure-openai-endpoint>
pulumi config set azureOpenAiDeployment <deployment-name>
pulumi config set azureOpenAiResourceId /subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.CognitiveServices/accounts/<account-name>
# 3. Deploy
pulumi up
# 1. Install Pulumi project dependencies
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# 2. Initialize and configure the stack
pulumi stack init dev
pulumi config set azure-native:location eastus
pulumi config set azureOpenAiEndpoint <azure-openai-endpoint>
pulumi config set azureOpenAiDeployment <deployment-name>
pulumi config set azureOpenAiResourceId /subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.CognitiveServices/accounts/<account-name>
# 3. Deploy
pulumi up
Azure OpenAI remains externally supplied. The guide stores references to your existing endpoint and deployment name in Key Vault, then grants the Function managed identity Key Vault secret access and Azure OpenAI access scoped to the configured resource ID.
Walk through the stack
The component creates Azure Functions HTTP trigger and exports endpointUrl so you can test the request path immediately after pulumi up. The sample handler accepts a JSON body with a prompt field and returns a JSON response with generated text or an operational error from the provider SDK.
Azure Functions reads the endpoint and deployment from Key Vault references and sends logs to Application Insights.
Keep prompts and generated output out of stack outputs. Runtime values belong in request bodies and platform logs, not Pulumi state.
Sample request handler
api/src/functions/chat.js
The Azure Functions HTTP trigger. It forwards the prompt to Azure OpenAI using managed identity and returns the model text. This same api/ directory ships in the TypeScript and Python starters.
import { app } from "@azure/functions";
import { DefaultAzureCredential, getBearerTokenProvider } from "@azure/identity";
import { AzureOpenAI } from "openai";
const apiVersion = "2024-10-21";
const scope = "https://cognitiveservices.azure.com/.default";
app.http("chat", { methods: ["POST"], authLevel: "anonymous", handler: async (request) => {
const body = await request.json().catch(() => ({}));
const prompt = body.prompt;
const endpoint = process.env.AZURE_OPENAI_ENDPOINT;
const deployment = process.env.AZURE_OPENAI_DEPLOYMENT;
if (!prompt || !endpoint || !deployment) {
return { status: 400, jsonBody: { error: "Missing prompt or Azure OpenAI configuration." } };
}
try {
const credential = new DefaultAzureCredential();
const azureADTokenProvider = getBearerTokenProvider(credential, scope);
const client = new AzureOpenAI({ endpoint, azureADTokenProvider, apiVersion });
const response = await client.chat.completions.create({ messages: [{ role: "user", content: prompt }], model: deployment });
return { jsonBody: { response: response.choices[0]?.message?.content ?? "", deployment } };
} catch (error) {
console.error("Azure OpenAI request failed", error);
return { status: 500, jsonBody: { error: "Azure OpenAI request failed." } };
}
}});
Operate the endpoint
After deployment:
curl -X POST "$(pulumi stack output endpointUrl)" \
-H "content-type: application/json" \
-d '{"prompt":"Write one sentence about infrastructure as code."}'
Open endpointUrl, verify the Function responds, and inspect applicationInsightsName for request logs.
Warning: The generated endpoint is public and unauthenticated by default. Anyone with the URL can call it, and leaked URLs can create model and token spend. Before production use, add auth, request validation, rate limits, and provider quota alarms.
This blueprint stays focused on runtime infrastructure and model invocation permissions.
Blueprint Pulumi program
The entrypoint reads stack config, creates the Azure OpenAI application component, and exports the HTTP endpoint plus observability handles.
import * as pulumi from "@pulumi/pulumi";
import { AiApp } from "./components/ai-app";
const config = new pulumi.Config();
const tags = { "pulumi:template": "ai-app-infrastructure", "pulumi:cloud": "azure-openai", "pulumi:language": "typescript" };
const app = new AiApp("ai-app", { namePrefix: `${pulumi.getProject()}-${pulumi.getStack()}`, modelId: config.get("modelId") || "", azureOpenAiEndpoint: config.require("azureOpenAiEndpoint"), azureOpenAiDeployment: config.require("azureOpenAiDeployment"), azureOpenAiResourceId: config.require("azureOpenAiResourceId"), tags });
export const endpointUrl = app.endpointUrl;
export const logResource = app.logResource;
export const runtimeIdentity = app.runtimeIdentity;
import pulumi
from components.ai_app import AiApp
config = pulumi.Config()
tags = {"pulumi:template": "ai-app-infrastructure", "pulumi:cloud": "azure-openai", "pulumi:language": "python"}
app = AiApp("ai-app", name_prefix=f"{pulumi.get_project()}-{pulumi.get_stack()}", model_id=config.get("modelId") or "", azure_open_ai_endpoint=config.require("azureOpenAiEndpoint"), azure_open_ai_deployment=config.require("azureOpenAiDeployment"), azure_open_ai_resource_id=config.require("azureOpenAiResourceId"), tags=tags)
pulumi.export("endpointUrl", app.endpoint_url)
pulumi.export("logResource", app.log_resource)
pulumi.export("runtimeIdentity", app.runtime_identity)
Reusable AI application component
The component provisions Azure Functions HTTP trigger, Application Insights, secure configuration, and least-privilege runtime access to Azure OpenAI.
components/ai-app.ts
Creates Azure Functions HTTP trigger, Application Insights, secure config, and model invocation IAM for Azure OpenAI.
import * as authorization from "@pulumi/azure-native/authorization";
import * as applicationinsights from "@pulumi/azure-native/applicationinsights";
import * as keyvault from "@pulumi/azure-native/keyvault";
import * as resources from "@pulumi/azure-native/resources";
import * as storage from "@pulumi/azure-native/storage";
import * as web from "@pulumi/azure-native/web";
import * as pulumi from "@pulumi/pulumi";
export interface AiAppArgs { namePrefix: string; modelId: string; azureOpenAiEndpoint: string; azureOpenAiDeployment: string; azureOpenAiResourceId: string; tags: Record<string, string>; }
export class AiApp extends pulumi.ComponentResource {
public readonly endpointUrl: pulumi.Output<string>; public readonly logResource: pulumi.Output<string>; public readonly runtimeIdentity: pulumi.Output<string>;
constructor(name: string, args: AiAppArgs, opts?: pulumi.ComponentResourceOptions) {
super("guides:aiAppInfrastructure:AzureOpenAI", name, {}, opts);
const client = authorization.getClientConfigOutput();
const rg = new resources.ResourceGroup(`${name}-rg`, { resourceGroupName: args.namePrefix, tags: args.tags }, { parent: this });
const account = new storage.StorageAccount(`${name}sa`, { resourceGroupName: rg.name, sku: { name: storage.SkuName.Standard_LRS }, kind: storage.Kind.StorageV2, tags: args.tags }, { parent: this });
const workspace = new applicationinsights.Component(`${name}-appi`, { resourceGroupName: rg.name, resourceName: `${args.namePrefix}-appi`, applicationType: applicationinsights.ApplicationType.Web, kind: "web", tags: args.tags }, { parent: this });
const vault = new keyvault.Vault(`${name}-kv`, { resourceGroupName: rg.name, vaultName: `${args.namePrefix}-kv`.replace(/[^a-zA-Z0-9-]/g, "").slice(0, 24), properties: { tenantId: client.tenantId, sku: { family: "A", name: keyvault.SkuName.Standard }, enableRbacAuthorization: true }, tags: args.tags }, { parent: this });
const endpointSecret = new keyvault.Secret(`${name}-endpoint`, { resourceGroupName: rg.name, vaultName: vault.name, secretName: "azure-openai-endpoint", properties: { value: args.azureOpenAiEndpoint } }, { parent: this });
const deploymentSecret = new keyvault.Secret(`${name}-deployment`, { resourceGroupName: rg.name, vaultName: vault.name, secretName: "azure-openai-deployment", properties: { value: args.azureOpenAiDeployment } }, { parent: this });
const plan = new web.AppServicePlan(`${name}-plan`, { resourceGroupName: rg.name, name: `${args.namePrefix}-plan`, kind: "functionapp", reserved: true, sku: { name: "Y1", tier: "Dynamic" }, tags: args.tags }, { parent: this });
const keys = storage.listStorageAccountKeysOutput({ resourceGroupName: rg.name, accountName: account.name });
const connectionString = pulumi.interpolate`DefaultEndpointsProtocol=https;AccountName=${account.name};AccountKey=${keys.keys[0].value};EndpointSuffix=core.windows.net`;
const app = new web.WebApp(`${name}-func`, { resourceGroupName: rg.name, name: `${args.namePrefix}-func`, kind: "functionapp,linux", serverFarmId: plan.id, identity: { type: web.ManagedServiceIdentityType.SystemAssigned }, siteConfig: { linuxFxVersion: "Node|20", appSettings: [ { name: "AzureWebJobsStorage", value: connectionString }, { name: "FUNCTIONS_EXTENSION_VERSION", value: "~4" }, { name: "FUNCTIONS_WORKER_RUNTIME", value: "node" }, { name: "APPLICATIONINSIGHTS_CONNECTION_STRING", value: workspace.connectionString }, { name: "AZURE_OPENAI_ENDPOINT", value: pulumi.interpolate`@Microsoft.KeyVault(SecretUri=${endpointSecret.properties.secretUri})` }, { name: "AZURE_OPENAI_DEPLOYMENT", value: pulumi.interpolate`@Microsoft.KeyVault(SecretUri=${deploymentSecret.properties.secretUri})` } ] }, httpsOnly: true, tags: args.tags }, { parent: this });
const secretsUserRole = "4633458b-17de-408a-b874-0445c86b69e6";
const openAiUserRole = "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd";
const principalId = app.identity.apply(identity => identity!.principalId!);
new authorization.RoleAssignment(`${name}-kv-secrets`, { principalId, principalType: authorization.PrincipalType.ServicePrincipal, roleDefinitionId: pulumi.interpolate`${vault.id}/providers/Microsoft.Authorization/roleDefinitions/${secretsUserRole}`, scope: vault.id }, { parent: this });
new authorization.RoleAssignment(`${name}-openai-user`, { principalId, principalType: authorization.PrincipalType.ServicePrincipal, roleDefinitionId: pulumi.interpolate`${args.azureOpenAiResourceId}/providers/Microsoft.Authorization/roleDefinitions/${openAiUserRole}`, scope: args.azureOpenAiResourceId }, { parent: this });
this.endpointUrl = pulumi.interpolate`https://${app.defaultHostName}/api/chat`; this.logResource = workspace.name; this.runtimeIdentity = principalId; this.registerOutputs({ endpointUrl: this.endpointUrl, logResource: this.logResource, runtimeIdentity: this.runtimeIdentity });
}
}
components/ai_app.py
Creates Azure Functions HTTP trigger, Application Insights, secure config, and model invocation IAM for Azure OpenAI.
import re
import pulumi
import pulumi_azure_native.authorization as authorization
import pulumi_azure_native.applicationinsights as applicationinsights
import pulumi_azure_native.keyvault as keyvault
import pulumi_azure_native.resources as resources
import pulumi_azure_native.storage as storage
import pulumi_azure_native.web as web
class AiApp(pulumi.ComponentResource):
def __init__(self, name, name_prefix, model_id, azure_open_ai_endpoint, azure_open_ai_deployment, azure_open_ai_resource_id, tags, opts=None):
super().__init__("guides:aiAppInfrastructure:AzureOpenAI", name, None, opts)
client = authorization.get_client_config_output()
rg = resources.ResourceGroup(f"{name}-rg", resource_group_name=name_prefix, tags=tags, opts=pulumi.ResourceOptions(parent=self))
account = storage.StorageAccount(f"{name}sa", resource_group_name=rg.name, sku=storage.SkuArgs(name=storage.SkuName.STANDARD_LRS), kind=storage.Kind("StorageV2"), tags=tags, opts=pulumi.ResourceOptions(parent=self))
appi = applicationinsights.Component(f"{name}-appi", resource_group_name=rg.name, resource_name_=f"{name_prefix}-appi", application_type=applicationinsights.ApplicationType.WEB, kind="web", tags=tags, opts=pulumi.ResourceOptions(parent=self))
vault_name = re.sub(r"[^a-zA-Z0-9-]", "", f"{name_prefix}-kv")[:24]
vault = keyvault.Vault(f"{name}-kv", resource_group_name=rg.name, vault_name=vault_name, properties=keyvault.VaultPropertiesArgs(tenant_id=client.tenant_id, sku=keyvault.SkuArgs(family="A", name=keyvault.SkuName.STANDARD), enable_rbac_authorization=True), tags=tags, opts=pulumi.ResourceOptions(parent=self))
endpoint_secret = keyvault.Secret(f"{name}-endpoint", resource_group_name=rg.name, vault_name=vault.name, secret_name="azure-openai-endpoint", properties=keyvault.SecretPropertiesArgs(value=azure_open_ai_endpoint), opts=pulumi.ResourceOptions(parent=self))
deployment_secret = keyvault.Secret(f"{name}-deployment", resource_group_name=rg.name, vault_name=vault.name, secret_name="azure-openai-deployment", properties=keyvault.SecretPropertiesArgs(value=azure_open_ai_deployment), opts=pulumi.ResourceOptions(parent=self))
plan = web.AppServicePlan(f"{name}-plan", resource_group_name=rg.name, name=f"{name_prefix}-plan", kind="functionapp", reserved=True, sku=web.SkuDescriptionArgs(name="Y1", tier="Dynamic"), tags=tags, opts=pulumi.ResourceOptions(parent=self))
keys = storage.list_storage_account_keys_output(resource_group_name=rg.name, account_name=account.name)
connection_string = pulumi.Output.concat("DefaultEndpointsProtocol=https;AccountName=", account.name, ";AccountKey=", keys.keys[0].value, ";EndpointSuffix=core.windows.net")
function = web.WebApp(f"{name}-func", resource_group_name=rg.name, name=f"{name_prefix}-func", kind="functionapp,linux", server_farm_id=plan.id, identity=web.ManagedServiceIdentityArgs(type=web.ManagedServiceIdentityType.SYSTEM_ASSIGNED), site_config=web.SiteConfigArgs(linux_fx_version="Node|20", app_settings=[web.NameValuePairArgs(name="AzureWebJobsStorage", value=connection_string), web.NameValuePairArgs(name="FUNCTIONS_EXTENSION_VERSION", value="~4"), web.NameValuePairArgs(name="FUNCTIONS_WORKER_RUNTIME", value="node"), web.NameValuePairArgs(name="APPLICATIONINSIGHTS_CONNECTION_STRING", value=appi.connection_string), web.NameValuePairArgs(name="AZURE_OPENAI_ENDPOINT", value=pulumi.Output.concat("@Microsoft.KeyVault(SecretUri=", endpoint_secret.properties.secret_uri, ")")), web.NameValuePairArgs(name="AZURE_OPENAI_DEPLOYMENT", value=pulumi.Output.concat("@Microsoft.KeyVault(SecretUri=", deployment_secret.properties.secret_uri, ")"))]), https_only=True, tags=tags, opts=pulumi.ResourceOptions(parent=self))
secrets_user_role = "4633458b-17de-408a-b874-0445c86b69e6"
open_ai_user_role = "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd"
authorization.RoleAssignment(f"{name}-kv-secrets", principal_id=function.identity.principal_id, principal_type=authorization.PrincipalType.SERVICE_PRINCIPAL, role_definition_id=pulumi.Output.concat(vault.id, "/providers/Microsoft.Authorization/roleDefinitions/", secrets_user_role), scope=vault.id, opts=pulumi.ResourceOptions(parent=self))
authorization.RoleAssignment(f"{name}-openai-user", principal_id=function.identity.principal_id, principal_type=authorization.PrincipalType.SERVICE_PRINCIPAL, role_definition_id=pulumi.Output.concat(azure_open_ai_resource_id, "/providers/Microsoft.Authorization/roleDefinitions/", open_ai_user_role), scope=azure_open_ai_resource_id, opts=pulumi.ResourceOptions(parent=self))
self.endpoint_url = pulumi.Output.concat("https://", function.default_host_name, "/api/chat")
self.log_resource = appi.name
self.runtime_identity = function.identity.principal_id
self.register_outputs({"endpointUrl": self.endpoint_url, "logResource": self.log_resource, "runtimeIdentity": self.runtime_identity})