Joining an Azure Container App to a vNet with Infrastructure as Code

Azure Container Apps are a new way to host containers in Azure. Announced last year, this preview service offers a serverless Kubernetes solution for running containers. If you want to know more about Container Apps, take a look at my article on “WTH are Azure Container Apps”. Recently Microsoft announced the ability to deploy container apps into a virtual network, which is a significant feature for anyone who wants to lock down their resources using Private Endpoints, or wants to run an internal-only service. The documentation provides details on how to set this up with the Portal or Azure CLI. Still, when I came to joining a vNet using an Infrastructure as Code deployment, I was left scratching my head, as it didn’t seem to be documented anywhere. Even the official ARM/Bicep specifications docs had no mention of it at the time (this has been fixed in the last couple of days). So I thought it was worth documenting how to do it.

We’ll look at how to do this in both Bicep and Pulumi. Unfortunately, Terraform does not yet have support for Container Apps. If you’re interested in using this feature in Terraform, follow this GitHub Issue.

API Version

The first thing we need to do is make sure we use the correct API version, and this is where I initially had problems, as the new version had not been documented. The vNet join is part of the App Service Environment rather than the individual apps, so all apps in an environment are on the same network. The resource type for this is Microsoft.Web/kubeEnvironments and you need to be using API version 2021-03-01

Note that container apps are moving namespace. In March 2022, they will shift from the Microsoft.Web namespace to Microsoft.Apps. Everything in this article should still work, but you will need to change the namespace.

vNet Requirements

To join the app service to a vNet, you need two subnets, one for the control plane resources and one for the application resources. Both subnets need to be at least /21 in size, but that is all the information the documentation currently provides. It is not clear if these subnets have to be dedicated to these resources. There is no delegation applied that I can see. The docs also do not provide information on NSG requirements for these subnets, and currently, state you should have an “allow all” approach for outbound traffic. Hopefully, we will get a more detailed requirement soon to lock these NSG’s down.

Creating Resource

Now that we have the correct subnets in place, we can create the Container App Environment and join it to the vNet. This is pretty simple, you just need to use the right API version and add the appSubnetResourceId and controlPlaneSubnetResourceId to the containerAppsConfiguration section. There are some other network settings you can apply there, but for most scenarios leaving them off and getting the defaults will work. The full spec for the containerAppsConfiguration section is:

containerAppsConfiguration: {
    appSubnetResourceId: 'string'
    controlPlaneSubnetResourceId: 'string'
    daprAIInstrumentationKey: 'string'
    dockerBridgeCidr: 'string'
    platformReservedCidr: 'string'
    platformReservedDnsIP: 'string'
}

If we apply that to our infrastructure as code, it will look like this:

Bicep

resource ContainerAppEnv 'Microsoft.Web/kubeEnvironments@2021-03-01' = {
  name: 'AcaEnvironment1'
  location: 'westeurope'
  kind: 'containerenvironment'
  properties: {
    environmentType: 'managed'
    internalLoadBalancerEnabled: false
    appLogsConfiguration: {
      destination: 'log-analytics'
      logAnalyticsConfiguration: {
        customerId: <log analytics customer id>
        sharedKey: <log analytics shared key>
      }
    }
    containerAppsConfiguration: {
      appSubnetResourceId: <resource Id of app subnet>
      controlPlaneSubnetResourceId: <resource Id of control plane subnet>
    }
 
  }
}

Pulumi (C#)

Make sure you set the correct API version either in your using statements or as part of the resource type.

using Pulumi.AzureNative.Web.V20210301;
using Pulumi.AzureNative.Web.V20210301.Inputs;

var conterEnv = new KubeEnvironment("containerenv", new KubeEnvironmentArgs()
        {
            EnvironmentType = "managed",
            ResourceGroupName = resourceGroup.Name,
            Location= "westeurope",
            InternalLoadBalancerEnabled = false,
            AppLogsConfiguration = new AppLogsConfigurationArgs()
            {
                Destination= "log-anlytics",
                LogAnalyticsConfiguration = new LogAnalyticsConfigurationArgs()
                {

                    CustomerId = "<Log analytics customer id>",
                    SharedKey = "<Log analytics shared key>"

                },
            },
            ContainerAppsConfiguration = new ContainerAppsConfigurationArgs()
            {
                ControlPlaneSubnetResourceId = "<control plan subnet Id>",
                AppSubnetResourceId = "<app subnet Id>",
            }
        });