microsoft / microsoft/aspire

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

Open
#19,800 3 comments 0 reactions 0 assignees View on GitHub
area-deployment triage:bot-seen
Dominant language
C#
Stars
6.3k
Forks
991
Avg merge
2d 12h
Merged PRs (30d)
201

Description

### Is there an existing issue for this?

- [x] 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`:

```csharp
#: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()
.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:

```powershell
$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:

```powershell
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)

```text
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

```text
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:

```csharp
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(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:

```text
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

- Azure CLI 2.84.0
- The original failure occurred under GitHub Actions on Ubuntu using an OIDC
service principal. The single-file reproduction produces the same ARM error
with a local Azure CLI user.
- The generated ACR module from the single-file reproduction is identical to
the original application's module after normalizing only the resource name.
- The implementation appears asymmetric: `AzurePublishingContext` handles
`KnownParameters.UserPrincipalId`, while
`BicepProvisioner.PopulateWellKnownParameters` does 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
- Individual-module deployment was introduced by
https://github.com/microsoft/aspire/pull/11098.
- https://github.com/microsoft/aspire/pull/18063 changes principal-type
resolution in the same method but does not handle `UserPrincipalId`.

Contributor guide

Open the contributing guide

Research direction

Start by comparing AzurePublishingContext.cs with Provisioning/Provisioners/BicepProvisioner.cs, especially PopulateWellKnownParameters, and review the individual-module deployment introduced by pull request 11098. Run the single-file apphost.cs reproduction with the documented aspire do provision-environment-acr command; done means aspire deploy provisions the ACR role assignment with userPrincipalId in the same pass without the ARM validation error.

Written by the indexing model from the issue text.

Assessment

Tech stack
azure, csharp
Domain
cloud, devops, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.