Employee Sync Function — complete setup guide

Complete end-to-end setup guide for the CiAnywhere employee sync Function App, including Azure configuration and validation.

This walks through everything, start to finish: getting the code into the (currently empty) Function App, wiring up the trigger, identity, secrets, and the two Table Storage tables, and testing the whole path before pointing the real ETL step at it.

Starting point: a resource group already containing a Storage Account, Key Vault (kv-cia-employee-sync), an empty Function App (Flex Consumption, Linux), Application Insights, a Smart Detector alert rule, and the App Service Plan.


1. Confirm the runtime version

Function App → Configuration → check FUNCTIONS_WORKER_RUNTIME_VERSION (or the runtime version shown on the Overview blade). Flex Consumption's generally-available PowerShell version is 7.4. This code has nothing 7.6-specific in it, but confirm what's actually configured so there are no surprises later.

2. Get the code into the Function App

The project has no module dependencies (see the "Why no Az module" note at the bottom), so deployment is just the five files as-is — no build step, no vendoring.

Recommended: Azure Functions Core Tools, since it detects the right deployment mechanism for Flex Consumption automatically rather than you having to pick the right CLI command yourself:

# from the EmployeeSyncFunction folder, containing host.json etc.
func azure functionapp publish <your-function-app-name>

Make sure Core Tools is a recent version (func --version — v4.x) so it understands Flex Consumption's deployment model.

Alternative: Azure CLI zip deploy. Microsoft's own documentation is inconsistent about which command Flex Consumption expects — some current docs show config-zip, others say Flex requires the newer unified deploy command. If one fails, try the other:

Compress-Archive -Path * -DestinationPath ../EmployeeSyncFunction.zip -Force

# Try this first
az functionapp deploy --resource-group <rg-name> --name <your-function-app-name> `
    --src-path ../EmployeeSyncFunction.zip --type zip

# If that errors, fall back to
az functionapp deployment source config-zip --resource-group <rg-name> `
    --name <your-function-app-name> --src ../EmployeeSyncFunction.zip

3. Confirm the trigger registered

Function App → Functions blade should now list HttpTrigger. A quick reachability check (expect a 401, not a 404 — that confirms routing works even before auth is configured):

Invoke-RestMethod -Method Post -Uri "https://<your-function-app-name>.azurewebsites.net/api/employees/sync"

4. Enable a system-assigned managed identity

Function App → Identity → System assigned → On → Save. This identity is what lets the function read the Key Vault secret and read/write Table Storage — no keys or connection strings anywhere in this design.

5. Grant that identity two roles

First check kv-cia-employee-sync → Access configuration, to see whether it uses Azure RBAC or Vault access policy — the two grant permissions differently.

Resource If RBAC If Access Policy
kv-cia-employee-sync Access control (IAM) → Add role assignment → Key Vault Secrets User → the Function App's identity Access policies → Add access policy → Secret permissions: Get → the Function App's identity
Storage Account Access control (IAM) → Add role assignment → Storage Table Data Contributor → the Function App's identity (Storage accounts only use RBAC)

6. Add the app settings

Function App → Configuration → New application setting, one at a time:

Name Value
CiaEtlSecret @Microsoft.KeyVault(VaultName=kv-cia-employee-sync;SecretName=ciaETLsecret)
TableStorageAccountName your storage account's name
EmployeeTableName Employees
OuMappingTableName OrganisationUnitMappings (optional — this is the default if unset)

After saving, confirm CiaEtlSecret shows a resolved (green) Key Vault reference status on the Configuration blade — a red/warning icon there means step 5's role grant hasn't propagated yet, or the vault/secret name is wrong.

Note on rotation: the platform caches this resolved value and refreshes it periodically (roughly daily, or on restart/scale) — not on every request. If you rotate the secret in Key Vault and need the Function to pick it up immediately, restart the Function App.

7. Create the two tables

Same storage account, two tables (Storage Explorer, portal, or New-AzStorageTable):

  • Employees — no need to pre-define columns; each upsert just writes whatever properties are present on that record. PartitionKey = paylocation, RowKey = employeeid. Every write also adds/preserves SyncCreatedUtc (set once, on first insert, and carried forward unchanged on every later update). There's no separate "last updated" field to maintain — Table Storage automatically stamps every entity with a built-in Timestamp property on every write, so that's already available for free. Query it like any other property, e.g. to find everything touched since a given point: $filter=Timestamp gt datetime'2026-07-29T00:00:00Z'.

  • OrganisationUnitMappings — seed with one row per 3-character prefix:

    PartitionKey RowKey MappedGuid
    Mapping GRO <guid>

    PartitionKey is always the literal string Mapping (keeps the function's lookup a cheap single-partition query); RowKey is the uppercase 3-char prefix; MappedGuid is what it resolves to. Any organisationunit value whose prefix isn't in this table gets logged as a warning and left unmapped, rather than written with a blank id.

8. Test end to end

$uri    = 'https://<your-function-app-name>.azurewebsites.net/api/employees/sync'
$secret = 'the-actual-secret-value'   # from Key Vault, local testing only
$body   = Get-Content ./sample-payload.xml -Raw

# Expect 200 with a JSON summary: totalRecords / upserted / skipped / failed
Invoke-RestMethod -Method Post -Uri $uri -Headers @{ Authorization = "Bearer $secret" } `
    -ContentType 'application/xml' -Body $body

# Expect a 401 — confirms bad tokens are actually rejected, not just accepted
try {
    Invoke-RestMethod -Method Post -Uri $uri -Headers @{ Authorization = 'Bearer wrong-secret' } `
        -ContentType 'application/xml' -Body $body
} catch {
    $_.Exception.Response.StatusCode
    $_.ErrorDetails.Message
}

Check the Employees table afterwards to confirm rows landed with the expected PartitionKey/RowKey/organisationUnitId.

9. Point the real ETL step at it

  • Method: POST

  • URL: https://<your-function-app-name>.azurewebsites.net/api/employees/sync

  • Headers: Authorization: Bearer <ciaETLsecret value>, Content-Type: application/xml (or text/xml)

  • Body: the XML array, unmodified

No function key needed — the bearer token check is the actual gate. (Consider also setting authLevel + a function key as defense-in-depth if you want belt-and-suspenders, though it's not required for this to work.)


Why no Az module

Earlier versions of this used Az.Accounts (Connect-AzAccount -Identity / Get-AzAccessToken) to get a Table Storage token via managed identity. That's now a direct Invoke-RestMethod call to the Functions platform's local token endpoint ($env:IDENTITY_ENDPOINT / $env:IDENTITY_HEADER, both set automatically once step 4 is done) — same result, but with nothing to vendor and nothing pinned to a module version. requirements.psd1 is intentionally empty (@{}).