The goal is to keep one stable Azure Container Apps Job—with its name, identity, permissions, networking, resources, and lifecycle settings—while varying approved settings for each execution instead of modifying ore creating new Container Apps Jobs.
job-orchestrator receives a Service Bus message, updates the live data-processor job, using it as a template, applies execution-scoped overrides, and starts an isolated execution. The main use case is selecting an approved container image and tag per message, equivalent to a manual Portal override but automated and safe for concurrent requests.
This POC currently overrides message and correlation values only. It retains the image from the live template until registry allowlisting and image validation are implemented.
It confirms the pattern and platform are valid.
This proof of concept uses Azure Container Apps Jobs, Azure Service Bus, and Azure Blob Storage to demonstrate event-driven orchestration and one-shot data processing.
The repository contains both Python 3.13 job implementations. job-orchestrator
receives and settles Service Bus messages and starts a message-specific
data-processor execution. data-processor captures the execution environment,
redacts sensitive values, serializes the snapshot as JSON, and uploads it to a
private Blob container using managed identity.
| Component | Purpose |
|---|---|
job-orchestrator Container Apps Job |
Python event-driven worker scaled from the Service Bus data-processing queue. It receives and settles one message, starts a correlated data-processor execution, and waits for its result. |
data-processor Container Apps Job |
One-shot processor started manually or by job-orchestrator to create and upload an execution environment snapshot. |
| Azure Container Registry | Builds and stores uniquely tagged data-processor and job-orchestrator images. |
| Azure Service Bus | Hosts the queue used by the event-driven orchestration POC. |
| Azure Blob Storage | Stores data-processor execution snapshots in data-processor-execution-snapshots. |
| User-assigned managed identities | Authenticate the jobs without credentials in source code or configuration. |
| Virtual network and private endpoint | Provide private connectivity from the Container Apps environment to Blob Storage. |
| Log Analytics | Collects Container Apps Job system and console logs. |
The intended processing flow is:
- A message is added to the Service Bus queue.
- KEDA activates up to ten
job-orchestratorexecutions. - Each orchestrator receives one message in PeekLock mode and validates its
producer-supplied
MessageIdand JSON body. - The orchestrator starts
data-processorwith an execution-specific template containingMESSAGE_CONTENTand correlation metadata. data-processorcreates a redacted JSON snapshot of its environment.data-processoruploads an idempotent, MessageId-addressed file to Blob Storage and verifies it withGet Blob Properties.- The orchestrator completes the Service Bus message only after that exact
data-processorexecution succeeds. Invalid messages are dead-lettered and transient or processor failures are abandoned for redelivery.
sequenceDiagram
autonumber
actor Producer
participant SB as Azure Service Bus queue
participant KEDA as KEDA scaler
participant JO as job-orchestrator job
participant ACA as Azure Container Apps API
participant DP as data-processor execution
participant Blob as Private Blob container
Producer->>SB: Enqueue JSON body with unique MessageId
KEDA->>SB: Observe queue depth
KEDA->>JO: Start event-driven execution
JO->>SB: Receive one message in PeekLock mode
JO->>SB: Register automatic lock renewal
JO->>JO: Validate MessageId, size, UTF-8, and JSON object
Note over JO,Blob: Azure API calls use user-assigned managed identities
alt Message is invalid
JO->>SB: Dead-letter with validation reason
else Message is valid
JO->>ACA: Read the live data-processor job template
ACA-->>JO: Return template
JO->>ACA: Start execution with message and correlation values
ACA->>DP: Start isolated one-shot execution
DP->>DP: Snapshot environment and redact sensitive values
DP->>Blob: Upload deterministic JSON blob (overwrite=false)
alt Blob is new
Blob-->>DP: Return upload metadata
DP->>Blob: Get Blob Properties
Blob-->>DP: Confirm content length and metadata
else Expected blob already exists
Blob-->>DP: Report existing blob
DP->>Blob: Get Blob Properties
Blob-->>DP: Confirm matching message hashes
end
DP-->>ACA: Exit and update execution status
loop Until the exact execution is terminal or times out
JO->>ACA: Get execution by name and resource ID
ACA-->>JO: Return correlation values and status
JO->>SB: Renew message lock
end
alt data-processor succeeded and correlation matches
JO->>SB: Complete message
else Processor, authentication, or timeout failure
JO->>SB: Abandon message for redelivery
end
end
Starting data-processor through the Container Apps management API provides
message-specific execution templates and precise correlation, but it also makes
job-orchestrator responsible for capacity control and execution lifecycle
decisions that an event-driven job controller would otherwise coordinate.
These responsibilities should be evaluated explicitly before moving beyond a
pilot.
| Control | Current value | Effect |
|---|---|---|
job-orchestrator event scale |
0-10 executions, one queued message per execution, polled every 30 seconds | Normally limits the orchestrated path to approximately ten in-flight messages. |
job-orchestrator replica |
0.5 vCPU, 1 GiB, 600-second timeout, no platform replica retry | One orchestrator remains allocated while it waits for one data-processor execution. |
data-processor execution |
Manual trigger, one replica, one required completion | Each management API start creates one isolated worker execution. |
data-processor replica |
0.5 vCPU, 1 GiB, 300-second timeout, one platform retry | A failed replica can be attempted once more within the execution timeout. |
| Processor monitoring | 300-second deadline, polled every 5 seconds | job-orchestrator follows the exact execution name and resource ID. |
| Service Bus lock | 5-minute initial lock, automatically renewed for up to 540 seconds | The message remains unsettled while data-processor runs. |
| Queue delivery | Maximum delivery count of 10 | Abandoned or lock-expired messages are redelivered and eventually dead-lettered. |
| Management API retry | Five attempts with jittered exponential backoff capped at 8 seconds | Only transient HTTP failures such as throttling and service errors are retried. |
The scale limit is not a hard global data-processor concurrency limit.
data-processor is a manually triggered job, so starts made by another caller,
overlapping redeliveries, or executions already accepted by the platform can
increase concurrency beyond the ten job-orchestrator executions. A production
design that retains this pattern should have a single authorized launcher or a
shared admission-control mechanism, such as a durable lease or semaphore, and
should reject or defer work when the environment is saturated.
At the configured maximum, ten messages can occupy ten job-orchestrator
replicas and ten data-processor replicas at the same time. At 0.5 vCPU and 1 GiB per
replica, that represents approximately 10 vCPU and 20 GiB before platform
overhead, retries, duplicate executions, image pulls, and unrelated workloads
are considered. Validate subscription quotas and Container Apps environment
capacity, and load-test Service Bus connections, Container Apps management API
rate limits, ACR pulls, Blob throughput, private-endpoint networking, and Log
Analytics ingestion. CPU and memory should be based on measured percentiles
rather than the POC defaults; out-of-memory termination, CPU throttling, and
slow startup all extend the Service Bus lock duration.
Timeouts must form a deliberate hierarchy. The data-processor replica timeout,
plus image startup and status propagation margin, should fit inside the
job-orchestrator processor deadline. That deadline and settlement margin should
fit inside lock renewal, and lock renewal should fit inside the job-orchestrator
replica timeout. The current 300-second data-processor timeout and 300-second
processor deadline leave no propagation margin, although the outer 540- and
600-second limits do. Tune these values together from observed startup and
processing latency.
Retries occur at several independent layers:
job-orchestratorretries selected transient Azure management API responses up to five times and honorsRetry-After.- Container Apps can retry one failed
data-processorreplica. job-orchestratorabandons the Service Bus message after processor, authentication, or transient Azure failures.- Service Bus redelivers the message until its maximum delivery count sends it to the dead-letter queue.
These layers multiply the possible amount of work for one logical message. Every side effect must therefore be idempotent, not only the final process exit. This POC uses a deterministic, MessageId-derived blob name and verifies message hash metadata before treating an existing blob as success. Any future database write, API call, notification, or downstream publication needs an equivalent idempotency key or transactional outbox.
The start of a Container Apps execution and Service Bus settlement are not one transaction. Important failure windows include:
job-orchestratorcan lose its response after Azure accepts a start, causing a retry or redelivery to create another execution.job-orchestratorcan stop afterdata-processorstarts; the message lock then expires while the original execution continues without its original monitor.data-processorcan succeed but message completion or lock renewal can fail, so the successful message is delivered again.- Image pull, scheduling, quota, managed identity, RBAC, private DNS, storage, or management API failures can prevent an execution from starting or finishing.
- A processor deadline can expire before Container Apps publishes the terminal status, particularly when timeout values have no margin.
- A correlation mismatch, deterministic blob collision, or malformed message is intentionally failed rather than accepted as ambiguous success.
- Repeated transient failures eventually move a message to the dead-letter queue and require an operational replay or disposal process.
Production monitoring should correlate the message hash, orchestrator
execution, data-processor execution, and blob name. Alert on queue age and
depth, active execution count, startup latency, management API throttling,
processor duration, retries, abandon and lock-loss rates, failed or timed-out
executions, dead-letter growth, and Blob collisions. Define retention and
cleanup for completed execution records and logs, and rehearse recovery from an
orchestrator restart while processors are still running.
job-orchestrator owns the Service Bus message state while data-processor runs.
The queue and the Container Apps execution are separate systems, so every state
transition must be intentional and observable:
| Message state | Transition | job-orchestrator behavior |
|---|---|---|
| Active and available | KEDA observes queue demand and an orchestrator receives one message in PeekLock mode. |
The message becomes locked to that receiver but remains in the queue. Prefetch is disabled and each execution receives at most one message. |
| Locked and validating | job-orchestrator validates MessageId, body type and count, size, UTF-8, JSON syntax, object shape, and broker metadata. |
Invalid messages are dead-lettered immediately with a bounded validation reason; valid messages continue to processing. |
| Locked and processing | job-orchestrator starts and polls the exact data-processor execution. |
AutoLockRenewer extends the lock while processing continues. The renewal budget must exceed processor startup and runtime plus settlement margin. |
| Completed | The correlated data-processor execution reports Succeeded, and no lock-renewal failure was observed. |
job-orchestrator calls complete_message; Service Bus permanently removes the message from the active queue. |
| Abandoned or lock expired | Processor start, authentication, polling, execution, correlation, or timeout fails. The same outcome occurs if the process exits before settlement and the lock later expires. | job-orchestrator attempts abandon_message, making the message available for another delivery. A later delivery has an increased delivery count and can start another data-processor execution. |
| Dead-lettered | Input validation fails, or repeated deliveries reach the queue's maximum delivery count. | Service Bus moves the message to the dead-letter subqueue. Operators must inspect, replay, or dispose of it; the POC does not automatically replay dead-lettered messages. |
Settlement itself can fail. If complete_message, abandon_message, or
dead_letter_message fails, job-orchestrator reports a settlement error but
cannot assume the broker accepted the requested transition. The message can
remain locked until expiry and then reappear. Monitoring must therefore treat
the broker-observed state as authoritative rather than relying only on the
process exit code.
There is no atomic transaction between starting data-processor and completing
the Service Bus message. A crash after the start request can leave a processor
running while the message is redelivered, and a completion failure after a
successful processor run can produce the same result. The deterministic blob
name and hash validation make the current output idempotent, but they do not
prevent duplicate executions. Any additional side effects require their own
idempotency or transaction strategy.
The POC does not defer messages, use Service Bus sessions, reschedule messages, or maintain a separate workflow-state record. If production requirements need ordered processing, delayed retry classes, cancellation, manual approval, or cross-service reconciliation, add an explicit durable state machine rather than inferring workflow state only from queue delivery count and Container Apps execution status.
Azure Container Apps Jobs already provides per-execution controls for
replicaTimeout, replicaRetryLimit, parallelism,
replicaCompletionCount, container CPU and memory, managed identity,
networking, and centralized logs. An event-driven Container Apps Job adds
KEDA-based polling and declarative minimum and maximum executions for a queue.
Making data-processor itself event-driven could let the platform perform
queue-based admission and remove the management-plane start-and-poll loop.
Application code would still own payload validation, Service Bus settlement,
idempotency, and business-level failure classification.
The current manual data-processor job still benefits from native timeout,
replica retry, resource isolation, execution status, and logging, but its
externally initiated starts are admitted by job-orchestrator rather than by its
own KEDA scale rule. Container Apps does not automatically deduplicate those
starts or make them transactional with Service Bus.
A Kubernetes Job exposes additional controller-level primitives when direct
cluster control is appropriate:
parallelism,completions, and indexed completion mode for bounded or partitioned work;backoffLimit, per-index backoff, pod restart policy, and pod failure policy for retry and terminal-failure classification;activeDeadlineSeconds, suspension, and time-to-live cleanup for completed Jobs;- resource requests and limits integrated with scheduling, namespace
ResourceQuotaandLimitRange, priorities, node selection, affinity, taints, and topology controls; - durable Job and Pod status managed by the Kubernetes Job controller, with replacement after Pod or node failure; and
- KEDA
ScaledJobwhen Kubernetes Jobs must be created from queue demand.
Those controls reduce custom scheduling and retry code, but they do not remove the need for idempotent processing, external-system transaction design, dead-letter handling, observability, or workload-specific admission policy. Container Apps Jobs provides a managed subset of these Kubernetes concepts; Kubernetes Jobs provides more control at the cost of operating cluster capacity, policy, upgrades, and the supporting controllers.
References:
.
|-- azure.yaml azd hooks and active deployment entry point
|-- infra/
| |-- main.bicep complete POC infrastructure definition
| |-- modules/ reusable infrastructure modules
| `-- remediation/
| |-- stage1/ private storage, DNS, endpoint, and RBAC
| |-- stage2/ preserved-job image and environment cutover
| |-- target.example.json synthetic public configuration example
| `-- target.local.json ignored authorized Azure target
|-- scripts/remediation/ guarded deployment, validation, and rollback
|-- src/data-processor/
| |-- data_processor/ Python application
| |-- tests/ unit tests
| |-- Dockerfile production image
| `-- requirements.lock hash-locked runtime dependencies
|-- src/job-orchestrator/
| |-- job_orchestrator/ Python orchestration application
| |-- tests/ orchestration and settlement tests
| |-- Dockerfile production image
| `-- requirements.lock hash-locked runtime dependencies
`-- materials/
`-- TriggerMessagePayload-sample.json
- PowerShell 7.
- Azure CLI and Azure Developer CLI (
azd). - An authenticated Azure CLI and
azdsession with access to the target subscription. - Permission to build images in the target Azure Container Registry and deploy resources in the target resource group.
- Python 3.13 for local
data-processortests. - Docker only for optional local image builds.
Clone the repository and authenticate the Azure tools:
git clone https://github.com/embergershared/aca-sbtriggerjoborch-execjobprocessor.git
Set-Location .\aca-sbtriggerjoborch-execjobprocessor
az login
azd auth loginCreate the machine-local target configuration:
Copy-Item `
.\infra\remediation\target.example.json `
.\infra\remediation\target.local.jsonReplace every synthetic value in target.local.json with the identifiers for
the existing Azure environment. Set originalHealthyBaselinePath to the
retained baseline below .azure\remediation-state. The local target file,
AZD state, deployment evidence, environment files, private keys, certificates,
Python environments, and mutable Squad state are excluded by the root
.gitignore.
The active deployment is intentionally bound to the ignored local target. For
another machine-local target, use the ignored
infra\remediation\target.<name>.json convention and set
REMEDIATION_TARGET_CONFIG to that repository-relative path:
$env:REMEDIATION_TARGET_CONFIG = `
'infra\remediation\target.development.json'Do not commit target files, .azure, exported ARM state, deployment baselines,
credentials, or generated environment files. The preflight rejects a different
subscription, resource group, region, identity, repository, or unauthorized
live job state.
Create isolated Python environments and install the hash-locked development dependencies from the approved package feed:
python -m venv .\.venv\data-processor
& .\.venv\data-processor\Scripts\Activate.ps1
python -m pip install `
--index-url https://packagefeedproxy.microsoft.io/pypi/simple/ `
--requirement .\src\data-processor\requirements-dev.txt
Push-Location .\src\data-processor
python -m pytest
Pop-Location
deactivate
python -m venv .\.venv\job-orchestrator
& .\.venv\job-orchestrator\Scripts\Activate.ps1
python -m pip install `
--index-url https://packagefeedproxy.microsoft.io/pypi/simple/ `
--requirement .\src\job-orchestrator\requirements-dev.txt
Push-Location .\src\job-orchestrator
python -m pytest
Pop-Location
deactivateOptionally build both images:
docker build --tag data-processor:local .\src\data-processor
docker build --tag job-orchestrator:local .\src\job-orchestratorThe active azure.yaml workflow reconciles an existing pair of Container Apps
Jobs; it is not an unguarded deployment to an arbitrary subscription. A
preprovision hook validates the AZD environment, Azure subscription, resource
identities, and authorized job state before Stage 1 can make changes. Confirm
that target.local.json and its original healthy baseline describe the target,
then create or select the matching AZD environment:
azd env new <environment-name>
az account set --subscription <azure-subscription-id>Persist the target's non-secret identifiers into the local AZD environment:
.\scripts\remediation\Set-RemediationEnvironment.ps1
$env:AZURE_RESOURCE_GROUP = azd env get-value AZURE_RESOURCE_GROUPRun the repository checks and preview the infrastructure changes:
.\scripts\remediation\Test-AzdDeploymentWiring.ps1
.\scripts\remediation\Test-StaticPreparation.ps1
azd provision --previewReview the preview, then build and deploy:
azd upThe active azd up workflow has two stages:
- Stage 1 reconciles private Blob Storage, the Blob private endpoint, private
DNS, and container-scoped
Storage Blob Data Contributoraccess. - The predeploy hook builds
src\data-processorandsrc\job-orchestratorin ACR with unique tags such asazd-YYYYMMDDTHHMMSSfffZ. Stage 2 validates both resolved manifests, captures both live job baselines, runs ARM validation and what-if, and updates the existing jobs.
The same Container Apps Job resources are preserved. Their managed identities,
triggers, resource limits, and other live properties are retained except for
the explicitly managed image and job-orchestrator runtime settings.
Both jobs use tagged image references rather than digest-qualified references:
<registry>.azurecr.io/data-processor:azd-YYYYMMDDTHHMMSSfffZ
<registry>.azurecr.io/job-orchestrator:azd-YYYYMMDDTHHMMSSfffZ
The resolved SHA-256 digest remains in local deployment evidence and is checked before cutover. If cutover or post-cutover verification fails after a write is attempted, the deployment restores the captured baseline.
At the end of a successful deployment, the postdeploy hook displays:
- The new
data-processorimage name, tag, and resolved digest. - The new
job-orchestratorimage name, tag, and resolved digest.
Deployment evidence is written under
.azure\remediation-state\azd-deploy-* and is excluded from deployment inputs.
The Container Apps Job supplies these environment variables:
| Variable | Required | Purpose |
|---|---|---|
AZURE_BLOB_ACCOUNT_URL |
Yes | HTTPS Blob service endpoint without credentials or a SAS query string. |
AZURE_BLOB_CONTAINER_NAME |
Yes | Existing destination Blob container. |
AZURE_CLIENT_ID |
No | Client ID of the user-assigned managed identity. |
DATA_PROCESSOR_IMAGE |
Deployed by azd up |
Full tagged image reference used by the execution. |
DATA_PROCESSOR_IMAGE_TAG |
Deployed by azd up |
Unique ACR tag used by the execution. |
The image reference and tag are included in every uploaded snapshot, allowing the JSON file to be correlated with the image that produced it.
KEDA activates job-orchestrator from the data-processing queue, but the Python
application receives and settles the message through the Azure Service Bus SDK.
Each execution receives at most one message with prefetch disabled.
A valid message must:
- Have a unique, producer-supplied Service Bus
MessageId. - Contain one data body no larger than 16,384 raw bytes.
- Decode as strict UTF-8 without NUL characters.
- Parse as a JSON object.
- Contain no secrets. The raw body is intentionally visible in the
data-processorexecution template and uploaded snapshot.
Body fields are opaque data-processor input. They cannot select a job, image,
command, identity, CPU, memory, or timeout. The full data-processor execution
template is copied from the live job and only these values are upserted for the
new execution:
MESSAGE_CONTENTMESSAGE_IDMESSAGE_SHA256DELIVERY_COUNTENQUEUED_TIME_UTC
The override applies only to the returned execution and does not change the
persisted data-processor job configuration. This allows concurrent messages
without a shared environment-variable update race.
Delivery is at least once because Service Bus settlement and Container Apps
execution start are not one transaction. data-processor therefore writes
orchestrated snapshots to a deterministic path based on the SHA-256 of
MessageId. A matching existing blob is an idempotent success; a hash or
content mismatch is never overwritten.
Manual executions without MESSAGE_ID create a blob with a sortable UTC name:
YYYY-MM-DDTHH-MM-SS.ffffffZ.json
Orchestrated executions use a deterministic path based on the SHA-256 hash of
the UTF-8 Service Bus MessageId:
messages/<64-hex-message-id-hash>.json
The message-ID hash and message-body hash are stored as Blob metadata. A retry with matching metadata succeeds without overwriting the existing Blob; a collision with different metadata fails.
Sensitive environment values are replaced by
[REDACTED:SENSITIVE_ENV_VALUE]. Redaction covers passwords, secrets, tokens,
keys, connection strings, SAS values, platform identity headers, and common
CI/CD credential variables. Environment values are never written to stdout or
stderr.
The application authenticates with ManagedIdentityCredential. It does not
accept storage account keys, connection strings, SAS tokens, embedded
credentials, or interactive authentication.
An execution succeeds only after:
- The JSON content is serialized.
- The blob upload completes.
- Azure returns upload metadata.
Get Blob Propertiesconfirms the blob exists.- The uploaded content length matches the serialized content length.
The process returns 0 on success and a nonzero exit code for configuration,
authentication, upload, or unexpected failures.
After azd up, send this exact raw body to the data-processing queue:
{"test":"ok"}Set a unique Service Bus MessageId property separately, for example
test-<unique-id>. Do not wrap or escape the JSON body. The producer
identity needs Azure Service Bus Data Sender on the queue.
List and follow job-orchestrator executions:
az containerapp job execution list `
--name job-orchestrator `
--resource-group $env:AZURE_RESOURCE_GROUP `
--output table
az containerapp job logs show `
--name job-orchestrator `
--resource-group $env:AZURE_RESOURCE_GROUP `
--container job-orchestrator `
--follow true `
--format textThe logs identify validation, the message-body hash, the exact data-processor
execution, status transitions, and settlement without printing the body or raw
MessageId.
MESSAGE_CONTENT, MESSAGE_ID, MESSAGE_SHA256, DELIVERY_COUNT, and
ENQUEUED_TIME_UTC exist only on the individual data-processor execution
template. They intentionally do not appear in the persisted job configuration,
which prevents concurrent messages from overwriting shared state. Inspect their
names on a specific execution with:
az containerapp job execution show `
--name data-processor `
--resource-group $env:AZURE_RESOURCE_GROUP `
--job-execution-name <execution-name> `
--query "properties.template.containers[0].env[].name" `
--output tsvSend the same body with the same MessageId again to test idempotency. A retry
may start another data-processor execution, but it must reuse the same
deterministic Blob and report an idempotent success rather than creating a
second file.
Start a data-processor execution:
az containerapp job start `
--name data-processor `
--resource-group $env:AZURE_RESOURCE_GROUPList recent executions:
az containerapp job execution list `
--name data-processor `
--resource-group $env:AZURE_RESOURCE_GROUP `
--output tabledata-processor writes progress to stdout, including:
- Execution start and completion.
- Blob name, serialized size, and environment variable count.
- Credential-free destination URL.
- Upload start and byte percentage.
- Upload ETag, last-modified time, request ID, and version ID.
- Final remote content-length verification.
Example completion messages:
Blob upload completed: destination=https://<account>.blob.core.windows.net/data-processor-execution-snapshots/<blob>.json size_bytes=<bytes> etag=<etag> last_modified=<time> request_id=<id> version_id=<version>.
Blob availability verified: destination=https://<account>.blob.core.windows.net/data-processor-execution-snapshots/<blob>.json content_length=<bytes> expected_length=<bytes>.
data-processor job execution completed: uploaded_blob=<blob>.json.
Blob Storage is configured with public network access disabled and
defaultAction: Deny. The data-processor identity receives Storage Blob Data Contributor at the container scope.
Successful PutBlob and GetBlobProperties operations prove that the job
uploaded and verified the blob. The Azure Portal may still show a network or
authorization error when the browser is outside the approved network path.
When using an IP allowlist or corporate proxy, verify the public egress IP
actually presented to Azure.
Both Dockerfiles install hash-locked dependencies from the approved Microsoft
package feed proxy and run as non-root UID/GID 10001.
Run the deployment wiring checks independently:
.\scripts\remediation\Test-AzdDeploymentWiring.ps1
.\scripts\remediation\Test-StaticPreparation.ps1More application-specific information is available in
src\data-processor\README.md and
src\job-orchestrator\README.md.
- Managed identity is used for Blob Storage, Service Bus, Container Apps management, and ACR access.
- Blob access is private and protected by private DNS and a private endpoint.
- Blob RBAC is scoped to the destination container.
- Container Registry anonymous pull and admin credentials are not used.
- The
data-processorcontainer runs as a non-root user. - Runtime dependencies are version-pinned and hash-locked.
- Deployment guards reject target drift and unauthorized job mutations.
job-orchestratorhas queue-scoped Service Bus Data Receiver anddata-processor-scoped Container Apps Jobs Operator; it does not have resource-group Contributor.- Message content is never persisted in the default job configuration or deployment evidence.
- Stage 2 preserves, verifies, and can roll back both existing jobs.
- Deployment and rollback both run ARM validation and what-if before mutation.