Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ await context.ReportingStep.CompleteAsync(
/// </summary>
internal bool EnableDashboard { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether HTTP endpoints should be preserved as HTTP instead of being upgraded to HTTPS.
/// Default is false (HTTP endpoints are upgraded to HTTPS).
/// </summary>
internal bool PreserveHttpEndpoints { get; set; }

/// <summary>
/// Gets the unique identifier of the Container App Environment.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,24 @@ public static IResourceBuilder<AzureContainerAppEnvironmentResource> WithDashboa
return builder;
}

/// <summary>
/// Configures whether HTTP endpoints should be upgraded to HTTPS in Azure Container Apps.
/// By default, HTTP endpoints are upgraded to HTTPS for security and WebSocket compatibility.
/// </summary>
/// <param name="builder">The AzureContainerAppEnvironmentResource to configure.</param>
/// <param name="upgrade">Whether to upgrade HTTP endpoints to HTTPS. Default is true.</param>
/// <returns><see cref="IResourceBuilder{T}"/></returns>
/// <remarks>
/// When disabled (<c>false</c>), HTTP endpoints will use HTTP scheme and port 80 in Azure Container Apps.
/// Note that explicit ports specified for development (e.g., port 8080) are still normalized
/// to standard ports (80/443) as required by Azure Container Apps.
/// </remarks>
public static IResourceBuilder<AzureContainerAppEnvironmentResource> WithHttpsUpgrade(this IResourceBuilder<AzureContainerAppEnvironmentResource> builder, bool upgrade = true)
{
builder.Resource.PreserveHttpEndpoints = !upgrade;
return builder;
}

/// <summary>
/// Configures the container app environment resource to use the specified Log Analytics Workspace.
/// </summary>
Expand Down
27 changes: 18 additions & 9 deletions src/Aspire.Hosting.Azure.AppContainers/ContainerAppContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,21 +226,30 @@ static bool Compatible(string[] schemes) =>
foreach (var resolved in httpIngress.ResolvedEndpoints)
{
var endpoint = resolved.Endpoint;
var preserveHttp = _containerAppEnvironmentContext.Environment.PreserveHttpEndpoints;

if (endpoint.UriScheme is "http" && endpoint.Port is not null and not 80)
// By default, HTTP ingress uses HTTPS in ACA (HTTP→HTTPS redirect breaks WebSocket upgrades)
// If PreserveHttpEndpoints is true, keep the original scheme
var scheme = preserveHttp ? endpoint.UriScheme : "https";
var port = scheme is "http" ? 80 : 443;

// Log when we're changing the scheme or port
if (!preserveHttp && endpoint.UriScheme is "http")
{
throw new NotSupportedException($"The endpoint '{endpoint.Name}' is an http endpoint and must use port 80");
_containerAppEnvironmentContext.Logger.LogInformation(
"Endpoint '{EndpointName}' on '{ResourceName}': upgrading to HTTPS (port 443) in Azure Container Apps. " +
"To opt out of this behavior, use .WithHttpsUpgrade(false) on the container app environment.",
endpoint.Name, Resource.Name);
}

if (endpoint.UriScheme is "https" && endpoint.Port is not null and not 443)
else if (endpoint.Port is not null && endpoint.Port != port)
{
throw new NotSupportedException($"The endpoint '{endpoint.Name}' is an https endpoint and must use port 443");
_containerAppEnvironmentContext.Logger.LogInformation(
"Endpoint '{EndpointName}' on '{ResourceName}' specifies port {DevPort} which is used for local development. " +
"In Azure Container Apps, {Scheme} endpoints use port {AcaPort}.",
endpoint.Name, Resource.Name, endpoint.Port, scheme.ToUpperInvariant(), port);
}

// For the http ingress port is always 80 or 443
var port = endpoint.UriScheme is "http" ? 80 : 443;

_endpointMapping[endpoint.Name] = new(endpoint.UriScheme, NormalizedContainerAppName, port, targetPort, true, httpIngress.External);
_endpointMapping[endpoint.Name] = new(scheme, NormalizedContainerAppName, port, targetPort, true, httpIngress.External);
}
}

Expand Down
71 changes: 63 additions & 8 deletions tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1182,41 +1182,96 @@ public async Task HttpAndTcpEndpointsCannotHaveTheSameTargetPort()
}

[Fact]
public async Task DefaultHttpIngressMustUsePort80()
public async Task DefaultHttpIngressUsesPort80EvenWithDifferentDevPort()
{
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);

builder.AddAzureContainerAppEnvironment("env");

// Dev port 8081 should be ignored in ACA, mapped to port 80
builder.AddContainer("api", "myimage")
.WithHttpEndpoint(port: 8081);
.WithHttpEndpoint(port: 8081, targetPort: 8080);

using var app = builder.Build();

await ExecuteBeforeStartHooksAsync(app, default);

var model = app.Services.GetRequiredService<DistributedApplicationModel>();

var ex = await Assert.ThrowsAsync<NotSupportedException>(() => ExecuteBeforeStartHooksAsync(app, default));
var container = Assert.Single(model.GetContainerResources());

container.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var target);

var resource = target?.DeploymentTarget as AzureProvisioningResource;

Assert.NotNull(resource);

Assert.Equal($"The endpoint 'http' is an http endpoint and must use port 80", ex.Message);
var (manifest, bicep) = await GetManifestWithBicep(resource);

await Verify(manifest.ToString(), "json")
.AppendContentAsFile(bicep, "bicep");
}

[Fact]
public async Task DefaultHttpsIngressMustUsePort443()
public async Task DefaultHttpsIngressUsesPort443EvenWithDifferentDevPort()
{
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);

builder.AddAzureContainerAppEnvironment("env");

// Dev port 8081 should be ignored in ACA, mapped to port 443
builder.AddContainer("api", "myimage")
.WithHttpsEndpoint(port: 8081);
.WithHttpsEndpoint(port: 8081, targetPort: 8443);

using var app = builder.Build();

await ExecuteBeforeStartHooksAsync(app, default);

var model = app.Services.GetRequiredService<DistributedApplicationModel>();

var ex = await Assert.ThrowsAsync<NotSupportedException>(() => ExecuteBeforeStartHooksAsync(app, default));
var container = Assert.Single(model.GetContainerResources());

container.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var target);

var resource = target?.DeploymentTarget as AzureProvisioningResource;

Assert.NotNull(resource);

var (manifest, bicep) = await GetManifestWithBicep(resource);

await Verify(manifest.ToString(), "json")
.AppendContentAsFile(bicep, "bicep");
}

[Fact]
public async Task CanPreserveHttpSchemeUsingWithHttpsUpgrade()
{
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);

builder.AddAzureContainerAppEnvironment("env")
.WithHttpsUpgrade(false); // Preserve HTTP scheme, don't upgrade to HTTPS

builder.AddContainer("api", "myimage")
.WithHttpEndpoint(port: 8080, targetPort: 80);

using var app = builder.Build();

await ExecuteBeforeStartHooksAsync(app, default);

Assert.Equal($"The endpoint 'https' is an https endpoint and must use port 443", ex.Message);
var model = app.Services.GetRequiredService<DistributedApplicationModel>();

var container = Assert.Single(model.GetContainerResources());

container.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var target);

var resource = target?.DeploymentTarget as AzureProvisioningResource;

Assert.NotNull(resource);

var (manifest, bicep) = await GetManifestWithBicep(resource);

await Verify(manifest.ToString(), "json")
.AppendContentAsFile(bicep, "bicep");
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location

param env_outputs_azure_container_apps_environment_default_domain string

param env_outputs_azure_container_apps_environment_id string

resource api 'Microsoft.App/containerApps@2025-01-01' = {
name: 'api'
location: location
properties: {
configuration: {
activeRevisionsMode: 'Single'
ingress: {
external: false
targetPort: 80
transport: 'http'
}
}
environmentId: env_outputs_azure_container_apps_environment_id
template: {
containers: [
{
image: 'myimage:latest'
name: 'api'
}
]
scale: {
minReplicas: 1
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "azure.bicep.v0",
"path": "api-containerapp.module.bicep",
"params": {
"env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}",
"env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location

param env_outputs_azure_container_apps_environment_default_domain string

param env_outputs_azure_container_apps_environment_id string

resource api 'Microsoft.App/containerApps@2025-01-01' = {
name: 'api'
location: location
properties: {
configuration: {
activeRevisionsMode: 'Single'
ingress: {
external: false
targetPort: 8080
transport: 'http'
}
}
environmentId: env_outputs_azure_container_apps_environment_id
template: {
containers: [
{
image: 'myimage:latest'
name: 'api'
}
]
scale: {
minReplicas: 1
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "azure.bicep.v0",
"path": "api-containerapp.module.bicep",
"params": {
"env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}",
"env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location

param env_outputs_azure_container_apps_environment_default_domain string

param env_outputs_azure_container_apps_environment_id string

resource api 'Microsoft.App/containerApps@2025-01-01' = {
name: 'api'
location: location
properties: {
configuration: {
activeRevisionsMode: 'Single'
ingress: {
external: false
targetPort: 8443
transport: 'http'
}
}
environmentId: env_outputs_azure_container_apps_environment_id
template: {
containers: [
{
image: 'myimage:latest'
name: 'api'
}
]
scale: {
minReplicas: 1
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "azure.bicep.v0",
"path": "api-containerapp.module.bicep",
"params": {
"env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}",
"env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}"
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@description('The location for the resource(s) to be deployed.')
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location

param env_outputs_azure_container_apps_environment_default_domain string
Expand Down Expand Up @@ -203,7 +203,7 @@ resource api 'Microsoft.App/containerApps@2025-02-02-preview' = {
}
{
name: 'HTTP_EP'
value: 'http://api.internal.${env_outputs_azure_container_apps_environment_default_domain}'
value: 'https://api.internal.${env_outputs_azure_container_apps_environment_default_domain}'
}
{
name: 'HTTPS_EP'
Expand All @@ -219,19 +219,19 @@ resource api 'Microsoft.App/containerApps@2025-02-02-preview' = {
}
{
name: 'PORT'
value: '80'
value: '443'
}
{
name: 'HOST'
value: 'api.internal.${env_outputs_azure_container_apps_environment_default_domain}'
}
{
name: 'HOSTANDPORT'
value: 'api.internal.${env_outputs_azure_container_apps_environment_default_domain}:80'
value: 'api.internal.${env_outputs_azure_container_apps_environment_default_domain}:443'
}
{
name: 'SCHEME'
value: 'http'
value: 'https'
}
{
name: 'INTERNAL_HOSTANDPORT'
Expand Down Expand Up @@ -260,4 +260,4 @@ resource api 'Microsoft.App/containerApps@2025-02-02-preview' = {
'${env_outputs_azure_container_registry_managed_identity_id}': { }
}
}
}
}
Loading
Loading