Salta ai contenuti
Docs Try Aspire
Docs Try

Deploy to Azure Kubernetes Service (AKS)

Questi contenuti non sono ancora disponibili nella tua lingua.

Deploy your Aspire application to Azure Kubernetes Service (AKS). Aspire provisions the AKS cluster, Azure Container Registry (ACR), and any Azure resources your app depends on — then deploys your application in a single command.

Start with Deploy to Kubernetes for the shared Kubernetes deployment model and target selection. For AKS hosting integration details, see AKS integration.

By default, local deployment uses Azure CLI credentials. Authenticate with Azure CLI before deploying:

Authenticate with Azure CLI
az login

Add the AKS hosting integration to your AppHost:

Aspire CLI — Add Azure Kubernetes
aspire add azure-kubernetes

The Aspire CLI adds the 📦 Aspire.Hosting.Azure.Kubernetes integration to your AppHost.

Then add the AKS environment in your AppHost:

AppHost.cs
var builder = DistributedApplication.CreateBuilder(args);
var aks = builder.AddAzureKubernetesEnvironment("aks");
var api = builder.AddProject<Projects.MyApi>("api");
builder.Build().Run();

When an AKS environment is present, all compute resources are automatically deployed to AKS — no additional opt-in is required.

When you run aspire deploy, Aspire provisions the following Azure resources:

  • AKS cluster — a managed Kubernetes cluster
  • Azure Container Registry (ACR) — to store container images for your application
  • Managed identity — for secure, credential-free access between AKS and ACR
  • Azure resources — any Azure resources referenced in your AppHost (databases, caches, messaging, etc.)

Aspire builds your container images, pushes them to ACR, generates Helm charts, and installs them to the AKS cluster.

Customize the system node pool VM size and scaling using WithSystemNodePool:

AppHost.cs
builder.AddAzureKubernetesEnvironment("aks")
.WithSystemNodePool("Standard_D4s_v5", minCount: 1, maxCount: 5);

Add additional node pools for workload isolation or specialized hardware, then use WithNodePool to schedule workloads on them:

AppHost.cs
var aks = builder.AddAzureKubernetesEnvironment("aks");
var gpuPool = aks.AddNodePool("gpupool", "Standard_NC6s_v3", minCount: 0, maxCount: 5);
builder.AddContainer("ml-worker", "my-ml-image")
.WithNodePool(gpuPool);

Deploy your application to AKS with a single command:

Deploy to AKS
aspire deploy

Aspire performs the following steps:

  1. Provisions Azure infrastructure — creates the AKS cluster, ACR, managed identity, and any Azure resources defined in your AppHost.

  2. Builds container images — builds Docker images for your project and container resources.

  3. Pushes images to ACR — tags and pushes the built images to the provisioned Azure Container Registry.

  4. Generates and installs Helm charts — creates Kubernetes manifests from your app model and installs them to the AKS cluster using Helm.

By default, the services Aspire deploys to AKS are reachable only from inside the cluster. To accept traffic from the public internet — for example a web frontend or a public API — you add an Application Gateway for Containers (AGC) load balancer and a Gateway API Gateway to your AppHost, then opt into automatic HTTPS with cert-manager. Everything in this section is provisioned by aspire deploy alongside the rest of your infrastructure; you don’t run any az aks, az network alb, or helm install commands by hand.

This walkthrough uses the integrated AGC + cert-manager APIs that ship with 📦 Aspire.Hosting.Azure.Kubernetes and require no additional package. If you have a pre-existing AKS cluster that wasn’t provisioned by Aspire, see the bring-your-own-cluster walkthroughs for Gateway API on AKS and Ingress on AKS instead.

When you opt in via AddLoadBalancer, Aspire flips the AKS cluster’s Bicep to enable the AKS-managed Gateway API and Application Load Balancer ingress profiles. After deploy, the cluster runs the Microsoft-managed AGC controller, which watches for Aspire’s generated Gateway and HTTPRoute resources and programs an Azure-hosted HTTPS frontend automatically.

The pieces fit together like this:

Aspire AppHost callWhat it produces in Azure / Kubernetes
AddAzureKubernetesEnvironmentAKS cluster (with Gateway API + ALB ingress profile when an LB is added) and ACR
AddAzureVirtualNetwork + AddSubnetA VNet and subnets — one for AKS nodes, one (or more) delegated to AGC
AddLoadBalancer(name, subnet)An AGC ApplicationLoadBalancer CR plus role assignments on the subnet
AddGateway(name).WithLoadBalancer(lb)A Gateway API Gateway resource attached to the AGC frontend
WithRoute(path, endpoint)An HTTPRoute that points the path at your service
AddCertManager(name).AddIssuer(...)cert-manager Helm release plus a ClusterIssuer
WithTls(issuer)An HTTPS listener and a cert-manager-issued certificate per gateway hostname

Application Gateway for Containers ingress on AKS uses preview Azure features that need to be registered once per subscription. Run these commands and wait for both feature registrations to report Registered:

Terminal window
az provider register --namespace Microsoft.ContainerService
az provider register --namespace Microsoft.ServiceNetworking
az feature register --namespace Microsoft.ContainerService \
--name ManagedGatewayAPIPreview
az feature register --namespace Microsoft.ContainerService \
--name ApplicationLoadBalancerPreview
# Wait until both features are Registered before deploying
az feature show --namespace Microsoft.ContainerService \
--name ApplicationLoadBalancerPreview --query properties.state -o tsv
az provider register --namespace Microsoft.ContainerService

AGC requires a dedicated subnet for its frontend that is delegated to Microsoft.ServiceNetworking/trafficControllers. The AKS node subnet and the AGC subnet must not overlap, and the AKS service CIDR defaults to 10.0.0.0/16 — pick a VNet range that avoids it.

AppHost.cs
#pragma warning disable ASPIREAZURE003 // AddSubnet is evaluation-only
var vnet = builder.AddAzureVirtualNetwork("vnet", "10.100.0.0/16");
var aksSubnet = vnet.AddSubnet("aks-nodes", "10.100.0.0/22");
var albSubnet = vnet.AddSubnet("alb-public", "10.100.4.0/24");
var aks = builder.AddAzureKubernetesEnvironment("aks")
.WithSubnet(aksSubnet);

Add an Application Gateway for Containers load balancer

Section titled “Add an Application Gateway for Containers load balancer”

AddLoadBalancer returns a typed handle that gateways and ingresses attach to. Behind the scenes it delegates the supplied subnet to AGC, grants the controller’s managed identity Network Contributor on the subnet, and applies the ApplicationLoadBalancer custom resource once the azure-alb-external GatewayClass is ready in the cluster.

AppHost.cs
var publicLb = aks.AddLoadBalancer("public", albSubnet);

Add a Gateway with a route to your service

Section titled “Add a Gateway with a route to your service”

AddGateway declares a Kubernetes Gateway API Gateway. WithLoadBalancer attaches it to the AGC ALB you just created and defaults the gatewayClassName to azure-alb-external. WithRoute adds an HTTPRoute that forwards a path to the named endpoint on one of your services.

AppHost.cs
var api = builder.AddProject<Projects.MyApi>("api")
.WithExternalHttpEndpoints();
aks.AddGateway("storefront")
.WithLoadBalancer(publicLb)
.WithRoute("/", api.GetEndpoint("http"));

Use the host overload of WithRoute when you need to send different hostnames to different backends — for example WithRoute("api.contoso.com", "/", api.GetEndpoint("http")).

Run aspire deploy. After it finishes, AGC assigns the gateway a frontend FQDN of the form <random>.fz<n>.alb.azure.com. Read it from the deployed Gateway and curl your service to confirm the load balancer and route are wired up before adding TLS:

Read the AGC FQDN and curl the gateway
kubectl get gateway storefront -o jsonpath='{.status.addresses[0].value}'
curl http://<the-fqdn-printed-above>/

AddCertManager installs the upstream cert-manager Helm chart with the CRDs and Gateway API watcher enabled and the --force-conflicts flag set so AKS’s Azure Policy add-on doesn’t break subsequent upgrades. AddIssuer adds a typed ClusterIssuer parented to that installation, and WithTls(issuer) on a gateway adds an HTTPS listener that cert-manager will populate with a real certificate.

AppHost.cs
var certManager = aks.AddCertManager("cert-manager");
var letsencrypt = certManager.AddIssuer("letsencrypt-prod")
.WithLetsEncryptProduction("ops@contoso.com")
.WithHttp01Solver();
aks.AddGateway("storefront")
.WithLoadBalancer(publicLb)
.WithRoute("/", api.GetEndpoint("http"))
.WithTls(letsencrypt);

WithTls(issuer) is the strongly-typed overload — it validates at AppHost-build time that the gateway and the issuer’s cert-manager installation belong to the same Kubernetes environment. There is also a no-argument WithTls() (auto-generated secret name, no issuer annotation) and a WithTls(secretName) overload for cases where you manage the certificate yourself.

Calling WithTls flips the gateway to an HTTPS-first posture:

  • A 301 Moved Permanently redirect is added to the HTTP listener so requests on port 80 are upgraded to https:// automatically. The cert-manager HTTP-01 solver registers an exact-match route for /.well-known/acme-challenge/<token> and wins over the redirect’s / prefix, so ACME continues to work.
  • A Strict-Transport-Security: max-age=31536000 response header is emitted on HTTPS responses so returning browsers refuse plain HTTP for a year.

To tune or disable these defaults, pass an options callback. The includeSubDomains and preload HSTS flags are off by default because they’re hard to reverse once browsers cache them — opt in explicitly when you’re ready to commit to the hostname:

AppHost.cs
aks.AddGateway("storefront")
.WithLoadBalancer(publicLb)
.WithRoute("/", api.GetEndpoint("http"))
.WithTls(letsencrypt, options =>
{
options.Hsts.IncludeSubDomains = true;
options.Hsts.Preload = true;
});

cert-manager validates domain ownership using the HTTP-01 challenge configured by WithHttp01Solver — it asks the gateway to serve a token at /.well-known/acme-challenge/<token> and verifies the response. There is no DNS API access required, so this works for the auto-assigned *.alb.azure.com FQDN out of the box.

To break the chicken-and-egg problem where AGC won’t program a Gateway whose HTTPS listener references a missing secret, Aspire pre-creates a self-signed bootstrap secret on the first deploy. Once AGC publishes the gateway’s FQDN, Aspire patches the listener with the real hostname and cert-manager swaps the bootstrap certificate for a Let’s Encrypt one. You may briefly see a self-signed certificate during the first deploy — this is expected.

After the deploy completes, watch the Certificate resource transition to Ready=True:

Inspect the cert-manager Certificate
kubectl get certificate
kubectl describe certificate storefront-tls

Confirm port 80 is now redirecting to HTTPS:

Verify the HTTP→HTTPS redirect
curl -i http://<the-fqdn>/
# HTTP/1.1 301 Moved Permanently
# Location: https://<the-fqdn>/

Then curl the gateway over HTTPS and confirm the certificate chain comes from Let’s Encrypt and the Strict-Transport-Security header is present:

Verify TLS and HSTS
curl -sIv https://<the-fqdn>/ 2>&1 | grep -iE "issuer:|strict-transport-security:"
# * issuer: C=US; O=Let's Encrypt; CN=R10
# strict-transport-security: max-age=31536000

The *.alb.azure.com FQDN is fine for testing but real applications need a hostname customers can remember. Adding a custom domain is a small change to the AppHost plus a DNS record at your registrar.

WithHostname adds the hostname to the gateway’s HTTPRoutes and to the HTTPS listener so cert-manager issues the certificate for the right name. Use the parameter overload when the hostname differs across deployment environments.

AppHost.cs
var hostname = builder.AddParameter("hostname"); // e.g. "api.contoso.com"
aks.AddGateway("storefront")
.WithLoadBalancer(publicLb)
.WithHostname(hostname)
.WithRoute("/", api.GetEndpoint("http"))
.WithTls(letsencrypt);

Provide the hostname value at deploy time:

Deploy with the hostname parameter
aspire deploy --parameter hostname=api.contoso.com

Read the AGC FQDN from the deployed gateway, then create a CNAME record at your DNS provider that points your custom hostname at it. The DNS provider doesn’t matter — Azure DNS, Cloudflare, Route 53, GoDaddy, anything that supports CNAME records will work.

Get the AGC FQDN to CNAME against
kubectl get gateway storefront -o jsonpath='{.status.addresses[0].value}'
# 8a3...cd.fz12.alb.azure.com
Example DNS record
api.contoso.com. CNAME 8a3...cd.fz12.alb.azure.com.

Re-running aspire deploy is idempotent. Once your DNS record propagates, cert-manager’s HTTP-01 challenge will succeed against the new hostname and a fresh certificate is issued automatically:

Verify the custom domain
curl -v https://api.contoso.com/ 2>&1 | grep -i "issuer:\|subject:"

To generate deployment artifacts without deploying, use aspire publish:

Publish AKS artifacts
aspire publish -o aks-artifacts

This generates Helm charts and Bicep infrastructure templates that you can review, customize, and deploy using your own CI/CD pipeline or GitOps workflow.

By default, local aspire deploy uses Azure CLI credentials. If you want a different credential source, set Azure:CredentialSource to one of the supported values: AzureCli, AzureDeveloperCli, VisualStudio, VisualStudioCode, AzurePowerShell, InteractiveBrowser, or Default.

For detailed Azure authentication configuration, see Deploy to Azure.

After authentication, aspire deploy needs a target subscription and location. These are configured via external parameters or environment variables:

  • Azure:SubscriptionId — the Azure subscription to deploy to
  • Azure:Location — the Azure region for resource provisioning (for example, eastus2)

Use PublishAsKubernetesService to customize the Kubernetes resources generated for individual services:

AppHost.cs
using Aspire.Hosting.Kubernetes.Resources;
builder.AddProject<Projects.MyApi>("api")
.PublishAsKubernetesService(resource =>
{
// Scale to 3 replicas
if (resource.Workload is Deployment deployment)
{
deployment.Spec.Replicas = 3;
}
});

Use AddHelmChart on the AKS environment to install pre-existing Helm charts — such as cert-manager or NGINX ingress — as post-deploy pipeline steps alongside your application.

See Install external Helm charts for a full walkthrough of adding external charts, configuring Helm values, overriding namespaces and release names, and choosing destroy-time uninstall behavior.

After aspire deploy provisions the AKS cluster, your local kubectl context is configured automatically. If you can’t reach the cluster, ensure your Azure CLI session is active and fetch credentials:

Get AKS credentials
az aks get-credentials --resource-group <resource-group> --name <cluster-name>

If pods fail with ImagePullBackOff, verify that the AKS cluster has the correct role assignment to pull from the provisioned ACR:

Check ACR pull access
az aks check-acr --resource-group <resource-group> --name <cluster-name> --acr <acr-name>.azurecr.io

If kubectl get certificate reports the certificate as not ready for more than a few minutes, describe it and the cert-manager Order/Challenge resources to see what failed:

Diagnose a stuck certificate
kubectl describe certificate storefront-tls
kubectl describe challenge -A

Common causes:

  • DNS hasn’t propagated yet when using a custom domain — Let’s Encrypt fetches http://<your-host>/.well-known/acme-challenge/... and that has to resolve to the AGC frontend.
  • HTTP traffic is blocked between Let’s Encrypt and the gateway — HTTP-01 needs port 80 open. AGC keeps the HTTP listener that AddGateway generates open by default; don’t remove it.
  • Production rate limit hit while iterating — switch the issuer to WithLetsEncryptStaging until everything else works, then flip back to production.
  • The contact email was rejected by ACME — see Let’s Encrypt rejects the contact email.
  • The bootstrap TLS secret is missing — see Gateway HTTPS listener reports InvalidCertificateRef.

The first time you aspire deploy with WithLetsEncryptProduction(email) or WithLetsEncryptStaging(email), cert-manager creates an ACME account using that email. Let’s Encrypt validates the address and refuses to register accounts whose email uses a reserved or forbidden domain. The ClusterIssuer will report Ready=False and kubectl describe clusterissuer <name> will show one of:

  • Domain name does not end with a valid public suffix — the TLD isn’t a registered public suffix. Reserved TLDs from RFC 2606 (.test, .example, .invalid, .localhost) are common offenders.
  • contact email has forbidden domain example.com — Let’s Encrypt explicitly blocks the IANA example domains (example.com, example.net, example.org).

Use a real address on a registered public domain (your corporate domain, or a personal mailbox like you@gmail.com). The address must be syntactically valid, but Let’s Encrypt does not send mail to it during provisioning — it’s the contact for expiry warnings.

Gateway HTTPS listener reports InvalidCertificateRef

Section titled “Gateway HTTPS listener reports InvalidCertificateRef”

If kubectl describe gateway storefront shows Secret default/storefront-tls does not exist on the HTTPS listener, AGC will refuse to program the HTTPS frontend and any in-flight cert-manager Challenge will stall because its solver HTTPRoute can’t be attached. This typically happens if you delete the storefront-tls secret manually to “force a reissue” — the bootstrap self-signed secret Aspire pre-creates is also wiped, breaking the chicken-and-egg pattern described in How the certificate gets issued.

Recover by recreating a placeholder secret so AGC can program the listener again — cert-manager will overwrite it with a real certificate within a minute:

Restore the bootstrap secret
TMPDIR=$(mktemp -d)
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
-keyout "$TMPDIR/key.pem" -out "$TMPDIR/cert.pem" \
-subj "/CN=bootstrap.invalid"
kubectl create secret tls storefront-tls \
--cert="$TMPDIR/cert.pem" --key="$TMPDIR/key.pem" -n default
rm -rf "$TMPDIR"

To force a legitimate reissue without removing the secret, annotate the Certificate instead:

Force cert-manager to reissue
kubectl cert-manager renew storefront-tls

If kubectl get gateway shows no address after aspire deploy succeeds, check that the AGC ingress profile actually came up on the cluster:

Check the AGC controller and GatewayClass
kubectl get gatewayclass azure-alb-external
kubectl get pods -n kube-system -l app=alb-controller

If the azure-alb-external GatewayClass doesn’t exist, the most common cause is that the ManagedGatewayAPIPreview and ApplicationLoadBalancerPreview features aren’t registered on the subscription — re-run the az feature register commands from Prerequisites for AGC, then aspire deploy again.

helm upgrade fails on cert-manager SSA conflicts

Section titled “helm upgrade fails on cert-manager SSA conflicts”

AddCertManager already passes --force-conflicts to helm upgrade to handle the field-manager conflict that AKS’s Azure Policy add-on creates against cert-manager’s ValidatingWebhookConfiguration. If you install cert-manager yourself with raw AddHelmChart instead, add .WithForceConflicts() to the chart resource, otherwise the second deploy will fail with a server-side-apply conflict on .webhooks[*].namespaceSelector.