Azure Container Instances (ACI) is a relatively new service in Azure. It allows the easiest possible deployment of containers to the cloud. The idea behind it is called “Serverless Containers”. The meaning is that your container(s) will be deployed to a random server you have no control of and no need to manage (which is great).

This concept is suited for short-lived containers since the billing is more expensive (per second) than a VM. One possible use case for ACI is to create additional microservices in burst times. Alternatively, you might need a container that does a certain job and then dies. Much like Azure Functions, when a function just isn’t enough.

Since I recently did a big project with ACI, I’ll share with you everything I learned about creating containers dynamically in code.

Getting Started

To get started with docker containers, you should have a prepared container image in a container registry.

With ACI, you will be creating Container Groups. A Container Group consists of one or more containers deployed on a single machine, that can securely communicate with each other. To the outside world, the container group can expose a single public IP and a single port. So one of the containers can act as a server.

There are several ways to create a Container Group. Whatever way you choose, you’ll have to specify:

Information on the container registry, including username and password if it’s private

Information on the container image within the container registry

Os Type – ACI supports both Windows and Linux containers

Number of CPU cores – affects billing

Memory (in GB) – affects billing

There are a bunch of other parameters as well.

We can create container groups in these ways:

With Azure Resource Manager (ARM) template. In Azure Portal With Azure Cloud Shell or Azure CLI. For example:

1 az container create -- name my - container - group -- image [ image - address - in - container - registry ] -- resource - group [ Azure - resource - group ] -- ip - address public -- port 3000 -- os - type Windows -- memory 3.5 -- registry - login - server [ container - registry - login - server ] -- registry - username [ username ] -- registry - password [ password ] In code with REST API.

Create containers programmatically with REST API

First of all, let’s talk a bit about Azure security. Azure doesn’t allow just anyone to be able to create containers on your Azure subscription. You need to be authorized to create Azure resources for that. Azure uses OAuth2.0 authorization with “Bearer” access tokens. This means that each HTTP request should contain an Authorization header with a valid Access Token.

Once you have obtained a valid access token, you can use Azure Container Instances Rest API to create containers.

Let’s see exactly how to do those things.

Source Code Available! I created a helper library with the code I used for ACI. It’s available on GitHub and has methods for everything you’re about to read.

Get an Access Token

There are 2 ways to get an Access Token:

If your service is an Azure App Service or Azure Function, the simplest way is to use Azure Managed Service Identity. For App Service or anything else, you can use an Application Registration to Azure Active Directory. Then, use the application’s Id and Secret to get an access token.

Using Azure Managed Service Identity (MSI)

There’s a good documentation on using MSI, which I won’t repeat here, except to give you a general idea on what should be done to start getting access tokens.

In your App Service, you’ll need to turn on managed-service-identity. This can be done in the portal, in Azure CLI or with ARM Template. Once turned on, you’ll have to give your App Service permissions to the Resource Group where you are going to create containers (see below).

When permissions are given, you can get a valid access token in code by:

Add a reference to the Microsoft.Azure.Services.AppAuthentication NuGet package Add the following code:

1 2 3 4 5 using Microsoft . Azure . Services . AppAuthentication ; using Microsoft . Azure . KeyVault ; // ... var azureServiceTokenProvider = new AzureServiceTokenProvider ( ) ; string accessToken = await azureServiceTokenProvider . GetAccessTokenAsync ( "https://management.azure.com/" ) ;

This will return the access token string and will take care to cache and reuse it internally.

If you’re not using .NET, or want to do it manually with REST, you can.

Using Active Directory Application Registration

There’s great documentation on application registration, which I also won’t repeat here, except to give you the general idea.

You’ll need to follow these steps:

Create an Application Registration in Azure active directory. This can be done in the portal. Save the application ID (AKA ‘clientId’). Add to the application a Key and save its Value (AKA ‘clientSecret’). Give the application permissions to create containers (see below). Use the application’s ID and Key to get OAuth access tokens. you’ll need to send an HTTP POST request in the following format:

1 2 3 4 5 POST / contoso . com / oauth2 / token HTTP / 1.1 Host : login . microsoftonline . com Content - Type : application / x - www - form - urlencoded grant_type = client_credentials & client_id = [ client id ] & client_secret = [ client secret ] & resource = https : //management.azure.com/

1 2 3 4 5 6 7 { "access_token" : "eyJhbGciOiJS..." , "token_type" : "Bearer" , "expires_in" : "3599" , "expires_on" : "1388452167" , "resource" : "https://service.contoso.com/" }

There’s code to get these access token on my GitHub repository, including logic to cache and refresh the token when expired. Feel free to use the library or just copy the relevant code.

Give Permissions to modify the Resource Group

The result will be a JSON document with the access token and expiration data. For example:

Whether you used Managed Service Identity or Application Registration, you need to give the proper permissions to create containers in the Resource Group.

Here’s the documentation on giving permissions to an application in Azure portal. The general idea is:

We’ll need to add permissions to the Resource Group where the containers are to be created (I used Owner, but Contributor might also work). In the portal, go to Resource Groups -> [The relevant resource group] -> Access Control (IAM). In the IAM, add either the registered application or the App Service (if used MSI) and assign the desired role (like Owner).

If the container registry is in a different resource group, you might need to add permissions for that as well.

If you want to add permissions in ARM template, it’s possible to do for Managed Identity, but not for a registered application – That can be done only in Azure portal or a PowerShell script after deployment. Please inform me if it changes.

Create, query and delete Container Groups in code

Now that all the authorization preparations are done, we can create containers in code. We will actually be creating Container Groups, with a single container in each. The name of the container group will be the same as the container’s name.

For all operations, we will use this link:

1 https : //management.azure.com/subscriptions/[subscriptionId]/resourceGroups/[resourceGroup]/providers/Microsoft.ContainerInstance/containerGroups/[containerGroupName]?api-version=2018-04-01

With parameters Subscription ID, Resource Group and Container Group Name.

For reasons unknown to me, the container name can’t be in Pascal case, or you’ll get a BadRequest response. Kebab case worked for me – e.g ‘my-neat-container’

Remember to include Authorization header with the Access Token we received before. The key should be “Authorization” and the value “Bearer [access-token]”.

Now there are 3 main operations: Create a container group, Delete container group, Query container group, Which are done by PUT, DELETE and GET operations respectively.

The full API can be viewed here.

Example C# code to Create a Container Group

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public async void CreateContainer ( string accessToken ) { var body = @ "{ " "id" ": " "/subscriptions/mySubscription/resourceGroups/myResourceGroup/providers/Microsoft.ContainerInstance/containerGroups/container-name" ", " "name" ": " "container-name" ", " "properties" ": { " "containers" ": [ { " "name" ": " "container-name" ", " "properties" ": { " "image" ": " "containerRegistry/imageName" ", " "ports" ": [ { " "port" ": 12345 } ], " "resources" ": { " "requests" ": { " "cpu" ": 2, " "memoryInGB" ": 8 } }, } } ], " "ipAddress" ": { " "ports" ": [ { " "protocol" ": " "TCP" ", " "port" ": 12345 } ], " "type" ": " "Public" ", }, }, " "type" ": " "Microsoft.ContainerInstance/containerGroups" " }" ; HttpContent content = new StringContent ( body , Encoding . UTF8 , "application/json" ) ; using ( var client = new HttpClient ( ) ) { client . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Bearer" , accessToken ) ; var url = "https://management.azure.com/mysubscriptions/mySubscription/resourceGroups/myRresourceGroup/providers/Microsoft.ContainerInstance/containerGroups/container-name?api-version=2018-04-01" ; HttpResponseMessage response = await client . PutAsync ( url , content ) ; string responseText = await response . Content . ReadAsStringAsync ( ) ; // Check out success status, given IP and other info from response JSON } }

Instead of writing JSON in code as in the example, you’ll probably want to be serializing and deserializing a Container class. Or you can copy that code from my class library on GitHub

Note all the hard-coded parameters used here: mySubscription, myResourceGroup, container-name, port 12345, cpu 2 and memory 8GB.

Example code to query container info

1 2 3 4 5 6 7 8 9 10 11 public async void GetContainerInfo ( string accessToken ) { var url = "https://management.azure.com/mysubscriptions/mySubscription/resourceGroups/myRresourceGroup/providers/Microsoft.ContainerInstance/containerGroups/containerName?api-version=2018-04-01" ; using ( var client = new HttpClient ( ) ) { client . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Bearer" , accessToken ) ; var response = await client . GetAsync ( url , HttpCompletionOption . ResponseContentRead ) ; var result = await response . Content . ReadAsStringAsync ( ) ; // Analyze result } }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 { "additionalProperties" : { } , "containers" : [ { "additionalProperties" : { } , "command" : null , "environmentVariables" : [ ] , "image" : "containerRegistry/imageName" , "instanceView" : null , "name" : "containerName" , "ports" : [ { "additionalProperties" : { } , "port" : 3000 , "protocol" : null } ] , "resources" : { "additionalProperties" : { } , "limits" : null , "requests" : { "additionalProperties" : { } , "cpu" : 1.0 , "memoryInGb" : 3.5 } } , "volumeMounts" : null } ] , "id" : "/subscriptions/mySubscribtionId/resourceGroups/MyResourceGroup/providers/Microsoft.ContainerInsta ce/containerGroups/containerName" , "imageRegistryCredentials" : [ { "additionalProperties" : { } , "password" : null , "server" : "containerRegistry" , "username" : "RegistryUsername" } ] , "instanceView" : { "additionalProperties" : { } , "events" : [ ] , "state" : "Pending" } , "ipAddress" : { "additionalProperties" : { } , "dnsNameLabel" : null , "fqdn" : null , "ip" : "13.72.108.42" , "ports" : [ { "additionalProperties" : { } , "port" : 12345 , "protocol" : "TCP" } ] } , "location" : "eastus" , "name" : "containerName" , "osType" : "Windows" , "provisioningState" : "Creating" , "resourceGroup" : "MyResourceGroup" , "restartPolicy" : "Always" , "tags" : null , "type" : "Microsoft.ContainerInstance/containerGroups" , "volumes" : null }

Example code to delete a container

1 2 3 4 5 6 7 8 9 10 11 public async void DeleteContainer ( string accessToken ) { var url = "https://management.azure.com/mysubscriptions/mySubscription/resourceGroups/myRresourceGroup/providers/Microsoft.ContainerInstance/containerGroups/containerName?api-version=2018-04-01" ; using ( var client = new HttpClient ( ) ) { client . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Bearer" , accessToken ) ; var response = await client . DeleteAsync ( url ) ; var result = await response . Content . ReadAsStringAsync ( ) ; // Analyze result } }

Is there an SDK I can use for this?

The response will be a JSON string in the same format as before. Here’s an example response:

I created a class library, available on GitHub with the code I used for ACI. It has all the basic ACI management including the authorization code in both methods (MSI and app registration).

Here’s some sample code creating a container using the library:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 using ACI = AciResourceAccess ; . . . var azureSubscriptionConfiugration = ACI . ConfigurationFactory . CreateConfigWithActiveDirectoryAppAuth ( azureSubscriptionId : "a187256a-2ebe-4a23-8bfc-8e194d8eagh7" , resourceGroup : "MyResourceGroup" , clientId : "n21d9e2f-1f1c-45cf-q7r7-79c363e5c740" , clientSecret : "zaTD123lof6JUiiMTUb+bGGldmA8NpIvTEht1w7rylA=" , tenantId : "32dd593d-c0bb-48e8-8cd1-8521ab9e3b5e" , imageRegistryServer : "mycontainerregistry.azurecr.io" , imageRegistryUsername : "RegistryUserName" , imageRegistryPassword : "/bf54vMBFEjOXQgTh/PzTwWj9fhcydn3" ) ; var containerCreationConfiguration = new ACI . ContainerCreationConfiguration ( ) { //For some reason, container name can't be in Pascal case. Kebab case works. ContainerName = "my-container" , CpuCore = 2 , MemoryInGB = 4 , ImageName = "mycontainerregistry.azurecr.io/myimage" , Port = 12345 , Location = "east us" , OS = ACI . ContainerCreationConfiguration . OsType . Windows } ; try { var resourceAccess = new ACI . AciResourceAccess ( azureSubscriptionConfiugration ) ; var container = resourceAccess . CreateContainer ( containerCreationConfiguration ) . GetAwaiter ( ) . GetResult ( ) ; Console . WriteLine ( "Container created successfully on ip " + container . properties . ipAddress . ip ) ; //Get container object Thread . Sleep ( 1000 ) ; container = await resourceAccess . GetContainer ( "my-container" ) ; Console . WriteLine ( "Container status now is: " + container . properties . containers . First ( ) . properties . instanceView . currentState . state ) ; //'Waiting' because it's still pulling image //Alternatively, we can use 'GetContainerGroupStatus' to get status await Task . Delay ( TimeSpan . FromMinutes ( 7 ) ) ; //Wait for image to finish pulling var containerStatus = await resourceAccess . GetContainerGroupStatus ( "my-container" ) ; Console . WriteLine ( "Container status now is: " + containerStatus ) ; //ContainerStatus.RUNNING //Delete container await resourceAccess . DeleteContainer ( "my-container" ) ; containerStatus = await resourceAccess . GetContainerGroupStatus ( "my-container" ) ; Console . WriteLine ( "Container status now is: " + containerStatus ) ; //ContainerStatus.DELETED } catch ( Exception e ) { Console . WriteLine ( $ "Error occured: {e}" ) ; }

Microsoft has a full-blown SDK, which you can use as well. Here’s a sample with Azure Container Instances.

Container Group Lifecycle

When a container group is created, it appears as a resource immediately and billing starts. The first thing that happens is the container will pull the image from the container registry. This can be as long as 1 minute for Linux containers or 5-6 minutes for Windows containers. During that stage, the container’s state is Waiting.

After pulling and executing whatever command needed, the container will change to Running state.

When the exec command finished, or the process in the container crashed, the following action depends on the Restart Policy that you can specify when creating the container. This can be set to Always, OnFailure or Never. If the container isn’t restarted, the state will change to Terminated. The resource will not disappear so you can still get logs and console output.

Get container logs and/or console output

When something goes wrong, there are several things we can do.

We can get logs with this Azure CLI / Azure Shell command:

1 az container logs -- name [ container group name ] -- resource - group [ resource group ]

We can get standard output with:

1 az container attach -- name [ container group name ] -- resource - group [ resource group ]

I don’t fully understand the difference between logs and attach. Both seem to show standard output, though attach also shows container state before the command, like when image pulling started and when it finished.

Communicating securely with our containers

The container now exposes a public IP, so anyone can communicate with it. In the future, a private Virtual Network will be available, but right now we have to use some kind of authorization infrastructure to communicate securely with the container.

In our project, we used IdentityServer to create an OAuth authentication, but I think the easiest way to provide security is to use Azure’s service-to-service client credentials authentication.

Summary

ACI offers an easy way to deploy containers. It has a straightforward API and allows you to get started with deployed containers as quickly as possible (so far).

Other upsides of “Serverless Containers” are that you don’t have to manage the running VM and there’s minimal upkeep/development needed.

The billing is per second, which makes it perfect for temporary containers.

There are several downsides I can see:

ACI doesn’t offer any sort of automatic scaling.

There are some security issues due to the public IP exposed (These might be solved in the future with VNet support) and you’ll have to develop your own authentication system.

The billing is more expensive than a VM or App Service you might have when running 24/7.

It’s still unclear to me on how much real-world needs ACI can resolve. In my current project though, ACI fitted beautifully.

Share:

Enjoy the blog? I would love you to subscribe! Performance Optimizations in C#: 10 Best Practices (exclusive article) SUBSCRIBE