Azure Container Apps(Preview) enables users to run containerised applications in a completely Serverless manner providing complete isolation of Orchestration and Infrastructure. Applications built on Azure Container Apps can dynamically scale based on the various triggers as well as KEDA-supported scalers
Features of Azure Container Apps include:
Run multiple Revisions of containerised applications
Autoscale apps based on any KEDA-supported scale trigger
Enable HTTPS Ingress without having to manage other Azure infrastructure like L7 Load Balancers
Easily implement Blue/Green deployment and perform A/B Testing by splitting traffic across multiple versions of an application
Azure CLI extension or ARM templates to automate management of containerised applications
Manage Application Secrets securely
View Application Logs using Azure Log Analytics
Manage multiple Container Apps using Azure APIM providing rich APIM Policies and Authentication mechanisms to the Container Apps. This can be achieved in couple of ways:
Leverage Virtual Network Integration feature of Container Apps to securely manage through API Management in a virtual Network
Use Self-hosted Gateway feature of APIM to treat this as a Container App and manage other Container apps
This article would demonstrate:
How to Setup Azure Container Apps using Azure CLI
How to Deploy a containerised Logic App as Azure Container App
How to Deploy a containerised Azure Function as Azure Container App
Deploy the Self-hosted Gateway component of APIM as a Container App
Integrate the two Container Apps with APIM Container App
Test the flow end to end
tenantId="<tenantId>"
subscriptionId="<subscriptionId>"
resourceGroup="<resourceGroup>"
monitoringResourceGroup="<monitoringResourceGroup>"
location="<location>"
logWorkspace="<logWorkspace>"
basicEnvironment="basic-env"
securedEnvironment="secure-env"
acrName="<acrName>"
registryServer="<container_registry_server>"
registryUserName="<container_registry_username>"
registryPassword="<container_registry_password>"
# Function App would call this url to get the POST url end point of the http trigerred Logic App
logicAppCallback=""
# Logic App POST url returned from the previous call
logicAppPost=""
# VNET for Securing Container Apps
containerAppVnetName="containerapp-workshop-vnet"
containerAppVnetId=
containerVnetPrefix=""
# Subnet for Control plane of the Container Apps Infrastructure
controlPlaneSubnetName="containerapp-cp-subnet"
controlPlaneSubnetId=
controlPlaneSubnetPrefix=""
# Private DNS zone for Container Apps
containerAppLinkName="containerapp-dns-plink"
# Subnet for hosting Container Apps
appsSubnetName="containerapp-app-subnet"
appsSubnetId=
appsSubnetPrefix=""
# Both Control plane Subnet and Application Services Subnet should be in same VNET viz. $containerAppVnetName
apimVnetName="apim-workshop-vnet"
apimVnetId=
apimVnetPrefix=""
apimSubnetName="apim-workshop-subnet"
apimSubnetId=
apimSubnetPrefix=""
# Private DNS zone for APIM
apimLinkName="apim-dns-plink"
# VNET peering between Container App Vnet and APIM VNet (In case two subnets are not within same Vnet)
containerAppPeeringName="containerpp-apim-peering"
apimPeeringName="apim-containerpp-peering"
# Add CLI extension for Container Apps
az extension add \
--source https://workerappscliextension.blob.core.windows.net/azure-cli-extension/containerapp-0.2.2-py2.py3-none-any.whl
# Register the Microsoft.Web namespace
az provider register --namespace Microsoft.Web
az provider show --namespace Microsoft.Web
az monitor log-analytics workspace create --resource-group $monitoringResourceGroup --workspace-name $logWorkspace
# Retrieve Log Analytics ResourceId
logWorkspaceId=$(az monitor log-analytics workspace show --query customerId -g $monitoringResourceGroup -n $logWorkspace -o tsv)
# Retrieve Log Analytics Secrets
logWorkspaceSecret=$(az monitor log-analytics workspace get-shared-keys --query primarySharedKey -g $monitoringResourceGroup -n $logWorkspace -o tsv)
# Simple environment with no additional security for the underlying sInfrastructure
az containerapp env create --name $basicEnvironment --resource-group $resourceGroup \
--logs-workspace-id $logWorkspaceId --logs-workspace-key $logWorkspaceSecret --location $location
Setup a Secured Container App environment integrating it with a Virtual Network
Restrict communication to the Secured environment is from within the Virtual Network or a peer Virtual Network
Deploy a Logic App as a Container App into the Secured environment
Deploy a Function App as a Container App into the Secured environment
Deploy an APIM instance in a peered Virtual Network (either External or Internal)
Configure APIM to connect to the Container Apps securely
# Container App Vnet
az network vnet create --name $containerVnetName --resource-group $resourceGroup --address-prefixes $containerVnetPrefix
containerAppVnetId=$(az network vnet show --name $containerVnetName --resource-group $resourceGroup --query="id" -o tsv)
# ControlPlane Subnet
az network vnet subnet create --name $controlPlaneSubnetName --vnet-name $containerVnetName --resource-group $resourceGroup --address-prefixes $controlPlaneSubnetPrefix
controlPlaneSubnetId=$(az network vnet subnet show -n $controlPlaneSubnetName --vnet-name $containerVnetName --resource-group $resourceGroup --query="id" -o tsv)
# Apps Subnet
az network vnet subnet create --name $appsSubnetName --vnet-name $containerVnetName --resource-group $resourceGroup --address-prefixes $appsSubnetPrefix
appsSubnetId=$(az network vnet subnet show -n $appsSubnetName --vnet-name $containerVnetName --resource-group $resourceGroup --query="id" -o tsv)
# APIM Vnet
az network vnet create --name $apimVnetName --resource-group $resourceGroup --address-prefixes $apimVnetPrefix
apimVnetId=$(az network vnet show --name $apimVnetName --resource-group $resourceGroup --query="id" -o tsv)
# APIM Subnet
az network vnet subnet create --name $apimSubnetName --vnet-name $apimVnetName --resource-group $resourceGroup --address-prefixes $apimSubnetPrefix
apimSubnetId=$(az network vnet subnet show --name $apimSubnetName --vnet-name $apimVnetName --resource-group $resourceGroup --query="id" -o tsv)
# VNET peering between Container App Vnet and APIM VNet (In case two subnets are not within same Vnet)
az network vnet peering create --name $containerAppPeeringName --remote-vnet $apimVnetId \
--resource-group $resourceGroup --vnet-name $containerVnetName --allow-vnet-access
az network vnet peering create --name $apimPeeringName --remote-vnet $containerAppVnetId \
--resource-group $resourceGroup --vnet-name $apimVnetName --allow-vnet-access
Please follow this excellent article to get a detailed view on this
az containerapp env create --name $securedEnvironment --resource-group $resourceGroup \
--logs-workspace-id $logWorkspaceId --logs-workspace-key $logWorkspaceSecret --location $location \
--controlplane-subnet-resource-id $controlPlaneSubnetId \
--app-subnet-resource-id $appsSubnetId --internal-only
--internal-only flag ensures that this environment can communicate with services on same virtual network or on a peered virtual network
Excluding --internal-only flag makes this environment reachable from other container apps in the same environment
defaultDomain=$(az containerapp env show --name $securedEnvironment --resource-group $resourceGroup --query="defaultDomain" -o tsv)
staticIp=$(az containerapp env show --name $securedEnvironment --resource-group $resourceGroup --query="staticIp" -o tsv)
az network private-dns zone create --name $defaultDomain --resource-group $resourceGroup
#az network private-dns zone show --name $defaultDomain --resource-group $resourceGroup
az network private-dns link vnet create --name $containerAppLinkName --resource-group $resourceGroup \
--virtual-network $containerAppVnetName --zone-name $defaultDomain
#az network private-dns link vnet show --name $containerAppLinkName --resource-group $resourceGroup --zone-name $defaultDomain
az network private-dns link vnet create --name $apimLinkName --resource-group $resourceGroup \
--virtual-network $apimVnetName --zone-name $defaultDomain
#az network private-dns link vnet show --name $apimLinkName --resource-group $resourceGroup --zone-name $defaultDomain
Build a Logic App with basic request/response workflow - viz. LogicContainerApp
Run and test this Logic app as docker container locally
Deploy the Logic App container onto Azure as a Container App
Host the Logic App inside a Virtual Network (Secured Environment)
Expose the container app with Internal Ingress - blocking all public access
Let us first Create and Deploy a Logic app as Docker Container
Logic App runs an Azure Function locally and hence few tools/extensions need to be installed
Azure Function Core Tools - v3.x
The above link is for macOS; please install the appropriate links in the same page for other Operating Systems
At the time of writing, Core tools 3.x only supports the Logic App Designer within Visual Studio Code
The current example has been tested with - Function Core Tools version 3.0.3904 on a Windows box
A Storage Account on Azure - which is needed by any Azure function App
Logic App (aka Azure Function) would use this storage to cache its state
VS Code Extension for Standard Logic App
VS Code Extension for Azure Function
VS Code extension for Docker
This is Optional but recommended; it makes life easy while dealing with Dockerfile and Docker CLI commands
Create a Local folder to host all files related Logic App - viz. LogicContainerApp
Open the folder in VS Code
Create a New Logic App Project in this Folder
Choose Stateful workflow in the process and name accordingly - viz. httperesflow
This generates all necessary files and sub-folders within the current folder
A folder named httpresflow is also added which contains the workflow.json file
This describes the Logic App Actions/triggers
This example uses a Http Request/Response type Logic App for simplicity
The Logic App would accept a Post body as below and would return back the same as response
{
"Zip": "testzip-2011.zip"
}
Right click on the workflow.json file and Open the Logic App Designer - this might take few seconds to launch
Add Http Request trigger
Save the Designer changes
Right click on the empty area on the workspace folder structure and Open the Context menu
Select the menu options that says - Convert to Nuget-based Logic App project
This would generate .NET specific files - along with a LogicContainerApp.csproj file
Open the local.settings.json file
Replace the value of AzureWebJobsStorage variable with the value from Storage Account Connection string created earlier
Add a Dockerfile in the workspace
FROM mcr.microsoft.com/azure-functions/node:3.0
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
AzureFunctionsJobHost__Logging__Console__IsEnabled=true \
FUNCTIONS_V2_COMPATIBILITY_MODE=true \
AzureWebJobsStorage='' \
AZURE_FUNCTIONS_ENVIRONMENT=Development \
WEBSITE_HOSTNAME=localhost \
WEBSITE_SITE_NAME=logiccontainerapp
COPY ./bin/Debug/netcoreapp3.1 /home/site/wwwroot
WEBSITE_SITE_NAME - this is the name by which entries are created in Storage Account by the Logic App while caching its state
Build docker image
docker build -t <repo_name>/<image_name>:<tag> .
Create the Logic App Container
docker run --name logiccontainerapp -e AzureWebJobsStorage=$azureWebJobsStorage -d -p 8080:80 <repo_name>/<image_name>:<tag>
Let us now Run the logic app locally as a Docker container
Open the Storage account created earlier
Open the Containers
Open azure-webjobs-secrets blob
http://localhost:8080/runtime/webhooks/workflow/api/management/workflows/httpresflow/triggers/manual/listCallbackUrl?api-version=2020-05-01-preview&code=<master_key_value_from_storage_account>
{
"value": "https://localhost:443/api/httpresflow/triggers/manual/invoke?api-version=2020-05-01-preview&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=<value>",
"method": "POST",
"basePath": "https://localhost/api/httpresflow/triggers/manual/invoke",
"queries": {
"api-version": "2020-05-01-preview",
"sp": "/triggers/manual/run",
"sv": "1.0",
"sig": "<value>"
}
}
http://localhost:8080/api/httpresflow/triggers/manual/invoke?api-version=2020-05-01-preview&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=<value>
{
"Zip": "testzip-2011.zip"
}
{
"Zip": "testzip-2011.zip"
}
containerAppVnetId=$(az network vnet show -n $containerAppVnetName --resource-group $resourceGroup --query="id" -o tsv)
controlPlaneSubnetId=$(az network vnet subnet show -n $controlPlaneSubnetName --vnet-name $containerAppVnetName --resource-group $resourceGroup --query="id" -o tsv)
appsSubnetId=$(az network vnet subnet show -n $appsSubnetName --vnet-name $containerAppVnetName --resource-group $resourceGroup --query="id" -o tsv)
az containerapp env create --name $securedEnvironment --resource-group $resourceGroup \
--logs-workspace-id $logWorkspaceId --logs-workspace-key $logWorkspaceSecret --location $location \
--controlplane-subnet-resource-id $controlPlaneSubnetId \
--app-subnet-resource-id $appsSubnetId
Let us now deploy the logic app container onto Azure as Container App
Push Logic App container image to Azure Container Registry
# If Container image is already created and tested, use Docker CLI
docker push <repo_name>/<image_name>:<tag>
OR
# Use Azure CLI command for ACR to build and push
az acr build -t <repo_name>/<image_name>:<tag> -r $acrName .
Create Azure Container App with this image
logicappImageName="$registryServer/logiccontainerapp:v1.0.0"
azureWebJobsStorage="<storage_account_connection_string"
az containerapp create --name logicontainerapp --resource-group $resourceGroup \
--image $logicappImageName --environment $securedEnvironment \
--registry-login-server $registryServer --registry-username $registryUserName \
--registry-password $registryPassword \
--ingress internal --target-port 80 --transport http \
--secrets azurewebjobsstorage=$azureWebJobsStorage \
--environment-variables "AzureWebJobsStorage=secretref:azurewebjobsstorage"
Note down the Logic App ingress url
Build an Azure Function App with Http POST trigger - viz. HttpLogicContainerApp
Azure Function would call the above logic app (i.e. LogicContainerApp) sending some Json as POST body
Function would receive the http response from Logic App and return back to the caller
Run and test this function app as docker container locally
Deploy the Function App container onto Azure as a Container App
Host the Function App inside a Virtual Network (Secured Environment)
Expose the container app with Internal Ingress - blocking all public access
This function will be triggered by a http Post call
This is going to invoke Logic App internally
Return the response back to the caller
Before we Deploy the function app, let us look at its code
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace HttpContainerApps
{
public static class HttpContainerApps
{
[FunctionName("container")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req,
ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
var name = req.Query["name"];
var cl = new HttpClient();
var uri = $"http://httpcontainerapp-secured.internal.greensea-4ecd9ebc.eastus.azurecontainerapps.io/api/container?name={name}";
var res = await cl.GetAsync(uri);
var response = await res.Content.ReadAsStringAsync();
log.LogInformation($"Status:{res.StatusCode}");
log.LogInformation($"Response:{response}-v1.0.4");
response = $"Hello, {response}-v1.0.4";
// var response = $"Secured, {name}-v1.0.3";
return new OkObjectResult(response);
}
}
}
Deploy Azure Function app as Container App
httpImageName="$registryServer/httplogiccontainerapp:v1.0.5"
# Function App would call this url to get the POST url end point of the http trigerred Logic App
logicAppCallbackUrl="https://<logicontainerapp_internal_ingress_url>/runtime/webhooks/workflow/api/management/workflows/httpresflow/triggers/manual/listCallbackUrl?api-version=2020-05-01-preview&code=<master_key_value_from_storage_account>"
# Logic App POST url returned from the previous call
logicAppPostUrl="https://<logicontainerapp_internal_ingress_url>/api/httpresflow/triggers/manual/invoke?api-version=2020-05-01-preview&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig={0}"
az containerapp create --name httplogiccontainerapp --resource-group $resourceGroup \
--image $httpImageName --environment $securedEnvironment \
--registry-login-server $registryServer --registry-username $registryUserName \
--registry-password $registryPassword \
--ingress internal --target-port 80 --transport http \
--secrets azurewebjobsstorage=$azureWebJobsStorage,logicappcallbackurl=$logicAppCallbackUrl,logicappposturl=$logicAppPostUrl \
--environment-variables "AzureWebJobsStorage=secretref:azurewebjobsstorage,LOGICAPP_CALLBACK_URL=secretref:logicappcallbackurl,LOGICAPP_POST_URL=secretref:logicappposturl"
This Container App is with Ingress type Internal so this would be at exposed publicly
Integrate both the Container Apps (Function App and Logic App) with Azure APIM
Create an APIM instance on Azure
Deploy APIM in an Internal Vnet or External Vnet and follow instructions accordingly
Add two Container Apps (as deployed above) as backend for the APIM
Integrate both the Container Apps (Function App and Logic App) with Azure APIM
Create an APIM instance on Azure with a Self-hosted Gateway
Deploy Self-hosted APIM as Container App and in the same Secured Environment as above
Add two Container Apps (as deployed above) as backend for the APIM
Expose the APIM Container App with External Ingress thus making it the only public facing endpoint for the entire system
APIM Container App (Self-hosted Gateway) would be able to call the internal Container Apps since being part of the same Secured Environment
Define ARM template for APIM Container App
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"containerappName": {
"defaultValue": "apimcontainerapp",
"type": "String"
},
"location": {
"defaultValue": "eastus",
"type": "String"
},
"environmentName": {
"defaultValue": "secure-env",
"type": "String"
},
"serviceEndpoint": {
"defaultValue": "",
"type": "String"
},
"serviceAuth": {
"defaultValue": "",
"type": "String"
}
},
"variables": {},
"resources": [
{
"apiVersion": "2021-03-01",
"type": "Microsoft.Web/containerApps",
"name": "[parameters('containerappName')]",
"location": "[parameters('location')]",
"properties": {
"kubeEnvironmentId": "[resourceId('Microsoft.Web/kubeEnvironments', parameters('environmentName'))]",
"configuration": {
"ingress": {
"external": true,
"targetPort": 8080,
"allowInsecure": false,
"traffic": [
{
"latestRevision": true,
"weight": 100
}
]
}
},
"template": {
// "revisionSuffix": "revapim",
"containers": [
{
"name": "conainerapp-apim-gateway",
"image": "mcr.microsoft.com/azure-api-management/gateway:latest",
"env": [
{
"name": "config.service.endpoint",
"value": "[parameters('serviceEndpoint')]"
},
{
"name": "config.service.auth",
"value": "[parameters('serviceAuth')]"
}
],
"resources": {
"cpu": 0.5,
"memory": "1Gi"
}
}
],
"scale": {
"minReplicas": 1,
"maxReplicas": 3
}
}
}
}
]
}
Deploy APIM as Container App
apimappImageName="mcr.microsoft.com/azure-api-management/gateway:latest"
serviceEndpoint="<service_Endpoint>"
serviceAuth="<service_Auth>"
az deployment group create -f ./api-deploy.json -g $resourceGroup \
--parameters serviceEndpoint=$serviceEndpoint serviceAuth=$serviceAuth
Add Container Apps as APIM back end
The Web Service URL would be the Internal Ingress url of the Http Container App
Grab the FQDN of the APIM Container App from the portal
The FQDN can be obtained through Azure CLI as well
fqdn=$(az containerapp show -g $resourceGroup -n apimcontainerapp --query="configuration.ingress.fqdn")
Make a call to the API URL as below and receive the response back
curl -k -X POST --data '{"zip":"test.zip"}' https://$fqdn/container/api/logicapp/
....
{"zip":"test.zip"}
Container App - Virtual Network Integration
Azure APIM - Virtual Network and Internal Virtual Network
Azure APIM Self-hosted Gateway
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.