Skip to content

christopherhouse/Azure-Functions-Document-Publishing-Example

Repository files navigation

Azure-Functions-Document-Publishing-Example

End-to-end mock of a document-publishing pipeline. Trace context propagates W3C-style from the publisher through Service Bus into the function and across both outbound HTTP calls, so the App Insights end-to-end map lights up under a single operation ID.

sequenceDiagram
    autonumber
    participant CLI as Publisher CLI
    participant SB as Service Bus topic
    participant Fn as Function (Flex Consumption)
    participant SP as SharePoint mock (ACA)
    participant SC as Sitecore mock (ACA)

    CLI->>SB: SendMessage<br/>(traceparent in ApplicationProperties)
    SB->>Fn: ServiceBusTrigger<br/>(parent activity continued)
    Fn->>SP: GET /sites/{s}/drives/{d}/items/{i}
    SP-->>Fn: DriveItem (200) or 404
    Fn->>SP: GET .../items/{i}/content
    SP-->>Fn: bytes (200) or 404
    Fn->>SC: POST /sitecore/api/items<br/>(base64 payload + metadata)
    SC-->>Fn: 200
    Fn-->>Fn: TrackEvent("DocumentPublished",<br/>siteId/driveId/itemId/<br/>sizeBytes/durationMs)
Loading

The function is triggered by Microsoft.Graph.ListUpdated-shaped CloudEvents landing on a Service Bus topic (in the real world: published by Microsoft Graph through an Event Grid partner topic). It calls SharePoint to fetch the referenced drive item metadata + content, then POSTs the file as a base64 blob to "Sitecore".

Identity-based connections everywhere in Azure (no SAS, no account keys). The local Service Bus emulator is the only path that uses SAS, because that's the only auth mode the emulator supports.

Stack

Concern Tool
Function runtime .NET 10, isolated worker, Flex Consumption
Mocks ASP.NET minimal API, Azure Container Apps
Container registry Azure Container Registry (Basic)
Messaging Azure Service Bus Standard (topic)
Observability Workspace-based App Insights + LAWS
IaC OpenTofu, AzureRM provider 4.x
CI/CD GitHub Actions, OIDC, no secrets
Local message bus Microsoft Service Bus emulator (Docker)

Repository layout

src/
  Shared/           DTOs + HttpClient-based SharePoint / Sitecore clients
  Function/         SB-triggered Flex Consumption function (+ dead-letter handler)
  SharePointMock/   ASP.NET minimal API, seeded DocumentStore + /_seed/catalog
  SitecoreMock/     ASP.NET minimal API receiving uploads (offline fallback)
  Publisher/        CLI that publishes mock events to the SB topic
infra/
  bootstrap/        One-time SP + federated creds + RG + state RBAC
  modules/          naming, observability, service-bus, container-*, function-app, diagnostic-settings
  environments/dev/ Calls modules into the workload RG
local/
  service-bus-emulator/   docker-compose for local SB emulator
.github/workflows/
  infra.yml         tofu plan/apply/destroy
  app.yml           build + deploy function
  mocks.yml         build + push images, update ACA revisions
  lint.yml          tofu fmt + dotnet format on every PR

How identity flows

No keys, no connection strings (except the local emulator). Every Azure-side hop authenticates as a managed identity:

  • GitHub Actions → Azure — federated OIDC. The bootstrap stack creates an Entra app with three federated credentials (main push, pull_request, environment:dev). Repo secrets are just AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID — no client secret exists.
  • Function → Service Bus / Storage — the function app's system-assigned managed identity is Azure Service Bus Data Receiver on the subscription and Storage Blob Data Owner on its own AzureWebJobsStorage account.
  • Function → SharePoint/Sitecore mock — plain HTTP today; container apps are reachable on the internal CAE network. Easy to put auth in front later.
  • Container Apps → ACR — each ACA has a user-assigned identity with AcrPull on the registry; ACR has anonymousPullEnabled = false.
  • Publisher CLI → Service BusDefaultAzureCredential. Locally that's your az login; in CI it's the workflow's federated identity.

One-time bootstrap

Run locally with your own Azure credentials. Creates the GitHub Actions Entra app, federated credentials, workload resource group, and the RBAC needed for GHA to take over.

az login --tenant 76de2d2d-77f8-438d-9a87-01806f2345da
az account set --subscription 8bd05b2f-62c5-4def-9869-f0617ebb3970

cd infra/bootstrap
cp terraform.tfvars.example terraform.tfvars
tofu init
tofu apply

Take the three outputs (gha_client_id, gha_tenant_id, gha_subscription_id) and add them as GitHub Actions repository secrets (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID). Also create a GitHub environment named dev — the federated credential is scoped to it.

Deploy from GitHub Actions

  1. infra — runs on push to main (or workflow_dispatch). Plans on PR. Provisions all Azure resources into rg-azfunc-pub-dev. Container apps come up on a public placeholder image.
  2. mocks — builds the two ASP.NET images, pushes to ACR (managed-identity auth), updates each container app revision in place.
  3. function-app — zips and deploys the function to the Flex Consumption plan.
  4. linttofu fmt -check + dotnet format --verify-no-changes on every PR; blocks formatting drift.

The container app image is ignore_changes'd in tofu so the mocks workflow can roll new revisions without infra drift.

Telemetry

The whole pipeline emits one trace per published document. Spans:

  • Publisher — root PublishSharePointEvent activity (ActivityKind.Producer), tagged with messaging.system=servicebus, the destination topic, plus sharepoint.{siteId,driveId,itemId,fileName} and event.id.
  • The W3C traceparent is written onto the Service Bus message as ApplicationProperties["Diagnostic-Id"] / traceparent / tracestate.
  • Function — the Functions Worker AI extension picks up Diagnostic-Id from the message and continues the trace. The function tags its invocation activity with the SharePoint dimensions too.
  • SharePoint mock + Sitecore mockAzure.Monitor.OpenTelemetry.AspNetCore auto-instruments the incoming HTTP requests, so each appears as its own span under the function's call.

After a successful publish the function emits a custom event:

Field Source
EventId CloudEvent id
EventType CloudEvent type (Microsoft.Graph.ListUpdated)
SiteId/DriveId/ItemId Event payload
FileName/MimeType DriveItem returned by SharePoint mock
sizeBytes Downloaded content length
durationMs Stopwatch around the publish op

Query it in App Insights:

customEvents
| where name == "DocumentPublished"
| extend siteId = tostring(customDimensions.SiteId),
         fileName = tostring(customDimensions.FileName),
         sizeBytes = todouble(customMeasurements.sizeBytes),
         durationMs = todouble(customMeasurements.durationMs)
| project timestamp, siteId, fileName, sizeBytes, durationMs, operation_Id

Failure handling: dead-letter loop

The function topic subscription has max_delivery_count = 10 and dead_lettering_on_message_expiration = true. A second function, DeadLetterHandlerFunction, is bound to the subscription's $DeadLetterQueue path. It:

  • Logs the dead-lettered message with reason + description + delivery count.
  • Emits a DocumentPublishingDeadLettered custom event with the same SharePoint dimensions if the body is parseable.
  • Survives non-JSON bodies (the enrichment is best-effort).
customEvents
| where name == "DocumentPublishingDeadLettered"
| extend reason = tostring(customDimensions.DeadLetterReason),
         siteId = tostring(customDimensions.SiteId),
         itemId = tostring(customDimensions.ItemId)
| order by timestamp desc

Local development

Start the Service Bus emulator

cd local/service-bus-emulator
cp .env.example .env   # set ACCEPT_EULA=Y + strong MSSQL_SA_PASSWORD
docker compose up -d

Run the mocks

dotnet run --project src/SharePointMock --urls http://0.0.0.0:8081
dotnet run --project src/SitecoreMock   --urls http://0.0.0.0:8082

# see what's seeded
curl http://localhost:8081/_seed/catalog

Run the function

cp src/Function/local.settings.json.example src/Function/local.settings.json
cd src/Function
func start            # or: dotnet run

local.settings.json is gitignored. It points the function at the local emulator (SAS) and the local SharePoint/Sitecore mocks on http://localhost:8081 and http://localhost:8082.

Publish a test event

The publisher's defaults already point at the SharePointMock's seeded welcome.txt sample (site-default / drive-default / item-default), so the happy path runs no-args once the emulator + mocks + function are up:

dotnet run --project src/Publisher -- \
  --ServiceBus:ConnectionString="Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"

To force a 404 path (publisher succeeds, function throws, message dead-letters after 10 retries):

dotnet run --project src/Publisher -- `
  --ServiceBus:ConnectionString="..." `
  --Event:ItemId=does-not-exist

Publish to the real Azure topic

az login
dotnet run --project src/Publisher -- `
  --ServiceBus:Namespace=sbns-azfunc-pub-dev.servicebus.windows.net

Uses DefaultAzureCredential — your az session is the principal. You'll need Azure Service Bus Data Sender on the topic.

Pointing "Sitecore" at webhook.site

Grab a unique URL from https://webhook.site and set sitecore_webhook_url in infra/environments/dev/terraform.tfvars, then apply. The function will POST uploads there instead of to the Sitecore container app.

Notes / known sharp edges

  • Flex Consumption requires the AzureWebJobsStorage setting to be set to an empty string and AzureWebJobsStorage__accountName / __credential=managedidentity / __clientId to be used for the actual storage connection. This is a current azurerm provider quirk.
  • Service Bus topic role assignments must be scoped to the subscription resource, not the topic. The function module already does this.
  • ACR must allow ARM audience tokens (default for new registries) for ACA managed-identity image pull to work.
  • dotnet format enforces an initializer-indent rule that puts => new() { ... } bodies two indent levels deeper than the lambda. The lint workflow will fail PRs that drift.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors