Skip to content

aspire deploy does not populate KnownParameters.UserPrincipalId for individual Bicep modules #19800

Description

Is there an existing issue for this?

  • I have searched the existing issues

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

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions