Is there an existing issue for this?
Describe the bug
We deploy a containerized application from GitHub Actions with aspire deploy.
Aspire provisions the Azure Container Registry, then the deployment pipeline
pushes the application image to it. The GitHub OIDC service principal therefore
needs AcrPush; the separate managed identity used by the Container App needs
only the AcrPull assignment that Aspire already provisions.
Because Aspire creates the registry during the first deployment, we cannot
grant the deployment principal access to it during an earlier identity-setup
step. We want the AppHost to create a registry-scoped AcrPush assignment
alongside the generated registry, keeping the deployment model code-first and
least-privileged.
The ACR documentation recommends giving an automated pipeline's service
principal or managed identity AcrPush, and ConfigureInfrastructure is the
documented extension point for customizations beyond the built-in APIs. We use
that extension point with KnownParameters.UserPrincipalId, whose API
documentation identifies it as the deployment principal.
aspire publish generates a valid root template for this model, but
aspire deploy provisions the ACR module without a value for
userPrincipalId. ARM rejects the module before creating the registry.
Without a supported way to express this assignment, the first deployment
requires an out-of-band or two-phase provisioning path for a resource that
Aspire otherwise owns.
Expected Behavior
aspire deploy can provision this registry-scoped assignment in the same pass
that creates the registry. If KnownParameters.UserPrincipalId is not intended
for this scenario, the deployment path should provide or document the supported
alternative.
Steps To Reproduce
Save this single-file AppHost as apphost.cs:
#:sdk Aspire.AppHost.Sdk@13.5.2
#:package Aspire.Hosting.Azure.AppContainers@13.5.2
using Aspire.Hosting.Azure;
using Azure.Provisioning;
using Azure.Provisioning.Authorization;
using Azure.Provisioning.ContainerRegistry;
using Azure.Provisioning.Expressions;
var builder = DistributedApplication.CreateBuilder(args);
var environment = builder.AddAzureContainerAppEnvironment("environment");
environment
.GetAzureContainerRegistry()
.ConfigureInfrastructure(infrastructure =>
{
var registry = infrastructure
.GetProvisionableResources()
.OfType<ContainerRegistryService>()
.Single();
var principalId = new ProvisioningParameter(
AzureBicepResource.KnownParameters.UserPrincipalId,
typeof(Guid));
infrastructure.Add(principalId);
var assignment = registry.CreateRoleAssignment(
ContainerRegistryBuiltInRole.AcrPush,
RoleManagementPrincipalType.ServicePrincipal,
principalId);
assignment.Name = BicepFunction.CreateGuid(
registry.Id,
principalId,
assignment.RoleDefinitionId);
infrastructure.Add(assignment);
});
builder.Build().Run();
After signing in with the Azure CLI, select a disposable resource group:
$env:Azure__SubscriptionId = az account show --query id --output tsv
$env:Azure__Location = "westus3"
$env:Azure__ResourceGroup = "aspire-user-principal-repro"
dnx Aspire.Cli@13.5.2 -y -- do provision-environment-acr `
--apphost .\apphost.cs `
--environment repro `
--non-interactive `
--pipeline-log-level debug `
--include-exception-details
aspire deploy includes provision-environment-acr; aspire do targets that
step and its dependencies without attempting unrelated resources.
For comparison, aspire publish succeeds:
dnx Aspire.Cli@13.5.2 -y -- publish `
--apphost .\apphost.cs `
--output-path .\output `
--environment repro `
--non-interactive
The generated main.bicep passes userPrincipalId: principalId, while
environment-acr.bicep declares userPrincipalId without a default.
Exceptions (if any)
Aspire.Hosting.Azure.ProvisioningFailedException:
Deployment failed: Error code = InvalidTemplate, Message = Deployment template
validation failed: 'The value for the template parameter 'userPrincipalId' at
line '19' and column '24' is not provided.'
Status: 400 (Bad Request)
ErrorCode: InvalidTemplate
Path: properties.template.parameters.userPrincipalId
at Aspire.Hosting.Azure.Provisioning.BicepProvisioner.GetOrCreateResourceAsync(...)
Aspire doctor output
Aspire Environment Check
========================
Aspire
Aspire CLI version 13.5.2
Developer Control Plane (DCP) connection health checks succeeded
AppHost
AppHost version 13.5.2 (apphost.cs)
.NET SDK
.NET 10.0.303 installed (x64)
Container Runtime
Docker v29.6.1: running
Environment
Operating system: Windows 10.0.26200.0
Summary: 6 passed, 3 unrelated development-certificate warnings, 0 failed
Anything else?
Verified workaround
We can resolve the GitHub OIDC service principal's Azure object ID before
starting Aspire and provide it explicitly through AppHost configuration:
var deploymentPrincipalId = builder.Configuration["DeploymentPrincipalId"];
if (!Guid.TryParseExact(deploymentPrincipalId, "D", out _))
{
throw new InvalidOperationException(
"DeploymentPrincipalId must contain the deployment principal's object ID.");
}
var principalId = new ProvisioningParameter(
AzureBicepResource.KnownParameters.UserPrincipalId,
typeof(string))
{
Value = new BicepValue<string>(deploymentPrincipalId),
};
infrastructure.Add(principalId);
The input must be the service principal's object ID, not its application/client
ID. The object ID is an identifier rather than a secret.
We tested this workaround against a disposable Azure resource group. Aspire
successfully provisioned one ACR and exactly one registry-scoped AcrPush
assignment whose principal ID and type matched the GitHub OIDC service
principal.
Using KnownParameters.PrincipalId without an explicit value does not work
around the problem. aspire deploy instead fails in
PopulateWellKnownParameters with:
An Azure principal parameter was not supplied a value. Ensure you are using an
environment that supports role assignments, for example
AddAzureContainerAppEnvironment.
Is explicitly supplying the service principal object ID the recommended way to
model this assignment, or is there an Aspire API intended to grant a role to the
deployment principal? If this customization is not intended, guidance on the
supported bootstrap flow would help.
Additional context
Is there an existing issue for this?
Describe the bug
We deploy a containerized application from GitHub Actions with
aspire deploy.Aspire provisions the Azure Container Registry, then the deployment pipeline
pushes the application image to it. The GitHub OIDC service principal therefore
needs
AcrPush; the separate managed identity used by the Container App needsonly the
AcrPullassignment that Aspire already provisions.Because Aspire creates the registry during the first deployment, we cannot
grant the deployment principal access to it during an earlier identity-setup
step. We want the AppHost to create a registry-scoped
AcrPushassignmentalongside the generated registry, keeping the deployment model code-first and
least-privileged.
The ACR documentation recommends giving an automated pipeline's service
principal or managed identity
AcrPush, andConfigureInfrastructureis thedocumented extension point for customizations beyond the built-in APIs. We use
that extension point with
KnownParameters.UserPrincipalId, whose APIdocumentation identifies it as the deployment principal.
aspire publishgenerates a valid root template for this model, butaspire deployprovisions the ACR module without a value foruserPrincipalId. ARM rejects the module before creating the registry.Without a supported way to express this assignment, the first deployment
requires an out-of-band or two-phase provisioning path for a resource that
Aspire otherwise owns.
Expected Behavior
aspire deploycan provision this registry-scoped assignment in the same passthat creates the registry. If
KnownParameters.UserPrincipalIdis not intendedfor this scenario, the deployment path should provide or document the supported
alternative.
Steps To Reproduce
Save this single-file AppHost as
apphost.cs:After signing in with the Azure CLI, select a disposable resource group:
aspire deployincludesprovision-environment-acr;aspire dotargets thatstep and its dependencies without attempting unrelated resources.
For comparison,
aspire publishsucceeds:The generated
main.biceppassesuserPrincipalId: principalId, whileenvironment-acr.bicepdeclaresuserPrincipalIdwithout a default.Exceptions (if any)
Aspire doctor output
Anything else?
Verified workaround
We can resolve the GitHub OIDC service principal's Azure object ID before
starting Aspire and provide it explicitly through AppHost configuration:
The input must be the service principal's object ID, not its application/client
ID. The object ID is an identifier rather than a secret.
We tested this workaround against a disposable Azure resource group. Aspire
successfully provisioned one ACR and exactly one registry-scoped
AcrPushassignment whose principal ID and type matched the GitHub OIDC service
principal.
Using
KnownParameters.PrincipalIdwithout an explicit value does not workaround the problem.
aspire deployinstead fails inPopulateWellKnownParameterswith:Is explicitly supplying the service principal object ID the recommended way to
model this assignment, or is there an Aspire API intended to grant a role to the
deployment principal? If this customization is not intended, guidance on the
supported bootstrap flow would help.
Additional context
service principal. The single-file reproduction produces the same ARM error
with a local Azure CLI user.
the original application's module after normalizing only the resource name.
AzurePublishingContexthandlesKnownParameters.UserPrincipalId, whileBicepProvisioner.PopulateWellKnownParametersdoes not:https://github.com/microsoft/aspire/blob/v13.5.2/src/Aspire.Hosting.Azure/AzurePublishingContext.cs
https://github.com/microsoft/aspire/blob/v13.5.2/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs
Deploy individual modules instead of main.bicep #11098.
resolution in the same method but does not handle
UserPrincipalId.