Skip to content

Fix BCP139 cross-resource-group ACR AcrPull role in compute environments#18118

Draft
davidfowl wants to merge 2 commits into
mainfrom
davidfowl/acr-crossrg-minimal-fix
Draft

Fix BCP139 cross-resource-group ACR AcrPull role in compute environments#18118
davidfowl wants to merge 2 commits into
mainfrom
davidfowl/acr-crossrg-minimal-fix

Conversation

@davidfowl

@davidfowl davidfowl commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #11256

When a compute environment (Azure Container Apps or Azure App Service) is configured to pull from an existing Azure Container Registry that lives in a different resource group (via PublishAsExisting("myacr", "my-existing-resource-group") + env.WithAzureContainerRegistry(acr)), the generated env.bicep inlined an AcrPull role assignment scoped directly to that cross-resource-group registry. Bicep rejects a resource whose scope doesn't match the file's scope with BCP139 ("A resource's scope must match the scope of the Bicep file ... use modules to deploy resources to a different scope"), so aspire publish produced bicep that fails to deploy.

Relationship to #17988

This is the companion fix for #17988. Live PR-build testing of #17988's Azure scope APIs found that the shared organization ACR scenario from #7514 still fails end to end for Azure Container Apps unless the AcrPull role assignment is moved out of the ACA environment module. #17988 provides the scope model/APIs, and this PR makes the ACA/App Service generated Bicep composition deployable for cross-resource-group existing registries.

What changed for users

Publishing an ACA or App Service environment that pulls from a cross-resource-group existing ACR now succeeds: the AcrPull role assignment is emitted as a separately-scoped module instead of being inlined in env.bicep, so it deploys without BCP139. The granted role is unchanged (AcrPull on the same identity and registry) — only where the assignment is declared moves. Run mode and same-resource-group scenarios are unaffected.

Approach (minimal)

In publish mode, the ACR-pull identity is materialized as a first-class AzureUserAssignedIdentityResource ({env}-mi) added to the model, and the existing internal ...AcrPullIdentityAnnotation is flagged with a new internal AssignAcrPullRole. This flips the environment's infrastructure callback onto its existing "bring-your-own identity" branch (the env module reads the identity id, and for App Service the client id, from a parameter), so the callback no longer emits the inline _mi identity or the inline AcrPull role.

A per-package pre-prepare pipeline step (requiredBy: PrepareResourcesStepName) resolves the environment's final registry — honoring any later WithAzureContainerRegistry swap, last-writer-wins — and adds a RoleAssignmentAnnotation(registry, AcrPull) to the generated identity. The unchanged AzureResourcePreparer then turns that annotation on the standalone identity into a separately-scoped {identity}-roles-{registry} module, setting Scope = resourceGroup(...) when the registry is an existing cross-resource-group resource. That scoped module is the BCP139 fix.

WithAcrPullIdentity (bring-your-own identity) removes the generated identity and skips the role assignment, preserving today's behavior where the caller owns the role via .WithRoleAssignments(acr, AcrPull).

Constraints honored

  • No public API change (no api/*.cs changes).
  • AzureResourcePreparer.cs untouched — the fix reuses its existing cross-resource-group role-module machinery.
  • AKS untouched; the ContainerRegistry package untouched.
  • No InternalsVisibleTo / .csproj changes. Reuses the existing public RoleAssignmentAnnotation.
  • Run mode behavior unchanged (cross-RG in run mode remains a pre-existing, out-of-scope limitation).

Validation

  • Two new regression tests (ACA + App Service) reproduce Role Assignment for Existing Azure Container Registry Fails Due to Bicep Scope Limitation #11256 with an existing cross-RG ACR and assert the generated env.bicep contains no inline AcrPull role assignment and that main.bicep emits a {env}-mi-roles-acr module scoped to resourceGroup('my-existing-resource-group').
  • Updated managed-identity / preparer count tests and the AzureDeployerTests fake provisioner (now supplies id/principalId/clientId outputs for the generated *-mi identity modules).
  • ~63 regenerated Verify snapshots reflect the expected, uniform delta: inline _mi identity + inline AcrPull role removed from env.bicep; the env module now takes the identity id (and client id, for App Service) as a parameter; the role moves to a scoped module.
  • Full Aspire.Hosting.Azure.Tests: 1477 passed, 5 skipped. The only failures (15) are pre-existing Docker-emulator functional tests (EventHubs/Storage/SignalR/Cosmos) unrelated to this change.

Minimal alternative to #17992 (which refactors AzureResourcePreparer and adds public API); opening this so both approaches can be compared. Not closing #17992.

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No

In publish mode, AddAzureContainerAppEnvironment and AddAzureAppServiceEnvironment
inlined an AcrPull role assignment into the environment's bicep file scoped to the
container registry. When the registry is an existing resource in a different resource
group (via PublishAsExisting), Bicep rejected the cross-scope role assignment with
BCP139 ("A resource's scope must match the scope of the Bicep file").

The fix reuses the existing model-resource + role-module machinery that the
user-supplied identity path (WithAcrPullIdentity) already uses, so the unmodified
AzureResourcePreparer emits a separate {identity}-roles-{registry} module and sets
its scope to the registry's resource group for an existing cross-RG registry.

- Add an internal AssignAcrPullRole flag to the existing per-package
  AcrPullIdentityAnnotation types to mark Aspire-generated default identities.
- In publish mode, create a default AzureUserAssignedIdentityResource ({env}-mi)
  and attach it via the existing annotation so the env callback reads the identity
  id as a parameter and skips the inline identity + inline AcrPull role. Run mode
  is unchanged.
- Add a per-package pre-prepare pipeline step that resolves the environment's final
  configured registry (honoring a later WithAzureContainerRegistry swap) and adds a
  RoleAssignmentAnnotation(registry, AcrPull) to the generated identity.
- WithAcrPullIdentity removes the generated identity when the user supplies their own.

No public API changes, no AzureResourcePreparer changes, no AKS changes. Adds
regression tests for both ACA and App Service asserting the inline cross-scope role
is gone and a role module scoped to the existing registry's resource group is emitted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 11, 2026 15:26
@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 18118

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 18118"

@davidfowl davidfowl marked this pull request as draft June 11, 2026 15:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes #11256 — a BCP139 Bicep deployment error triggered when an Azure compute environment (ACA or App Service) is configured to pull from an existing Azure Container Registry in a different resource group. The inline AcrPull role assignment in env.bicep failed because Bicep requires modules for cross-scope deployments. The fix extracts the managed identity and its role assignment into separately deployable modules, reusing the existing AzureResourcePreparer cross-resource-group scoping machinery.

Changes:

  • The ACR-pull identity (env-mi) is now materialized as a first-class AzureUserAssignedIdentityResource in publish mode, with the AcrPull role assignment emitted as a scoped module (env-mi-roles-env-acr) instead of being inlined in env.bicep.
  • A new pipeline step (per compute package) runs before azure-prepare-resources to resolve the final registry and attach the RoleAssignmentAnnotation to the generated identity, ensuring correct last-writer-wins semantics for WithAzureContainerRegistry.
  • Two new regression tests (ACA + App Service) verify cross-resource-group ACR scenarios produce correctly scoped modules.
Show a summary per file
File Description
src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs Adds pipeline step, helper methods for identity creation, role assignment, and registry resolution (ACA path)
src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentAcrPullIdentityAnnotation.cs Adds assignAcrPullRole constructor parameter and AssignAcrPullRole property
src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentExtensions.cs Same pipeline step and helper methods (App Service path, near-identical to ACA)
src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentAcrPullIdentityAnnotation.cs Adds assignAcrPullRole constructor parameter and AssignAcrPullRole property
tests/Aspire.Hosting.Azure.Tests/AzureContainerAppEnvironmentExtensionsTests.cs New regression test for cross-RG ACR with ACA
tests/Aspire.Hosting.Azure.Tests/AzureAppServiceEnvironmentExtensionsTests.cs New regression test for cross-RG ACR with App Service
tests/Aspire.Hosting.Azure.Tests/AzureUserAssignedIdentityTests.cs Updated resource collection assertions to account for new env-mi identity
tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs Fake provisioner updated with env-mi module outputs
tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs Updated resource count and collection assertions
tests/Aspire.Hosting.Azure.Tests/AzureAppServiceTests.cs Updated resource count and collection assertions
~63 .verified.bicep/.verified.json/.verified.txt snapshots Reflect identity moved to standalone module, role assignment moved to scoped module

Copilot's findings

  • Files reviewed: 72/72 changed files
  • Comments generated: 1

@davidfowl davidfowl mentioned this pull request Jun 12, 2026
14 tasks
The AcrPull role helpers and pipeline step are intentionally duplicated
across the AppContainers and AppService packages (they don't share
internals). Add reciprocal keep-in-sync notes so future maintainers know
the two copies must evolve together.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

CLI E2E Tests unknown — 115 passed, 0 failed, 2 unknown (commit 1032edd)

View all recordings
- Test Detail
AddPackageInteractiveWhileAppHostRunningDetached Recording · Job · CLI logs
AddPackageWhileAppHostRunningDetached Recording · Job · CLI logs
AgentCommands_AllHelpOutputs_AreCorrect Recording · Job · CLI logs
AgentInitCommand_DefaultSelection_InstallsDefaultSkills Recording · Job · CLI logs
AgentInitCommand_MigratesDeprecatedConfig Recording · Job · CLI logs
AgentInit_NonInteractive_BundleOnlySkillsNotInCatalog Recording · Job · CLI logs
AgentMcpListResources_ExcludesResourceMarkedWithExcludeFromMcp Recording · Job · CLI logs
AgentMcpListStructuredLogsReturnsLogsFromStarterApp Recording · Job · CLI logs
AgentMcpListStructuredLogsReturnsLogsFromStarterApp_DevLocalhost Recording · Job · CLI logs
AgentMcpListStructuredLogsReturnsLogsFromStarterApp_Isolated Recording · Job · CLI logs
AllPublishMethodsBuildDockerImages Recording · Job · CLI logs
AspireAddAndStartWorkAgainstLegacyAppHostTs Recording · Job · CLI logs
AspireAddPackageVersionToDirectoryPackagesProps Recording · Job · CLI logs
AspireInitSingleFileAppHostRunsViaDotnetRunAppHost Recording · Job · CLI logs
AspireInit_ExistingAppHostDir_RecreatesNuGetConfigKeepsFiles Recording · Job · CLI logs
AspireInit_SolutionFile_BuildsAgainstChannelHive Recording · Job · CLI logs
AspireStartUpdatesStaleTypeScriptAppHostPath Recording · Job · CLI logs
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps Recording · Job · CLI logs
AspireUpdateRemovesOrphanAppHostPackageVersionWhenSdkAlreadyCurrent Recording · Job · CLI logs
Banner_DisplayedOnFirstRun Recording · Job · CLI logs
Banner_DisplayedWithExplicitFlag Recording · Job · CLI logs
Banner_NotDisplayedWithNoLogoFlag Recording · Job · CLI logs
CertificatesClean_RemovesCertificates Recording · Job · CLI logs
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate Recording · Job · CLI logs
CertificatesTrust_WithUntrustedCert_TrustsCertificate Recording · Job · CLI logs
ConfigSetGet_CreatesNestedJsonFormat Recording · Job · CLI logs
CreateAndRunAspireStarterProject Recording · Job · CLI logs
CreateAndRunAspireStarterProjectWithBundle Recording · Job · CLI logs
CreateAndRunEmptyAppHostProject Recording · Job · CLI logs
CreateAndRunJavaEmptyAppHostProject Recording · Job · CLI logs
CreateAndRunJsReactProject Recording · Job · CLI logs
CreateAndRunPolyglotAppHostWithDevLocalhostUrls Recording · Job · CLI logs
CreateAndRunPythonReactProject Recording · Job · CLI logs
CreateAndRunTypeScriptEmptyAppHostProject Recording · Job · CLI logs
CreateAndRunTypeScriptStarterProject Recording · Job · CLI logs
CreateJavaAppHostWithViteApp Recording · Job · CLI logs
CreateTypeScriptAppHostWithViteApp_UsesConfiguredToolchain Recording · Job · CLI logs
DashboardRunWithAgentMcpListTracesReturnsNoTraces Recording · Job · CLI logs
DashboardRunWithAgentMcpListTracesReturnsNoTraces_DevLocalhost Recording · Job · CLI logs
DashboardRunWithOtelTracesReturnsNoTraces Recording · Job · CLI logs
DashboardRunWithOtelTracesReturnsNoTraces_DevLocalhost Recording · Job · CLI logs
DeployK8sBasicApiService Recording · Job · CLI logs
DeployK8sWithExternalHelmChart Recording · Job · CLI logs
DeployK8sWithGarnet Recording · Job · CLI logs
DeployK8sWithMongoDB Recording · Job · CLI logs
DeployK8sWithMySql Recording · Job · CLI logs
DeployK8sWithPostgres Recording · Job · CLI logs
DeployK8sWithRabbitMQ Recording · Job · CLI logs
DeployK8sWithRedis Recording · Job · CLI logs
DeployK8sWithSqlServer Recording · Job · CLI logs
DeployK8sWithValkey Recording · Job · CLI logs
DeployTypeScriptAppToKubernetes Recording · Job · CLI logs
DescribeCommandResolvesReplicaNames Recording · Job · CLI logs
DescribeCommandShowsRunningResources Recording · Job · CLI logs
DetachFormatJsonProducesValidJson Recording · Job · CLI logs
DetachFormatJsonProducesValidJsonWhenRestartingExistingInstance Recording · Job · CLI logs
DoPublishAndDeployListStepsWork Recording · Job · CLI logs
DocsCommand_RendersInteractiveMarkdownFromLocalSource Recording · Job · CLI logs
DoctorCommand_DetectsDeprecatedAgentConfig Recording · Job · CLI logs
DoctorCommand_TypeScriptAppHostReportsMissingConfiguredToolchain Recording · Job · CLI logs
DoctorCommand_WithSslCertDir_ShowsTrusted Recording · Job · CLI logs
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted Recording · Job · CLI logs
DotNetRunFileBasedAppHostUsesAspireCliBundle Recording · Job · CLI logs
DotNetRunProjectAppHostUsesAspireCliBundle Recording · Job · CLI logs
GatewayWithoutExternalEndpoint_FailsPublishWithGuidance Recording · Job · CLI logs
GeneratedAspireDevScript_StartsWatchMode_WithConfiguredToolchain Recording · Job · CLI logs
GlobalMigration_HandlesCommentsAndTrailingCommas Recording · Job · CLI logs
GlobalMigration_HandlesMalformedLegacyJson Recording · Job · CLI logs
GlobalMigration_PreservesAllValueTypes Recording · Job · CLI logs
GlobalMigration_SkipsWhenNewConfigExists Recording · Job · CLI logs
GlobalSettings_MigratedFromLegacyFormat Recording · Job · CLI logs
IngressWithoutExternalEndpoint_FailsPublishWithGuidance Recording · Job · CLI logs
InitTypeScriptAppHost_AugmentsExistingViteRepoInWorkspaceSubdirectory Recording · Job · CLI logs
InteractiveCSharpInitCreatesExpectedFiles Recording · Job · CLI logs
InvalidAppHostPathWithComments_IsHealedOnRun Recording · Job · CLI logs
JavaScriptHostingApisRunFromTypeScriptAppHost Recording · Job · CLI logs
LatestCliCanStartStableChannelAppHost Recording · Job · CLI logs
LatestCliCanStartStableChannelTypeScriptAppHost Recording · Job · CLI logs
LegacySettingsMigration_AdjustsRelativeAppHostPath Recording · Job · CLI logs
LogsCommandShowsResourceLogs Recording · Job · CLI logs
OtelLogsReturnsStructuredLogsFromStarterApp Recording · Job · CLI logs
OtelLogsReturnsStructuredLogsFromStarterAppIsolated Recording · Job · CLI logs
ProcessCommandCallbackReceivesCliArguments Recording · Job · CLI logs
PsCommandListsRunningAppHost Recording · Job · CLI logs
PsFormatJsonOutputsOnlyJsonToStdout Recording · Job · CLI logs
PublishJavaScriptPatternsGeneratesExpectedDockerComposeArtifacts Recording · Job · CLI logs
PublishWithConfigureEnvFileUpdatesEnvOutput Recording · Job · CLI logs
PublishWithDockerComposeServiceCallbackSucceeds Recording · Job · CLI logs
PublishWithoutOutputPathUsesAppHostDirectoryDefault Recording · Job · CLI logs
ResourceCommand_FailedExec_ShowsLogPathAndLogHasEntries Recording · Job · CLI logs
ResourceCommand_SetAndDeleteParameterUpdatesDescribeOutput Recording · Job · CLI logs
RestoreGeneratesSdkFiles Recording · Job · CLI logs
RestoreGeneratesSdkFiles_WithConfiguredToolchain Recording · Job · CLI logs
RestoreRefreshesGeneratedSdkAfterAddingIntegration Recording · Job · CLI logs
RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes Recording · Job · CLI logs
RunFromParentDirectory_UsesExistingConfigNearAppHost Recording · Job · CLI logs
RunReportsSyntaxErrorsForDotNetAppHost Recording · Job · CLI logs
RunReportsSyntaxErrorsForTypeScriptAppHost Recording · Job · CLI logs
SecretCrudOnDotNetAppHost Recording · Job · CLI logs
SecretCrudOnTypeScriptAppHost Recording · Job · CLI logs
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels Recording · Job · CLI logs
StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets Recording · Job · CLI logs
StartReportsSyntaxErrorsForDotNetAppHost Recording · Job · CLI logs
StartReportsSyntaxErrorsForTypeScriptAppHost Recording · Job · CLI logs
StopAllAppHostsFromAppHostDirectory Recording · Job · CLI logs
StopJavaPolyglotAppHostUsingApphostDirectory Recording · Job · CLI logs
StopNonInteractiveSingleAppHost Recording · Job · CLI logs
StopTypeScriptPolyglotAppHostUsingApphostDirectory Recording · Job · CLI logs
StopWithNoRunningAppHostExitsSuccessfully Recording · Job · CLI logs
TerminalAttachFrontend_ShowsViteHelpAndDetaches Recording · Job · CLI logs
TypeScriptAppHostRunDoesNotDeadlockWhenLazyOptionsInvokeAsyncCallback Recording · Job · CLI logs
TypeScriptAppHostWithVite_AllowsDifferentGuestPkgManager Recording · Job · CLI logs
UnAwaitedChainsCompileWithAutoResolvePromises Recording · Job · CLI logs
UpdateToStable_CSharpEmptyAppHost_KeepsConfigChannel Recording · Job · CLI logs
UpdateToStable_CSharpSingleFileInit_KeepsConfigChannel Recording · Job · CLI logs
UpdateToStable_TypeScriptSingleFileInit_KeepsConfigChannel Recording · Job · CLI logs
UpdateToStable_TypeScript_PreviewsStablePkgsAndKeepsChannel Recording · Job · CLI logs

📹 Recordings uploaded automatically from CI run #27402601210

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Role Assignment for Existing Azure Container Registry Fails Due to Bicep Scope Limitation

2 participants