Skip to content

This sample shows how to communicate with an AKS-hosted workload via Private Link, Application Gateway, and AGIC.

License

Notifications You must be signed in to change notification settings

Azure-Samples/aks-agic-private-link

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

page_type languages products name description urlFragment azureDeploy
sample
azurecli
bicep
bash
csharp
yaml
json
azure
azure-application-gateway
azure-resource-manager
azure-kubernetes-service
azure-container-registry
azure-storage
azure-blob-storage
azure-storage-accounts
azure-bastion
azure-private-link
azure-virtual-network
azure-key-vault
azure-monitor
azure-log-analytics
azure-virtual-machines
How to call a workload in AKS via Private Link, Application Gateway, and Application Gateway Ingress Controller
This sample shows how to communicate with an AKS-hosted workload via Private Link, Application Gateway, and Application Gateway Ingress Controller.
aks-agic-private-link

How to call an AKS-hosted workload via Application Gateway Private Link and Application Gateway Ingress Controller

Azure Application Gateway can connect to a backend application via Azure Private Link Service (PLS). For more information, see Application Gateway Private Link.

Private Link for Application Gateway allows you to connect workloads over a private connection spanning across different virtual networks and Azure subscriptions. When configured, a private endpoint will be placed into a defined virtual network's subnet, providing a private IP address for client applications looking to communicate to a service behind an Application Gateway. For a list of other PaaS services that support Private Link functionality, see What is Azure Private Link?.

This article shows how to use Azure Application Gateway, Azure Web Application Firewall, and Azure Private Link Service (PLS) to securely expose and protect a workload running in Azure Kubernetes Service(AKS) via the Application Gateway Ingress Controller.

Prerequisites

Architecture

This sample provides a set of Bicep modules to deploy and configure an Azure Application Gateway with an WAF Policy as regional layer 7 load balancer in front of a public or a private AKS cluster with API Server VNET Integration, Azure CNI as a network plugin and Dynamic IP Allocation. The sample implements a scenario where a client application consumes a service exposed by a SaaS provider. The server application workload runs on an Azure Kubernetes Service(AKS) cluster and is exposed via the Application Gateway Ingress Controller. The frontend IP configuration of the Azure Application Gateway is configured to be exposed via Private Link. A frontend IP address is the IP address associated with an application gateway. You can configure an application gateway to have a public IP address, a private IP address, or both. An application gateway supports one public or one private IP address. Your virtual network and public IP address must be in the same location as your application gateway.

NOTE
At the time of this writing, Application Gateway Private Link configuration support for tunneling traffic through an Azure private endpoint to a private IP only Application Gateway is unsupported.

The following diagram shows the architecture and network topology deployed by the sample:

AKS Architecture

A Deployment Script is used to create a sample httpbin web application via YAML manifests. An ingress is created to expose the Kubernetes service via the Azure Application Gateway via the Application Gateway Ingress Controller.

Bicep modules are parametric, so you can choose any network plugin:

NOTE
The sample was tested only with Azure CNI with dynamic IP allocation. Azure CNI Overlay does not currently support the Application Gateway Ingress Controller. For more information, see Limitations with Azure CNI Overlay.

The Bicep modules also allow installing the following extensions and add-ons for Azure Kubernetes Service(AKS):

In addition, this sample shows how to deploy an Azure Kubernetes Service cluster with the following features:

In a production environment, we strongly recommend deploying a private AKS cluster with Uptime SLA. For more information, see private AKS cluster with a Public DNS address. Alternatively, you can deploy a public AKS cluster and secure access to the API server using authorized IP address ranges.

The Bicep modules deploy the following Azure resources for the service provider:

The Bicep modules deploy the following Azure resources for the service consumer:

NOTE
You can find the architecture.vsdx file used for the diagram under the visio folder.

What is Bicep?

Bicep is a domain-specific language (DSL) that uses a declarative syntax to deploy Azure resources. It provides concise syntax, reliable type safety, and support for code reuse. Bicep offers the best authoring experience for your infrastructure-as-code solutions in Azure.

Deploy the Bicep modules

You can deploy the Bicep modules in the bicep folder using the deploy.sh Bash script in the same folder. Specify a value for the following parameters in the deploy.sh script and main.parameters.json parameters file before deploying the Bicep modules.

  • prefix: specifies a prefix for all the Azure resources.
  • authenticationType: specifies the type of authentication when accessing the Virtual Machine. sshPublicKey is the recommended value. Allowed values: sshPublicKey and password.
  • vmAdminUsername: specifies the name of the administrator account of the virtual machine.
  • vmAdminPasswordOrKey: specifies the SSH Key or password for the virtual machine.
  • aksClusterSshPublicKey: specifies the SSH Key or password for AKS cluster agent nodes.
  • aadProfileAdminGroupObjectIDs: when deploying an AKS cluster with Microsoft Entra ID and Azure RBAC integration, this array parameter contains the list of Microsoft Entra ID group object IDs that will have the admin role of the cluster.
  • keyVaultObjectIds: Specifies the object ID of the service principals to configure in Key Vault access policies.

We suggest reading sensitive configuration data such as passwords or SSH keys from a pre-existing Azure Key Vault resource. For more information, see Use Azure Key Vault to pass secure parameter value during Bicep deployment.

Application Gateway Bicep module

The following table contains the Bicep code used to deploy the Azure Application Gateway and its WAF Policy. Please note that the module configures the Application Gateway Private Link only if the value of the privateLinkEnabled parameter is true. If the Application Gateway is configured only with a public frontend IP configuration, the private link will use this configuration, otherwise it will use the private frontend IP configuration.

// Parameters
@description('Specifies the name of the Application Gateway.')
param name string

@description('Specifies the sku of the Application Gateway.')
param skuName string = 'WAF_v2'

@description('Specifies the frontend IP configuration type.')
@allowed([
  'Public'
  'Private'
  'Both'
])
param frontendIpConfigurationType string

@description('Specifies the name of the public IP adddress used by the Application Gateway.')
param publicIpAddressName string = '${name}PublicIp'

@description('Specifies the location of the Application Gateway.')
param location string

@description('Specifies the resource tags.')
param tags object

@description('Specifies the resource id of the subnet used by the Application Gateway.')
param subnetId string

@description('Specifies the resource id of the subnet used by the Application Gateway Private Link.')
param privateLinkSubnetId string

@description('Specifies the private IP address of the Application Gateway.')
param privateIpAddress string

@description('Specifies the availability zones of the Application Gateway.')
param availabilityZones array

@description('Specifies the workspace id of the Log Analytics used to monitor the Application Gateway.')
param workspaceId string

@description('Specifies the lower bound on number of Application Gateway capacity.')
param minCapacity int = 1

@description('Specifies the upper bound on number of Application Gateway capacity.')
param maxCapacity int = 10

@description('Specifies whether create or not a Private Link for the Application Gateway.')
param privateLinkEnabled bool = false

@description('Specifies the name of the WAF policy')
param wafPolicyName string = '${name}WafPolicy'

@description('Specifies the mode of the WAF policy.')
@allowed([
  'Detection'
  'Prevention'
])
param wafPolicyMode string = 'Prevention'

@description('Specifies the state of the WAF policy.')
@allowed([
  'Enabled'
  'Disabled '
])
param wafPolicyState string = 'Enabled'

@description('Specifies the maximum file upload size in Mb for the WAF policy.')
param wafPolicyFileUploadLimitInMb int = 100

@description('Specifies the maximum request body size in Kb for the WAF policy.')
param wafPolicyMaxRequestBodySizeInKb int = 128

@description('Specifies the whether to allow WAF to check request Body.')
param wafPolicyRequestBodyCheck bool = true

@description('Specifies the rule set type.')
param wafPolicyRuleSetType string = 'OWASP'

@description('Specifies the rule set version.')
param wafPolicyRuleSetVersion string = '3.2'

@description('Specifies the name of the Key Vault resource.')
param keyVaultName string

// Variables
var diagnosticSettingsName = 'diagnosticSettings'
var applicationGatewayResourceId = resourceId('Microsoft.Network/applicationGateways', name)
var keyVaultSecretsUserRoleDefinitionId = resourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')
var gatewayIPConfigurationName = 'DefaultGatewayIpConfiguration'
var frontendPortName = 'DefaultFrontendPort'
var backendAddressPoolName = 'DefaultBackendPool'
var backendHttpSettingsName = 'DefaultBackendHttpSettings'
var httpListenerName = 'DefaultHttpListener'
var routingRuleName = 'DefaultRequestRoutingRule'
var privateLinkName = 'DefaultPrivateLink'
var publicFrontendIPConfigurationName = 'PublicFrontendIPConfiguration'
var privateFrontendIPConfigurationName = 'PrivateFrontendIPConfiguration'
var frontendIPConfigurationName = frontendIpConfigurationType == 'Public' ? publicFrontendIPConfigurationName : privateFrontendIPConfigurationName
var applicationGatewayZones = !empty(availabilityZones) ? availabilityZones : []

var publicFrontendIPConfiguration = {
  name: publicFrontendIPConfigurationName
  properties: {
    privateIPAllocationMethod: 'Dynamic'
    publicIPAddress: {
      id: applicationGatewayPublicIpAddress.id
    }
    privateLinkConfiguration: privateLinkEnabled && frontendIpConfigurationType == 'Public' ? {
      id: '${applicationGatewayResourceId}/privateLinkConfigurations/${privateLinkName}'
    } : null
  }
}

var privateFrontendIPConfiguration = {
  name: privateFrontendIPConfigurationName
  properties: {
    privateIPAllocationMethod: 'Static'
    privateIPAddress: privateIpAddress
    subnet: {
      id: subnetId
    }
    privateLinkConfiguration: privateLinkEnabled && frontendIpConfigurationType != 'Public'? {
      id: '${applicationGatewayResourceId}/privateLinkConfigurations/${privateLinkName}'
    } : null
  }
}

var frontendIPConfigurations = union(
  frontendIpConfigurationType == 'Public' ? array(publicFrontendIPConfiguration) : [],
  frontendIpConfigurationType == 'Private' ? array(privateFrontendIPConfiguration) : [],
  frontendIpConfigurationType == 'Both' ? concat(array(publicFrontendIPConfiguration), array(privateFrontendIPConfiguration)) : []
)

var sku = union({
    name: skuName
    tier: skuName
  }, maxCapacity == 0 ? {
    capacity: minCapacity
  } : {})

var applicationGatewayProperties = union({
    sku: sku
    gatewayIPConfigurations: [
      {
        name: gatewayIPConfigurationName
        properties: {
          subnet: {
            id: subnetId
          }
        }
      }
    ]
    frontendIPConfigurations: frontendIPConfigurations
    frontendPorts: [
      {
        name: frontendPortName
        properties: {
          port: 80
        }
      }
    ]
    backendAddressPools: [
      {
        name: backendAddressPoolName
      }
    ]
    backendHttpSettingsCollection: [
      {
        name: backendHttpSettingsName
        properties: {
          port: 80
          protocol: 'Http'
          cookieBasedAffinity: 'Disabled'
          requestTimeout: 30
          pickHostNameFromBackendAddress: true
        }
      }
    ]
    httpListeners: [
      {
        name: httpListenerName
        properties: {
          frontendIPConfiguration: {
            id: '${applicationGatewayResourceId}/frontendIPConfigurations/${frontendIPConfigurationName}'
          }
          frontendPort: {
            id: '${applicationGatewayResourceId}/frontendPorts/${frontendPortName}'
          }
          protocol: 'Http'
        }
      }
    ]
    requestRoutingRules: [
      {
        name: routingRuleName
        properties: {
          ruleType: 'Basic'
          priority: 1000
          httpListener: {
            id: '${applicationGatewayResourceId}/httpListeners/${httpListenerName}'
          }
          backendAddressPool: {
            id: '${applicationGatewayResourceId}/backendAddressPools/${backendAddressPoolName}'
          }
          backendHttpSettings: {
            id: '${applicationGatewayResourceId}/backendHttpSettingsCollection/${backendHttpSettingsName}'
          }
        }
      }
    ]
    privateLinkConfigurations: privateLinkEnabled ? [
      {
        name: privateLinkName
        properties: {
          ipConfigurations: [
            {
              name: 'PrivateLinkDefaultIPConfiguration'
              properties: {
                privateIPAllocationMethod: 'Dynamic'
                subnet: {
                  id: privateLinkSubnetId
                }
              }
            }
          ]
        }
      }
    ] : []
    firewallPolicy: {
      id: wafPolicy.id
    }
  }, maxCapacity > 0 ? {
    autoscaleConfiguration: {
      minCapacity: minCapacity
      maxCapacity: maxCapacity
    }
  } : {})

var applicationGatewayLogCategories = [
  'ApplicationGatewayAccessLog'
  'ApplicationGatewayFirewallLog'
  'ApplicationGatewayPerformanceLog'
]
var applicationGatewayMetricCategories = [
  'AllMetrics'
]
var applicationGatewayLogs = [for category in applicationGatewayLogCategories: {
  category: category
  enabled: true
}]
var applicationGatewayMetrics = [for category in applicationGatewayMetricCategories: {
  category: category
  enabled: true
}]

// Resources
resource applicationGatewayIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
  name: '${name}Identity'
  location: location
}

resource applicationGatewayPublicIpAddress 'Microsoft.Network/publicIPAddresses@2022-07-01' = if (frontendIpConfigurationType != 'Private') {
  name: publicIpAddressName
  location: location
  zones: applicationGatewayZones
  sku: {
    name: 'Standard'
  }
  properties: {
    publicIPAllocationMethod: 'Static'
  }
}

resource applicationGateway 'Microsoft.Network/applicationGateways@2022-07-01' = {
  name: name
  location: location
  tags: tags
  zones: applicationGatewayZones
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${applicationGatewayIdentity.id}': {}
    }
  }
  properties: applicationGatewayProperties
}

resource wafPolicy 'Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies@2022-07-01' = {
  name: wafPolicyName
  location: location
  tags: tags
  properties: {
    customRules: [
      {
        name: 'BlockMe'
        priority: 1
        ruleType: 'MatchRule'
        action: 'Block'
        matchConditions: [
          {
            matchVariables: [
              {
                variableName: 'QueryString'
              }
            ]
            operator: 'Contains'
            negationConditon: false
            matchValues: [
              'blockme'
            ]
          }
        ]
      }
      {
        name: 'BlockEvilBot'
        priority: 2
        ruleType: 'MatchRule'
        action: 'Block'
        matchConditions: [
          {
            matchVariables: [
              {
                variableName: 'RequestHeaders'
                selector: 'User-Agent'
              }
            ]
            operator: 'Contains'
            negationConditon: false
            matchValues: [
              'evilbot'
            ]
            transforms: [
              'Lowercase'
            ]
          }
        ]
      }
    ]
    policySettings: {
      requestBodyCheck: wafPolicyRequestBodyCheck
      maxRequestBodySizeInKb: wafPolicyMaxRequestBodySizeInKb
      fileUploadLimitInMb: wafPolicyFileUploadLimitInMb
      mode: wafPolicyMode
      state: wafPolicyState
    }
    managedRules: {
      managedRuleSets: [
        {
          ruleSetType: wafPolicyRuleSetType
          ruleSetVersion: wafPolicyRuleSetVersion
        }
      ]
    }
  }
}

resource keyVault 'Microsoft.KeyVault/vaults@2021-10-01' existing = {
  name: keyVaultName
}

resource keyVaultSecretsUserApplicationGatewayIdentityRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  scope: keyVault
  name: guid(keyVault.id, applicationGatewayIdentity.name, 'keyVaultSecretsUser')
  properties: {
    roleDefinitionId: keyVaultSecretsUserRoleDefinitionId
    principalType: 'ServicePrincipal'
    principalId: applicationGatewayIdentity.properties.principalId
  }
}

resource applicationGatewayDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
  name: diagnosticSettingsName
  scope: applicationGateway
  properties: {
    workspaceId: workspaceId
    logs: applicationGatewayLogs
    metrics: applicationGatewayMetrics
  }
}

// Outputs
output id string = applicationGateway.id
output name string = applicationGateway.name
output privateLinkFrontendIPConfigurationName string = privateLinkEnabled ? frontendIPConfigurationName : ''

Deployment Script

The sample makes use of a Deployment Script to run the install-helm-charts-and-agic-sample.sh Bash script which installs the httpbin web application via YAML templates and the following packages to the AKS cluster via Helm. For more information on deployment scripts, see Use deployment scripts in Bicep. The script also installs the cert-Manager via Helm and a cluster issues for the Application Gateway Ingress Controller.

# Install kubectl
az aks install-cli --only-show-errors

# Get AKS credentials
az aks get-credentials \
  --admin \
  --name $clusterName \
  --resource-group $resourceGroupName \
  --subscription $subscriptionId \
  --only-show-errors

# Check if the cluster is private or not
private=$(az aks show --name $clusterName \
  --resource-group $resourceGroupName \
  --subscription $subscriptionId \
  --query apiServerAccessProfile.enablePrivateCluster \
  --output tsv)

# Install Helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 -o get_helm.sh -s
chmod 700 get_helm.sh
./get_helm.sh &>/dev/null

# Add Helm repos
helm repo add jetstack https://charts.jetstack.io

# Update Helm repos
helm repo update

if [[ $private == 'true' ]]; then
  # Log whether the cluster is public or private
  echo "$clusterName AKS cluster is public"

  # Install certificate manager
  command="helm install cert-manager jetstack/cert-manager \
    --create-namespace \
    --namespace cert-manager \
    --set installCRDs=true \
    --set nodeSelector.\"kubernetes\.io/os\"=linux"

  az aks command invoke \
    --name $clusterName \
    --resource-group $resourceGroupName \
    --subscription $subscriptionId \
    --command "$command"
  
    # Create cluster issuer for the Application Gateway Ingress Controller (AGIC)
  command="cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-application-gateway
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: $email
    privateKeySecretRef:
      name: letsencrypt
    solvers:
    - http01:
        ingress:
          class: azure/application-gateway
          podTemplate:
            spec:
              nodeSelector:
                "kubernetes.io/os": linux
EOF"

  az aks command invoke \
    --name $clusterName \
    --resource-group $resourceGroupName \
    --subscription $subscriptionId \
    --command "$command"

  # Create a namespace for the application
  command="kubectl create namespace $namespace"

  az aks command invoke \
    --name $clusterName \
    --resource-group $resourceGroupName \
    --subscription $subscriptionId \
    --command "$command"

  # Create a deployment and service for the application
  command="cat <<EOF | kubectl apply -n $namespace -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin
spec:
  replicas: 3
  selector:
    matchLabels:
      app: httpbin
  template:
    metadata:
      labels:
        app: httpbin
    spec:
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: httpbin
      - maxSkew: 1
        topologyKey: kubernetes.io/hostname
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: httpbin
      nodeSelector:
        "kubernetes.io/os": linux
      containers:
      - image: docker.io/kennethreitz/httpbin
        imagePullPolicy: IfNotPresent
        name: httpbin
        resources:
          requests:
            memory: "64Mi"
            cpu: "125m"
          limits:
            memory: "128Mi"
            cpu: "250m"
        ports:
        - containerPort: 80
        env:
        - name: PORT
          value: "80"
---
apiVersion: v1
kind: Service
metadata:
  name: httpbin
spec:
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
  type: ClusterIP
  selector:
    app: httpbin
EOF"

  az aks command invoke \
    --name $clusterName \
    --resource-group $resourceGroupName \
    --subscription $subscriptionId \
    --command "$command"

  # Create an ingress resource for the application
  command="cat <<EOF | kubectl apply -n $namespace -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: httpbin
spec:
  ingressClassName: azure/application-gateway
  rules:
  - host: $hostName
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: httpbin
            port:
              number: 80
EOF"

  az aks command invoke \
    --name $clusterName \
    --resource-group $resourceGroupName \
    --subscription $subscriptionId \
    --command "$command"

else
  # Log whether the cluster is public or private
  echo "$clusterName AKS cluster is public"

  # Install certificate manager
  helm install cert-manager jetstack/cert-manager \
    --create-namespace \
    --namespace cert-manager \
    --set installCRDs=true \
    --set nodeSelector."kubernetes\.io/os"=linux

  # Create cluster issuer for the Application Gateway Ingress Controller (AGIC)
  cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-application-gateway
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: $email
    privateKeySecretRef:
      name: letsencrypt
    solvers:
    - http01:
        ingress:
          class: azure/application-gateway
          podTemplate:
            spec:
              nodeSelector:
                "kubernetes.io/os": linux
EOF

  # Create a namespace for the application
  kubectl create namespace $namespace

  # Create a deployment and service for the application
  cat <<EOF | kubectl apply -n $namespace -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin
spec:
  replicas: 3
  selector:
    matchLabels:
      app: httpbin
  template:
    metadata:
      labels:
        app: httpbin
    spec:
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: httpbin
      - maxSkew: 1
        topologyKey: kubernetes.io/hostname
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: httpbin
      nodeSelector:
        "kubernetes.io/os": linux
      containers:
      - image: docker.io/kennethreitz/httpbin
        imagePullPolicy: IfNotPresent
        name: httpbin
        resources:
          requests:
            memory: "64Mi"
            cpu: "125m"
          limits:
            memory: "128Mi"
            cpu: "250m"
        ports:
        - containerPort: 80
        env:
        - name: PORT
          value: "80"
---
apiVersion: v1
kind: Service
metadata:
  name: httpbin
spec:
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
  type: ClusterIP
  selector:
    app: httpbin
EOF

  # Create an ingress resource for the application
  cat <<EOF | kubectl apply -n $namespace -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: httpbin
spec:
  ingressClassName: azure-application-gateway
  rules:
  - host: $hostName
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: httpbin
            port:
              number: 80
EOF

fi

# Create output as JSON file
echo '{}' |
  jq --arg x 'prometheus' '.prometheus=$x' |
  jq --arg x 'cert-manager' '.certManager=$x' |
  jq --arg x 'ingress-basic' '.nginxIngressController=$x' >$AZ_SCRIPTS_OUTPUT_PATH

The httpbin web application is deployed via YAML templates. In particular, an ingress object is used to expose the application using the Application Gateway Ingress Controller via the HTTP protocol. The default ingress hostname is httpbin.contoso.internal, but you can control the hostname using the following parameters in the main.bicep module:

@description('Specifies the subdomain of the Kubernetes ingress object.')
param subdomain string = 'httpbin'

@description('Specifies the domain of the Kubernetes ingress object.')
param domain string = 'contoso.internal'

The ingress object can be easily modified to expose the server via HTTPS and provide a certificate for TLS termination. You can use the cert-manager installed by the script to issue a Let's Encrypt certificate. For more information, see Use certificates with LetsEncrypt.org on Application Gateway for AKS clusters. In particular, cert-manager can create and then delete DNS-01 records in Azure DNS but it needs to authenticate to Azure first. The suggested authentication method is Managed Identity Using AAD Workload Identity.

Test the application

If the deployment succeeds, you should be able to access the AKS-hosted httpbin web application from the client virtual machine as follows:

  • Navigate to Azure Portal and connect to the client virtual machine via Azure Bastion.
  • Run the the nslookup httpbin.contoso.internal command. If you customized the subdomain and domain used by the ingress object and Private DNS Zone, make sure to replace httpbin.contoso.internal with subdomain.domain. The command should return the private IP address of the ApplicationGatewayPrivateEndpoint used by the client virtual machine to invoke the httpbin web application as shown in the following figure.

nslookup

  • Call any of the REST API methods exposed by httpbin web application, for example /headers. If the call succeeds, you should see a result like the one in the following figure.

nslookup

Review deployed resources

Use the Azure portal, Azure CLI, or Azure PowerShell to list the deployed resources in the resource group.

Azure CLI

az resource list --resource-group <resource-group-name>

PowerShell

Get-AzResource -ResourceGroupName <resource-group-name>

Clean up resources

When you no longer need the resources you created, just delete the resource group. This will remove all the Azure resources.

Azure CLI

az group delete --name <resource-group-name>

PowerShell

Remove-AzResourceGroup -Name <resource-group-name>

Next Steps

You could change the default hostname used by the ingress object and expose the backend service via HTTPS using a TLS/SSL certificate for your domain. For more information, see Use certificates with LetsEncrypt.org on Application Gateway for AKS clusters. If you use Azure DNS to manage your domain, you could extend the Bicep modules to automatically create a custom domain for your Front Door and create a CNAME DNS record in your public DNS zone.