-
Notifications
You must be signed in to change notification settings - Fork 669
Hook up Azure Deployer to BicepProvisioner #10845
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
var builder = DistributedApplication.CreateBuilder(args); | ||
|
||
builder.AddAzureContainerAppEnvironment("env"); | ||
|
||
var storage = builder.AddAzureStorage("storage"); | ||
|
||
storage.AddBlobs("blobs"); | ||
storage.AddBlobContainer("mycontainer1", blobContainerName: "test-container-1"); | ||
storage.AddBlobContainer("mycontainer2", blobContainerName: "test-container-2"); | ||
storage.AddQueue("myqueue", queueName: "my-queue"); | ||
|
||
#if !SKIP_DASHBOARD_REFERENCE | ||
// This project is only added in playground projects to support development/debugging | ||
// of the dashboard. It is not required in end developer code. Comment out this code | ||
// or build with `/p:SkipDashboardReference=true`, to test end developer | ||
// dashboard launch experience, Refer to Directory.Build.props for the path to | ||
// the dashboard binary (defaults to the Aspire.Dashboard bin output in the | ||
// artifacts dir). | ||
builder.AddProject<Projects.Aspire_Dashboard>(KnownResourceNames.AspireDashboard); | ||
#endif | ||
|
||
builder.Build().Run(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>$(DefaultTargetFramework)</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
<IsAspireHost>true</IsAspireHost> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Compile Include="..\..\KnownResourceNames.cs" Link="KnownResourceNames.cs" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<AspireProjectOrPackageReference Include="Aspire.Hosting.Azure.AppContainers" /> | ||
<AspireProjectOrPackageReference Include="Aspire.Hosting.Azure" /> | ||
<AspireProjectOrPackageReference Include="Aspire.Hosting.Azure.Storage" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{ | ||
"$schema": "https://json.schemastore.org/launchsettings.json", | ||
"profiles": { | ||
"https": { | ||
"commandName": "Project", | ||
"dotnetRunMessages": true, | ||
"launchBrowser": true, | ||
"applicationUrl": "https://localhost:17222;http://localhost:15020", | ||
"environmentVariables": { | ||
"ASPNETCORE_ENVIRONMENT": "Development", | ||
"DOTNET_ENVIRONMENT": "Development", | ||
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21267", | ||
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22116" | ||
} | ||
}, | ||
"http": { | ||
"commandName": "Project", | ||
"dotnetRunMessages": true, | ||
"launchBrowser": true, | ||
"applicationUrl": "http://localhost:15020", | ||
"environmentVariables": { | ||
"ASPNETCORE_ENVIRONMENT": "Development", | ||
"DOTNET_ENVIRONMENT": "Development", | ||
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19093", | ||
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20094" | ||
} | ||
}, | ||
"deploy": { | ||
"commandName": "Project", | ||
"commandLineArgs": "--publisher default --deploy true --output-path deploy", | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"Logging": { | ||
"LogLevel": { | ||
"Default": "Information", | ||
"Microsoft.AspNetCore": "Warning" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"Logging": { | ||
"LogLevel": { | ||
"Default": "Information", | ||
"Microsoft.AspNetCore": "Warning", | ||
"Aspire.Hosting.Dcp": "Warning" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -4,17 +4,74 @@ | |||||||||||
#pragma warning disable ASPIREAZURE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. | ||||||||||||
#pragma warning disable ASPIREPUBLISHERS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. | ||||||||||||
|
||||||||||||
using Aspire.Hosting.Azure.Provisioning; | ||||||||||||
using Aspire.Hosting.Azure.Provisioning.Internal; | ||||||||||||
using Aspire.Hosting.Publishing; | ||||||||||||
|
||||||||||||
namespace Aspire.Hosting.Azure; | ||||||||||||
|
||||||||||||
internal sealed class AzureDeployingContext( | ||||||||||||
IProvisioningContextProvider provisioningContextProvider, | ||||||||||||
IUserSecretsManager userSecretsManager) | ||||||||||||
IUserSecretsManager userSecretsManager, | ||||||||||||
IBicepProvisioner bicepProvisioner, | ||||||||||||
IPublishingActivityReporter activityReporter) | ||||||||||||
{ | ||||||||||||
public async Task DeployModelAsync(CancellationToken cancellationToken = default) | ||||||||||||
public async Task DeployModelAsync(AzureEnvironmentResource resource, CancellationToken cancellationToken = default) | ||||||||||||
{ | ||||||||||||
var userSecrets = await userSecretsManager.LoadUserSecretsAsync(cancellationToken).ConfigureAwait(false); | ||||||||||||
await provisioningContextProvider.CreateProvisioningContextAsync(userSecrets, cancellationToken).ConfigureAwait(false); | ||||||||||||
var provisioningContext = await provisioningContextProvider.CreateProvisioningContextAsync(userSecrets, cancellationToken).ConfigureAwait(false); | ||||||||||||
|
||||||||||||
if (resource.PublishingContext is null) | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does this happen? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently -- never. I left this as an explicit check for when we do #10620. |
||||||||||||
{ | ||||||||||||
throw new InvalidOperationException($"Publishing context is not initialized. Please ensure that the {nameof(AzurePublishingContext)} has been initialized before deploying."); | ||||||||||||
} | ||||||||||||
|
||||||||||||
var deployingStep = await activityReporter.CreateStepAsync("Deploying to Azure", cancellationToken).ConfigureAwait(false); | ||||||||||||
await using (deployingStep.ConfigureAwait(false)) | ||||||||||||
{ | ||||||||||||
// Map parameters from the AzurePublishingContext | ||||||||||||
foreach (var (parameterResource, provisioningParameter) in resource.PublishingContext.ParameterLookup) | ||||||||||||
{ | ||||||||||||
if (parameterResource == resource.Location) | ||||||||||||
{ | ||||||||||||
resource.Parameters[provisioningParameter.BicepIdentifier] = provisioningContext.Location.Name; | ||||||||||||
} | ||||||||||||
else if (parameterResource == resource.ResourceGroupName) | ||||||||||||
{ | ||||||||||||
resource.Parameters[provisioningParameter.BicepIdentifier] = provisioningContext.ResourceGroup.Name; | ||||||||||||
} | ||||||||||||
else if (parameterResource == resource.PrincipalId) | ||||||||||||
{ | ||||||||||||
resource.Parameters[provisioningParameter.BicepIdentifier] = provisioningContext.Principal.Id.ToString(); | ||||||||||||
} | ||||||||||||
else | ||||||||||||
{ | ||||||||||||
// TODO: Prompt here. | ||||||||||||
await deployingStep.FailAsync("Deployment contains unresolvable parameters.", cancellationToken).ConfigureAwait(false); | ||||||||||||
Comment on lines
+49
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The TODO comment indicates incomplete functionality for handling unresolvable parameters. This could leave the deployment in an inconsistent state without proper parameter resolution.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
try | ||||||||||||
{ | ||||||||||||
var azureTask = await deployingStep.CreateTaskAsync("Provisioning Azure environment", cancellationToken).ConfigureAwait(false); | ||||||||||||
await using (azureTask.ConfigureAwait(false)) | ||||||||||||
{ | ||||||||||||
try | ||||||||||||
{ | ||||||||||||
await bicepProvisioner.GetOrCreateResourceAsync(resource, provisioningContext, cancellationToken).ConfigureAwait(false); | ||||||||||||
} | ||||||||||||
catch (Exception ex) | ||||||||||||
{ | ||||||||||||
await azureTask.FailAsync($"Provisioning failed: {ex.Message}", cancellationToken).ConfigureAwait(false); | ||||||||||||
throw; | ||||||||||||
} | ||||||||||||
} | ||||||||||||
} | ||||||||||||
catch (Exception ex) | ||||||||||||
{ | ||||||||||||
await deployingStep.FailAsync($"Deployment failed: {ex.Message}", cancellationToken).ConfigureAwait(false); | ||||||||||||
throw; | ||||||||||||
} | ||||||||||||
} | ||||||||||||
} | ||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,9 @@ | |
|
||
using System.Diagnostics.CodeAnalysis; | ||
using Aspire.Hosting.ApplicationModel; | ||
using Aspire.Hosting.Azure.Provisioning; | ||
using Aspire.Hosting.Azure.Provisioning.Internal; | ||
using Aspire.Hosting.Publishing; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Options; | ||
|
||
|
@@ -17,7 +19,7 @@ namespace Aspire.Hosting.Azure; | |
/// Emits a <c>main.bicep</c> that aggregates all provisionable resources. | ||
/// </summary> | ||
[Experimental("ASPIREAZURE001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")] | ||
public sealed class AzureEnvironmentResource : Resource | ||
public sealed class AzureEnvironmentResource : AzureBicepResource | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this represents main.bicep now? Should this be an AzureProvisoningResource instead? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yep.
AzureProvisioningResource encodes some concepts that I don't think make sense for main.bicep (role assignments, existing resources). Also, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not make it one? The underlying logic in AzureProvisioningContext creates an infrastructure object. I don’t think it need to be in this PR There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We need to be able to set outputs from the deployment onto the AzureEnvironmentResource so that we can propogate them to future steps (e.g. resolving the registry info for the ACR instance that we automatically provision) and we need a shape that matches the API of the AzureBicepProvisioner. If |
||
{ | ||
/// <summary> | ||
/// Gets or sets the Azure location that the resources will be deployed to. | ||
|
@@ -34,6 +36,8 @@ public sealed class AzureEnvironmentResource : Resource | |
/// </summary> | ||
public ParameterResource PrincipalId { get; set; } | ||
|
||
internal AzurePublishingContext? PublishingContext { get; set; } | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="AzureEnvironmentResource"/> class. | ||
/// </summary> | ||
|
@@ -43,10 +47,11 @@ public sealed class AzureEnvironmentResource : Resource | |
/// <param name="principalId">The Azure principal ID that will be used to deploy the resources.</param> | ||
/// <exception cref="ArgumentNullException">Thrown when the name is null or empty.</exception> | ||
/// <exception cref="ArgumentException">Thrown when the name is invalid.</exception> | ||
public AzureEnvironmentResource(string name, ParameterResource location, ParameterResource resourceGroupName, ParameterResource principalId) : base(name) | ||
public AzureEnvironmentResource(string name, ParameterResource location, ParameterResource resourceGroupName, ParameterResource principalId) : base(name, templateFile: "main.bicep") | ||
{ | ||
Annotations.Add(new PublishingCallbackAnnotation(PublishAsync)); | ||
Annotations.Add(new DeployingCallbackAnnotation(DeployAsync)); | ||
Annotations.Add(ManifestPublishingCallbackAnnotation.Ignore); | ||
|
||
Location = location; | ||
ResourceGroupName = resourceGroupName; | ||
|
@@ -57,24 +62,28 @@ private Task PublishAsync(PublishingContext context) | |
{ | ||
var azureProvisioningOptions = context.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(); | ||
|
||
var azureCtx = new AzurePublishingContext( | ||
PublishingContext = new AzurePublishingContext( | ||
context.OutputPath, | ||
azureProvisioningOptions.Value, | ||
context.Logger, | ||
context.ActivityReporter); | ||
|
||
return azureCtx.WriteModelAsync(context.Model, this); | ||
return PublishingContext.WriteModelAsync(context.Model, this); | ||
} | ||
|
||
private Task DeployAsync(DeployingContext context) | ||
{ | ||
var provisioningContextProvider = context.Services.GetRequiredService<IProvisioningContextProvider>(); | ||
var userSecretsManager = context.Services.GetRequiredService<IUserSecretsManager>(); | ||
var bicepProvisioner = context.Services.GetRequiredService<IBicepProvisioner>(); | ||
var activityPublisher = context.Services.GetRequiredService<IPublishingActivityReporter>(); | ||
|
||
var azureCtx = new AzureDeployingContext( | ||
provisioningContextProvider, | ||
userSecretsManager); | ||
userSecretsManager, | ||
bicepProvisioner, | ||
activityPublisher); | ||
|
||
return azureCtx.DeployModelAsync(context.CancellationToken); | ||
return azureCtx.DeployModelAsync(this, context.CancellationToken); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Azure; | ||
using Azure.ResourceManager; | ||
using Azure.ResourceManager.Resources; | ||
using Azure.ResourceManager.Resources.Models; | ||
|
||
namespace Aspire.Hosting.Azure.Provisioning.Internal; | ||
|
||
internal sealed class DefaultArmDeploymentCollection(ArmDeploymentCollection armDeploymentCollection) : IArmDeploymentCollection | ||
{ | ||
public Task<ArmOperation<ArmDeploymentResource>> CreateOrUpdateAsync( | ||
WaitUntil waitUntil, | ||
string deploymentName, | ||
ArmDeploymentContent content, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
return armDeploymentCollection.CreateOrUpdateAsync(waitUntil, deploymentName, content, cancellationToken); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
using System.Text.Json.Nodes; | ||
using System.Text.RegularExpressions; | ||
using Aspire.Hosting.Azure.Utils; | ||
using Aspire.Hosting.Publishing; | ||
using Azure; | ||
using Azure.Core; | ||
using Azure.ResourceManager.Resources; | ||
|
@@ -28,9 +29,11 @@ internal sealed partial class DefaultProvisioningContextProvider( | |
IArmClientProvider armClientProvider, | ||
IUserPrincipalProvider userPrincipalProvider, | ||
ITokenCredentialProvider tokenCredentialProvider, | ||
DistributedApplicationExecutionContext distributedApplicationExecutionContext) : IProvisioningContextProvider | ||
DistributedApplicationExecutionContext distributedApplicationExecutionContext, | ||
IOptions<PublishingOptions> publishingOptions) : IProvisioningContextProvider | ||
{ | ||
private readonly AzureProvisionerOptions _options = options.Value; | ||
private readonly PublishingOptions _publishingOptions = publishingOptions.Value; | ||
|
||
private readonly TaskCompletionSource _provisioningOptionsAvailable = new(TaskCreationOptions.RunContinuationsAsynchronously); | ||
|
||
|
@@ -259,6 +262,7 @@ public async Task<ProvisioningContext> CreateProvisioningContextAsync(JsonObject | |
} | ||
|
||
var principal = await userPrincipalProvider.GetUserPrincipalAsync(cancellationToken).ConfigureAwait(false); | ||
var outputPath = _publishingOptions.OutputPath is { } outputPathValue ? Path.GetFullPath(outputPathValue) : null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We past the output path to the ProvisioningContext during deployment so that we can reuse the bicep templates that were generated by the publish step during deployment. |
||
|
||
return new ProvisioningContext( | ||
credential, | ||
|
@@ -268,7 +272,9 @@ public async Task<ProvisioningContext> CreateProvisioningContextAsync(JsonObject | |
tenantResource, | ||
location, | ||
principal, | ||
userSecrets); | ||
userSecrets, | ||
distributedApplicationExecutionContext, | ||
outputPath); | ||
} | ||
|
||
private string GetDefaultResourceGroupName() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its kind of strange that we add
AddAzureContainerAppEnvironment(...)
when we aren't using anything other than Azure resources. I realize this is probably just the case for this playground but I wonder if we will see cases of people using apphosts to deploy things without any compute (containers/projects) because the resources they deploy have some built-in compute functionality that they are leveraging.In which case - does that API name make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could see a world where you only only use Aspire deploy to deploy Azure resources and not any actual compute resources. For example, I saw a scenario recently on a static front-end deployed on an Azure Storage instance with Azure Front Door for routing.
We've set up the APIs for AddAcaEnv such that you don't really "discover" you need them until you try to publish or deploy a project with compute resources (or role assignments).
We've really overloaded the term "environment" in our APIs and I'm not sure how we dig ourselves out of it. Although it won't really be exposed in public API to the user until the add compute.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Today we need a compute environment or we'll generate invalid bicep. If you provision an azure resource that depends on managed identity (which we do by default) but don't provide a compute environment, we'll generate invalid bicep (with a missing user assigned managed identity).