Keep automation jobs alive even when governance locks public storage access
This document treats "how to keep an automation job running even when a cloud governance policy auto-locks your storage" as a single Azure architecture case study, explaining which services were combined and why, from the perspective of an engineer who wants to rebuild it. (As of 2026-06-16 · Region: East US 2 · Target: the two digest agents behind moontaeyang.com)
🎯 What this demo shows: Many enterprise/regulated environments run a central governance automation that periodically enforces security baselines like "block public storage access (
publicNetworkAccess=Disabled)" silently. Even if you turn it back on, it flips off again within minutes to a day. In such an environment, an automation job that relied on public access suddenly breaks one day. The fix is not to fight it but to route around it via a private path (Private Endpoint) — a design that coexists with the policy while keeping the job unbroken.
MCAPSGov-AutomationApp) periodically reverts storage
publicNetworkAccess to Disabled → the Container Apps Job that read/wrote blobs over the public
path fails with AuthorizationFailure.pna=Disabled blocks only the public internet; Private Endpoint (private IP)
traffic is always allowed. So if you put the job inside a VNet and give the storage a private
path (PE), the job keeps working even when the policy locks public access.Keywords: Private Endpoint · VNet-injected Container Apps · Private DNS · VNet Peering · Managed Identity (keyless) · coexisting with policy
[Public users] ※ Unaffected even when storage public access (pna) is off
│ HTTPS
▼
Azure Static Web Apps ── moontaeyang.com (engit-daily · azure-daily sections)
▲ SWA owns public serving
│ swa deploy (outbound)
│
┌─────────────────────────── rg-english-daily ───────────────────────────┐
│ VNet 10.60.0.0/16 │
│ ├ snet-aca /23 (delegation: Microsoft.App/environments) │
│ │ └ Container Apps Env (VNet-injected, workload profile) │
│ │ └ engitdaily-job ── UAMI (keyless) ─┐ │
│ └ snet-pe /24 │ blob R/W │
│ └ Private Endpoint(blob) 10.60.2.4 ◀──────┘ │
│ │ private path │
│ ▼ │
│ Storage engitdailyst… (PE passes even when pna=Disabled) │
│ Private DNS: privatelink.blob.core.windows.net │
│ engitdailyst… → 10.60.2.4 (+ azwhatsnewst… → 10.61.2.4 x-reg) │
└──────────────────────────────┬──────────────────────────────────────────┘
│ VNet Peering (bidirectional, Connected)
┌──────────────────────────────┴──── rg-azwhatsnew ───────────────────────┐
│ VNet 10.61.0.0/16 (snet-aca /23, snet-pe /24) │
│ Container Apps Env (VNet-injected) └ azwhatsnew-job ─ UAMI ─ PE 10.61.2.4 │
│ Storage azwhatsnewst… · Private DNS (same, engit record x-registered) │
└─────────────────────────────────────────────────────────────────────────┘
Governance (MCAPSGov-AutomationApp): periodically forces both storages' pna to Disabled → jobs still work via PE
caeName to …-caev2-… to create a new env (workload profile +
vnetConfiguration.infrastructureSubnetId). Because environmentId is an immutable job property,
delete the job first (az containerapp job delete) before redeploying to avoid a conflict.Microsoft.Network/privateEndpoints (groupIds=blob) for the storage in snet-pe.privatelink.blob.core.windows.net Private DNS zone + VNet link + dnsZoneGroup → the PE
auto-registers the A record.engitdailyst….blob.core.windows.net → 10.60.2.4 (private) → passes even
when pna=Disabled.blob sub-resource is enough (the
static-website $web also goes through the blob endpoint).Connected) — so packets route to the other VNet's PE private IP.
2. DNS cross-registration — since a VNet links to only one zone of a given name, put both
storages' A records in each zone (engit zone += azwhatsnewst…→10.61.2.4, azwn zone +=
engitdailyst…→10.60.2.4).pna=Disabled, the storage static-web endpoint (z##.web.core.windows.net) returns 404.
But visitors arrive via Static Web Apps (moontaeyang.com), so the public site is up 24/7.--static-website step fails on the data plane when pna=Disabled, so guard it
with || echo skip.With both storages locked (publicNetworkAccess=Disabled + defaultAction=Deny), both jobs ran:
| Check | Result |
|---|---|
| engit job / azwn job | both Succeeded |
| own blob write | Blob upload complete … ✅ |
| cross-mirror (read peer warehouse) | mirrored engit-daily:4 / azure-daily:8 ✅ (via PE) |
| auth errors | 0 |
| live sites | moontaeyang.com /·/engit-daily/·/azure-daily/ all 200 |
| notification | Telegram 200 |
→ Empirically confirmed: automation keeps running even in the steady-state where the policy has locked public access.
| Item | Value |
|---|---|
| Private Endpoint | 2 ≈ $15/mo + small data processing |
| Private DNS / VNet / Peering | effectively free (same-region peering, tiny queries) |
| Env recreation | one-time effort (downtime is short, between cron runs) |
pna=Enabled at job start is cheaper and needs no
infra, but it keeps "tug-of-war" with the policy and briefly reopens public access. Option B (PE)
works with public closed forever → cleaner security and satisfies the regulated "no data-plane
exposure" requirement.Microsoft.Network/privateEndpoints, separate from the governance's
storage/NSG rewrites, and pna=Disabled doesn't block PEs → durably safe.environmentId is immutable).privatelink.blob.core.windows.net Private DNS to the storage.--static-website, etc.) tolerant of pna=Disabled.--public-network-access Disabled) and run the job to verify the
steady-state.agents/english-it-daily/infra/main.bicep, agents/azure-whats-new-digest/infra/main.bicep
(VNet · PE · Private DNS · VNet-injected env)agents/*/deploy.sh (static-web step tolerant of pna)reference/mcaps-governance-체크리스트.md (§3 storage pna)playbook/CHANGELOG.md 2026-06-16