From 5bbb513b86014c1233b524abb4a0c8d9e16683ae Mon Sep 17 00:00:00 2001 From: jaydestro <2974195+jaydestro@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:30:35 -0400 Subject: [PATCH] docs: add illustrative Azure deployment examples --- .github/workflows/azure-cd-example.yml | 146 +++++++++++++++ README.md | 13 ++ infra/azure-example.bicep | 239 +++++++++++++++++++++++++ 3 files changed, 398 insertions(+) create mode 100644 .github/workflows/azure-cd-example.yml create mode 100644 infra/azure-example.bicep diff --git a/.github/workflows/azure-cd-example.yml b/.github/workflows/azure-cd-example.yml new file mode 100644 index 0000000..1949eb0 --- /dev/null +++ b/.github/workflows/azure-cd-example.yml @@ -0,0 +1,146 @@ +# MOCK-UP ONLY: illustrates a deployment workflow; the deployment job is disabled. +# Phi/Ollama and the Cosmos emulator are local/CI dependencies, NOT deployment +# dependencies. A real deployment would use Azure Cosmos DB and a reader-selected +# Foundry model, with the reader's own endpoints and identity configuration. +# Cloud application changes, resource provisioning and model selection are outside +# this mock-up. Running it only displays the notice; it cannot sign in or deploy. +# Companion reference: infra/azure-example.bicep describes the host, real Cosmos +# database and separate runtime/GitHub identities. It is not invoked here. +# +# Prerequisites (create/configure these yourself, not through this example): +# - An existing Linux App Service with Python 3.12 and +# SCM_DO_BUILD_DURING_DEPLOYMENT=true for installing requirements.txt. +# - A protected GitHub environment named azure-example, restricted to main. +# - Environment secrets AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, +# and environment variable AZURE_WEBAPP_NAME; no client secret/publish profile. +# - OIDC federation subject repo:/:environment:azure-example, +# with Website Contributor scoped to the target app, not the subscription. +# - A successful manual run of emulator-ci.yml for this exact main commit. +# Reference: https://learn.microsoft.com/azure/app-service/deploy-github-actions + +name: Azure CD example (mock-up) + +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: azure-cd-example + cancel-in-progress: false + +jobs: + notice: + name: Example-only notice + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Explain the example and its limits + shell: bash + run: | + echo "::notice::Mock-up only: Azure sign-in and deployment are disabled." + cat >> "$GITHUB_STEP_SUMMARY" <<'NOTICE' + ## Azure deployment example only + This mock-up illustrates CI-gated packaging, OIDC login and ZIP deployment + to an existing Azure App Service. Only this notice runs; the illustrative + deployment job is disabled and cannot sign in or deploy. + + Phi/Ollama and the Cosmos emulator belong to local development and CI only. + They are not part of the deployment target. A real deployment would use + Azure Cosmos DB and a reader-selected Foundry model, with the reader's + own endpoints and identity configuration. No model is selected here. + + Companion reference: infra/azure-example.bicep illustrates the host, real + Cosmos database and scoped identities. This workflow does not provision it. + + A reader adapting this pattern must provide a cloud-ready application and + the prerequisites at the top of this file before changing the disabled guard. + The existing emulator CI remains a pre-deployment check, not a cloud runtime. + Infrastructure provisioning, model evaluation and production readiness + are not demonstrated or validated by this example. + NOTICE + + deploy: + name: Illustrative App Service deployment (disabled) + needs: notice + if: ${{ false && github.ref == 'refs/heads/main' }} + runs-on: ubuntu-24.04 + timeout-minutes: 30 + environment: azure-example + permissions: + contents: read + actions: read + id-token: write + steps: + - name: Check out the selected revision + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Require successful CI for this exact commit + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + count="$(gh run list --repo "$GITHUB_REPOSITORY" --workflow emulator-ci.yml \ + --commit "$GITHUB_SHA" --event workflow_dispatch --status success \ + --limit 1 --json databaseId --jq 'length')" + if [[ "$count" != "1" ]]; then + echo "::error::Run Cosmos emulator CI manually for this exact main commit first." + exit 1 + fi + + - name: Require reader-supplied Azure configuration + shell: bash + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_WEBAPP_NAME: ${{ vars.AZURE_WEBAPP_NAME }} + run: | + for name in AZURE_CLIENT_ID AZURE_TENANT_ID AZURE_SUBSCRIPTION_ID AZURE_WEBAPP_NAME; do + if [[ -z "${!name}" ]]; then + echo "::error::Configure $name in the azure-example environment." + exit 1 + fi + done + + - name: Set up Python 3.12 + uses: actions/setup-python@v7 + with: + python-version: "3.12" + + - name: Install uv 0.13.0 from Microsoft + shell: bash + run: | + python -m pip install --no-deps \ + "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/download/uv/0.13/uv-0.13.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + + - name: Package tracked application files and locked dependencies + shell: bash + run: | + package="$RUNNER_TEMP/azure-cd-example" + mkdir -p "$package" + git archive HEAD app | tar -x -C "$package" + printf '%s\n' \ + '--index-url https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/' \ + > "$package/requirements.txt" + uv export --locked --no-dev --no-emit-project --format requirements-txt \ + >> "$package/requirements.txt" + cd "$package" + zip -qr "$RUNNER_TEMP/azure-cd-example.zip" app requirements.txt + + - name: Sign in with a federated identity + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Deploy the example ZIP to the existing app + uses: azure/webapps-deploy@v3 + with: + app-name: ${{ vars.AZURE_WEBAPP_NAME }} + package: ${{ runner.temp }}/azure-cd-example.zip + startup-command: uvicorn app.api:app --host 0.0.0.0 --port 8000 \ No newline at end of file diff --git a/README.md b/README.md index 8606a33..9d3e29a 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,19 @@ The dispatch feed adds changing operational data: publish a pickup-bay update, then ask the agent where drivers should collect their paperwork. Its answer must cite the exact bay and update reference stored in Cosmos DB. +## Azure deployment samples + +The [CD workflow](.github/workflows/azure-cd-example.yml) and +[Bicep template](infra/azure-example.bicep) are **illustrative samples only**, not +a tested or production-ready Azure deployment. The workflow's deployment job is +disabled; running it only displays a notice and does not sign in or deploy. + +A real deployment would use Azure Cosmos DB and a reader-selected Microsoft +Foundry model, not the emulator or local Phi/Ollama runtime used in CI. The +examples do not select or provision a model. The application remains local-only; +adapt its cloud data/model configuration and supply your own Azure, GitHub and +identity settings before turning these examples into a working deployment. + ## Open a ready-to-use development environment If you use GitHub Codespaces, click **Open in Codespaces** above to open a browser diff --git a/infra/azure-example.bicep b/infra/azure-example.bicep new file mode 100644 index 0000000..f9601df --- /dev/null +++ b/infra/azure-example.bicep @@ -0,0 +1,239 @@ +// EXAMPLE ONLY: companion infrastructure for the disabled Azure CD mock-up. +// This does not deploy code or make the unchanged emulator/Ollama app cloud-ready. +// No Phi model, emulator, Ollama server or Foundry model is provisioned here. +// The reader supplies an existing Foundry endpoint/model and grants this app's +// runtimePrincipalId the appropriate inference role on that existing resource. +// Public HTTPS endpoints are used for simplicity; configure request authentication, +// network restrictions and operational controls before using this beyond an example. +// Creating the role assignments requires additional role-assignment permissions; +// the GitHub deployment identity deliberately cannot provision this infrastructure. +// Reference: https://learn.microsoft.com/azure/app-service/deploy-github-actions + +targetScope = 'resourceGroup' + +@description('Example resource location; choose a region supporting the selected services.') +param location string = resourceGroup().location + +@description('Lowercase alphanumeric resource prefix, optionally containing internal hyphens.') +@minLength(3) +@maxLength(16) +param namePrefix string = 'fleetcharge' + +@description('Reader-owned GitHub repository in owner/repository form, not a personal token.') +@minLength(3) +param githubRepository string + +@description('Create and protect this GitHub environment separately; restrict it to main.') +param githubEnvironment string = 'azure-example' + +type FoundryConfiguration = { + @description('Existing HTTPS OpenAI-compatible Foundry endpoint; no default account.') + @minLength(1) + endpoint: string + @description('Reader-selected existing model deployment; no default model.') + @minLength(1) + modelDeployment: string +} + +@description('Existing Foundry configuration only; this template never creates a model.') +param foundry FoundryConfiguration + +@description('Example Cosmos database; all containers are precreated, not provisioned by runtime code.') +param cosmosDatabaseName string = 'fleetcharge' + +var suffix = uniqueString(resourceGroup().id, namePrefix) +var tags = { + purpose: 'deployment-example-only' +} +var partitions = { + depots: 'depotId' + routes: 'depotId' + vehicles: 'depotId' + chargers: 'depotId' + telemetry: 'vehicleId' + recommendations: 'depotId' + dispatchUpdates: 'depotId' +} + +resource plan 'Microsoft.Web/serverfarms@2025-03-01' = { + name: 'asp-${namePrefix}-${suffix}' + location: location + tags: tags + kind: 'linux' + sku: { + name: 'B1' + tier: 'Basic' + } + properties: { + reserved: true + } +} + +resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2026-03-15' = { + name: 'cosmos-${namePrefix}-${suffix}' + location: location + tags: tags + kind: 'GlobalDocumentDB' + properties: { + databaseAccountOfferType: 'Standard' + disableLocalAuth: true + minimalTlsVersion: 'Tls12' + publicNetworkAccess: 'Enabled' + consistencyPolicy: { + defaultConsistencyLevel: 'Session' + } + capabilities: [ + { name: 'EnableServerless' } + ] + locations: [ + { + locationName: location + failoverPriority: 0 + } + ] + } +} + +resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2026-03-15' = { + parent: cosmos + name: cosmosDatabaseName + properties: { + resource: { + id: cosmosDatabaseName + } + } +} + +resource containers 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2026-03-15' = [for partition in items(partitions): { + parent: database + name: partition.key + properties: { + resource: { + id: partition.key + partitionKey: { + paths: [ '/${partition.value}' ] + kind: 'Hash' + version: 2 + } + } + } +}] + +resource webApp 'Microsoft.Web/sites@2025-03-01' = { + name: 'app-${namePrefix}-${suffix}' + location: location + tags: tags + kind: 'app,linux' + identity: { + type: 'SystemAssigned' + } + properties: { + serverFarmId: plan.id + httpsOnly: true + publicNetworkAccess: 'Enabled' + siteConfig: { + linuxFxVersion: 'PYTHON|3.12' + appCommandLine: 'uvicorn app.api:app --host 0.0.0.0 --port 8000' + alwaysOn: true + minTlsVersion: '1.2' + scmMinTlsVersion: '1.2' + ftpsState: 'Disabled' + appSettings: [ + { name: 'SCM_DO_BUILD_DURING_DEPLOYMENT', value: 'true' } + { name: 'COSMOS_ENDPOINT', value: cosmos.properties.documentEndpoint } + { name: 'COSMOS_DATABASE', value: cosmosDatabaseName } + { name: 'COSMOS_MODE', value: 'azure' } + { name: 'AGENT_MODEL_MODE', value: 'foundry' } + { name: 'FOUNDRY_ENDPOINT', value: foundry.endpoint } + { name: 'FOUNDRY_MODEL_DEPLOYMENT', value: foundry.modelDeployment } + ] + } + } +} + +resource ftpPublishingPolicy 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2025-03-01' = { + parent: webApp + name: 'ftp' + properties: { + allow: false + } +} + +resource scmPublishingPolicy 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2025-03-01' = { + parent: webApp + name: 'scm' + properties: { + allow: false + } +} + +// Cosmos data-plane access is separate from Azure resource-management roles. +resource cosmosDataAccess 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2026-03-15' = { + parent: cosmos + name: guid(cosmos.id, webApp.id, cosmosDatabaseName, 'data-contributor') + properties: { + principalId: webApp.identity.principalId + roleDefinitionId: '${cosmos.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002' + scope: '${cosmos.id}/dbs/${cosmosDatabaseName}' + } + dependsOn: [ database ] +} + +resource githubIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { + name: 'id-${namePrefix}-github-${suffix}' + location: location + tags: tags +} + +resource githubFederation 'Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials@2024-11-30' = { + parent: githubIdentity + name: 'github-environment' + properties: { + issuer: 'https://token.actions.githubusercontent.com' + subject: 'repo:${githubRepository}:environment:${githubEnvironment}' + audiences: [ 'api://AzureADTokenExchange' ] + } +} + +resource websiteContributor 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + scope: subscription() + name: 'de139f84-1756-47ae-9be6-808fbbe84772' +} + +resource githubDeploymentAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: webApp + name: guid(webApp.id, githubIdentity.id, websiteContributor.id) + properties: { + roleDefinitionId: websiteContributor.id + principalId: githubIdentity.properties.principalId + principalType: 'ServicePrincipal' + } +} + +type GitHubConfiguration = { + environment: string + secrets: { + AZURE_CLIENT_ID: string + AZURE_TENANT_ID: string + AZURE_SUBSCRIPTION_ID: string + } + variables: { + AZURE_WEBAPP_NAME: string + } +} + +@description('Copy these identifiers into your protected GitHub environment, not this repository.') +output githubConfiguration GitHubConfiguration = { + environment: githubEnvironment + secrets: { + AZURE_CLIENT_ID: githubIdentity.properties.clientId + AZURE_TENANT_ID: subscription().tenantId + AZURE_SUBSCRIPTION_ID: subscription().subscriptionId + } + variables: { + AZURE_WEBAPP_NAME: webApp.name + } +} + +@description('Grant this runtime identity inference access on your existing Foundry resource separately.') +output runtimePrincipalId string = webApp.identity.principalId \ No newline at end of file