Lewati ke konten
Docs Try Aspire
Docs Try

Configure Gateway API on AKS

Konten ini belum tersedia dalam bahasa Anda.

This walkthrough shows how to use AddGateway to expose .NET Aspire services on Azure Kubernetes Service (AKS) with Application Gateway for Containers (AGC) and automatic TLS certificate provisioning via cert-manager HTTP-01 challenges.

Gateway API with HTTP-01 is the recommended approach for TLS on AKS because:

  • No DNS API access required — cert-manager validates domain ownership via HTTP, not DNS TXT records.
  • No Azure DNS zone or managed identity needed — significantly simpler infrastructure than DNS-01.
  • Works with any domain — including the auto-assigned AGC FQDN (*.alb.azure.com), your own custom domain, or even without a pre-existing DNS zone.

The following steps create the Azure infrastructure needed for Gateway API with HTTP-01 TLS. If you already have an AKS cluster with ALB and cert-manager configured, skip to Configure the AppHost.

Application Gateway for Containers requires several resource providers and preview features to be registered in your Azure subscription:

Terminal window
# Register required resource providers
az provider register --namespace Microsoft.ContainerService
az provider register --namespace Microsoft.Network
az provider register --namespace Microsoft.NetworkFunction
az provider register --namespace Microsoft.ServiceNetworking
# Register preview features for ALB controller and Gateway API
az feature register --namespace Microsoft.ContainerService \
--name ManagedGatewayAPIPreview
az feature register --namespace Microsoft.ContainerService \
--name ApplicationLoadBalancerPreview
# Wait for registration to complete (may take a few minutes)
az feature show --namespace Microsoft.ContainerService \
--name ApplicationLoadBalancerPreview --query properties.state -o tsv
# Re-register the provider after feature registration
az provider register --namespace Microsoft.ContainerService
# Install required CLI extensions
az extension add --name alb
az extension add --name aks-preview
Terminal window
# Variables — customize these for your environment
RESOURCE_GROUP=my-aspire-aks
LOCATION=westus3
CLUSTER_NAME=my-aspire-aks
ACR_NAME=myaspireacr
# Create resource group
az group create --name $RESOURCE_GROUP --location $LOCATION
# Create AKS cluster with ALB controller, OIDC issuer, and workload identity
az aks create \
--resource-group $RESOURCE_GROUP \
--name $CLUSTER_NAME \
--location $LOCATION \
--enable-oidc-issuer \
--enable-workload-identity \
--generate-ssh-keys
# Enable the ALB controller addon
az aks update \
--resource-group $RESOURCE_GROUP \
--name $CLUSTER_NAME \
--enable-alb
# Get cluster credentials
az aks get-credentials \
--resource-group $RESOURCE_GROUP \
--name $CLUSTER_NAME
Terminal window
# Create Azure Container Registry
az acr create \
--resource-group $RESOURCE_GROUP \
--name $ACR_NAME \
--sku Basic
# Attach ACR to AKS (grants AcrPull access)
az aks update \
--resource-group $RESOURCE_GROUP \
--name $CLUSTER_NAME \
--attach-acr $ACR_NAME

Create a dedicated subnet for AGC in the cluster’s VNet, then deploy the ApplicationLoadBalancer custom resource:

Terminal window
# Get the managed cluster resource group and VNet
MC_RG=$(az aks show --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME \
--query nodeResourceGroup -o tsv)
VNET_NAME=$(az network vnet list --resource-group $MC_RG --query "[0].name" -o tsv)
# Create a subnet for the ALB (delegated to ServiceNetworking)
az network vnet subnet create \
--resource-group $MC_RG \
--vnet-name $VNET_NAME \
--name subnet-alb \
--address-prefix 10.237.0.0/24 \
--delegations Microsoft.ServiceNetworking/trafficControllers
# Get the full subnet ID
SUBNET_ID=$(az network vnet subnet show \
--resource-group $MC_RG \
--vnet-name $VNET_NAME \
--name subnet-alb \
--query id -o tsv)
# Deploy the ApplicationLoadBalancer CRD
kubectl apply -f - <<EOF
apiVersion: alb.networking.azure.io/v1
kind: ApplicationLoadBalancer
metadata:
name: alb-aspire
namespace: default
spec:
associations:
- $SUBNET_ID
EOF
# Wait for it to become ready
kubectl get applicationloadbalancer alb-aspire --watch

Install cert-manager with Gateway API support enabled. Unlike DNS-01 validation, HTTP-01 does not require a managed identity, federated credentials, or workload identity configuration — cert-manager only needs to create a temporary HTTPRoute on the Gateway.

Terminal window
# Install cert-manager with Gateway API support
helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=true \
--set config.enableGatewayAPI=true \
--wait
# Verify cert-manager is running
kubectl get pods -n cert-manager

Create a ClusterIssuer that uses the gatewayHTTPRoute solver. This tells cert-manager to respond to ACME HTTP-01 challenges by creating a temporary HTTPRoute on the Gateway — the challenge is served from the same frontend and IP as your application.

Terminal window
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-http01
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: your-email@example.com
privateKeySecretRef:
name: letsencrypt-http01-account-key
solvers:
- http01:
gatewayHTTPRoute:
parentRefs:
- kind: Gateway
EOF
# Verify the issuer is ready
kubectl get clusterissuer letsencrypt-http01

Add a Kubernetes Gateway to your AppHost project to expose your frontend service through AGC. The gateway configuration includes the gateway class, ALB annotations, routing, hostname, and TLS settings.

var builder = DistributedApplication.CreateBuilder(args);
// Parameters for deployment configuration
var registryEndpoint = builder.AddParameter("registryEndpoint");
var gatewayClassName = builder.AddParameter("gatewayClassName");
var loadBalancerName = builder.AddParameter("loadBalancerName");
var loadBalancerNamespace = builder.AddParameter("loadBalancerNamespace");
var clusterIssuer = builder.AddParameter("clusterIssuer");
var externalFqdn = builder.AddParameter("externalFqdn");
// Kubernetes environment
var k8s = builder.AddKubernetesEnvironment("k8s");
// Container registry
var acr = builder.AddContainerRegistry("acr", registryEndpoint);
// Application services
var api = builder.AddProject<Projects.ApiService>("api");
var frontend = builder.AddProject<Projects.Frontend>("frontend")
.WithReference(api);
// Gateway with TLS
var gateway = k8s.AddGateway("ingress")
.WithGatewayClass(gatewayClassName)
.WithGatewayAnnotation(
"alb.networking.azure.io/alb-name", loadBalancerName)
.WithGatewayAnnotation(
"alb.networking.azure.io/alb-namespace", loadBalancerNamespace)
.WithRoute("/", frontend.GetEndpoint("http"))
.WithHostname(externalFqdn)
.WithTls();
builder.Build().Run();

The WithTls() call with no arguments auto-generates the TLS secret name (ingress-tls) and adds a cert-manager annotation to the Gateway referencing the ClusterIssuer specified via the clusterIssuer parameter.

Set environment variables to provide parameter values, then run aspire deploy:

Terminal window
export Parameters__registryEndpoint=myaspireacr.azurecr.io
export Parameters__gatewayClassName=azure-alb-external
export Parameters__loadBalancerName=alb-aspire
export Parameters__loadBalancerNamespace=default
export Parameters__clusterIssuer=letsencrypt-http01
export Parameters__externalFqdn=myapp.example.com
aspire deploy

After deployment, the Gateway is assigned a public FQDN by AGC. Create a DNS record pointing your hostname to this address.

  1. Get the Gateway address:

    Terminal window
    kubectl get gateway ingress -o jsonpath='{.status.addresses[0].value}'

    This returns the AGC frontend FQDN, for example: abc123.fz50.alb.azure.com.

  2. Create a CNAME record at your DNS provider pointing your hostname (e.g., myapp.example.com) to the AGC FQDN.

    If using Azure DNS:

    Terminal window
    AGC_FQDN=$(kubectl get gateway ingress \
    -o jsonpath='{.status.addresses[0].value}')
    az network dns record-set cname set-record \
    --resource-group $RESOURCE_GROUP \
    --zone-name example.com \
    --record-set-name myapp \
    --cname $AGC_FQDN
  3. Wait for cert-manager to complete the HTTP-01 challenge. cert-manager starts the challenge immediately on deployment and retries with exponential backoff until DNS resolves. This typically takes 5–10 minutes once the DNS record is active.

After deployment and DNS propagation, verify that your Gateway is configured correctly and TLS is working.

  1. Check the Gateway status:

    Terminal window
    kubectl get gateway ingress -o wide

    The ADDRESS column should show the public FQDN assigned by AGC. If the address is empty, see the Troubleshooting section.

  2. Check the certificate status:

    Terminal window
    kubectl get certificate
    kubectl describe certificate ingress-tls

    The certificate should show Ready: True once cert-manager has completed the HTTP-01 challenge. This typically takes 5–10 minutes after DNS is pointed to the Gateway.

  3. Test HTTPS access:

    Terminal window
    curl https://myapp.example.com

    You should receive a valid response with a trusted TLS certificate from Let’s Encrypt.

When you deploy a Gateway with WithHostname() and WithTls(), the following sequence occurs:

  1. Bootstrap TLS. On the first deploy, Aspire generates a self-signed placeholder certificate and creates the Kubernetes Secret referenced by the Gateway’s HTTPS listener. This makes the listener valid immediately so AGC can start programming the frontend.

  2. Gateway programming. AGC detects the Gateway resource and provisions a frontend with a public FQDN. The HTTPS listener is valid (using the bootstrap certificate) so AGC begins routing traffic.

  3. HTTP-01 challenge. cert-manager detects the Gateway’s cert-manager.io/issuer annotation and creates a Certificate resource. The HTTP-01 solver creates a temporary HTTPRoute on the same Gateway, responding to the ACME challenge at /.well-known/acme-challenge/<token>. Because the HTTPRoute shares the same frontend and IP, the ACME server’s validation request reaches the solver directly.

  4. Certificate issuance. Once the ACME server verifies the challenge response, Let’s Encrypt issues a trusted certificate. cert-manager stores it in the Kubernetes Secret, replacing the bootstrap certificate. AGC picks up the new certificate automatically.

  5. Automatic renewal. cert-manager renews the certificate before expiry (default: 30 days before). The HTTP-01 flow repeats transparently.

Why HTTP-01 works with Gateway API but not Ingress

Section titled “Why HTTP-01 works with Gateway API but not Ingress”

AGC creates a separate frontend (with its own FQDN and IP) for each Ingress resource. When cert-manager creates a solver Ingress for an HTTP-01 challenge, AGC assigns it a different frontend — the ACME server’s validation request goes to the wrong IP and fails.

With Gateway API, multiple HTTPRoute resources share a single Gateway and therefore a single frontend. When cert-manager creates a temporary solver HTTPRoute, it attaches to the same Gateway and is served from the same IP. The ACME validation request reaches the solver, and the challenge succeeds.

ApproachWorks with Ingress?Works with Gateway?Needs DNS API?
HTTP-01 (gatewayHTTPRoute)❌ (separate frontends per Ingress)No
DNS-01 (Azure DNS)Yes
Manual (acme.sh / openssl)Manual DNS TXT

If you already have an Azure DNS zone and prefer DNS-based validation, you can use DNS-01 instead of HTTP-01. This requires additional infrastructure: a managed identity with the DNS Zone Contributor role, federated credentials for workload identity, and patching the cert-manager deployment.

For the full DNS-01 setup steps, see the cert-manager section of Configure Ingress on AKS. The AppHost configuration is the same — only the ClusterIssuer and clusterIssuer parameter value differ.

Deploy without TLS (not recommended)

To deploy without TLS, omit WithTls() from your Gateway configuration:

var gateway = k8s.AddGateway("ingress")
.WithGatewayClass(gatewayClassName)
.WithGatewayAnnotation(
"alb.networking.azure.io/alb-name", loadBalancerName)
.WithGatewayAnnotation(
"alb.networking.azure.io/alb-namespace", loadBalancerNamespace)
.WithRoute("/", frontend.GetEndpoint("http"))
.WithHostname(externalFqdn);
Deploy without a hostname

To deploy without a hostname, omit WithHostname() and WithTls():

var gateway = k8s.AddGateway("ingress")
.WithGatewayClass(gatewayClassName)
.WithGatewayAnnotation(
"alb.networking.azure.io/alb-name", loadBalancerName)
.WithGatewayAnnotation(
"alb.networking.azure.io/alb-namespace", loadBalancerNamespace)
.WithRoute("/", frontend.GetEndpoint("http"));

After deployment, get the Gateway’s assigned address:

Terminal window
kubectl get gateway ingress -o jsonpath='{.status.addresses[0].value}'

If kubectl get gateway shows no address:

  1. Verify GatewayClassName: Ensure the gatewayClassName parameter matches a valid GatewayClass in your cluster:
    Terminal window
    kubectl get gatewayclass
  2. Check ALB annotations: The alb.networking.azure.io/alb-name and alb.networking.azure.io/alb-namespace annotations must reference an existing ApplicationLoadBalancer resource:
    Terminal window
    kubectl get applicationloadbalancer -A
  3. Check ALB controller logs: Look for errors in the ALB controller pod:
    Terminal window
    kubectl logs -n kube-system -l app=alb-controller --tail=50

cert-manager HTTP-01 challenge not completing

Section titled “cert-manager HTTP-01 challenge not completing”

If the certificate stays in a Pending state:

  1. Check the challenge status:

    Terminal window
    kubectl get challenge
    kubectl describe challenge
  2. Verify DNS resolves to the Gateway. The ACME server must be able to reach http://<your-hostname>/.well-known/acme-challenge/<token>. If DNS doesn’t point to the Gateway’s AGC address yet, the challenge will fail and retry with exponential backoff (starting at ~1 minute, up to ~32 minutes).

  3. Ensure the Gateway has a valid HTTPS listener. AGC won’t program the frontend if any listener references a TLS secret that doesn’t exist. Aspire creates a bootstrap self-signed certificate automatically on first deploy. If the secret was manually deleted, recreate it:

    Terminal window
    # Check if the secret exists
    kubectl get secret ingress-tls
    # If missing, redeploy with aspire deploy to recreate the bootstrap cert
  4. Verify cert-manager has Gateway API support enabled:

    Terminal window
    kubectl get deployment cert-manager -n cert-manager \
    -o jsonpath='{.spec.template.spec.containers[0].args}'

    Look for --enable-gateway-api-routes=true in the args. If missing, upgrade the Helm release with --set config.enableGatewayAPI=true.

Certificate order stuck in “errored” state

Section titled “Certificate order stuck in “errored” state”

If a cert-manager Order resource shows state: errored, delete the Certificate to trigger a fresh ACME flow:

Terminal window
# Check the order status
kubectl get order
kubectl describe order <order-name>
# Delete the certificate to trigger re-issuance
kubectl delete certificate ingress-tls
# Redeploy to recreate
aspire deploy