How to add SSL to Azure Container Instance App?

Viewed 14679

As the title says, I need to setup SSL for an app hosted in Azure Container Instances, however, I'm not quite sure where I need to start.

I have a containerized app hosted via Azure Container Instances at the address http://myApp.northamerica.azurecontainer.io. This address is masked by the 'official' address at http://api.myApp.com.

Is there any reason why I can't just add SSL to the superficial domain @ http://api.myApp.com, that redirects to the real domain @ http://myApp.northamerica.azurecontainer.io? Or do I need to add SSL to both domains?

Furthermore, if I need to secure both domains with SSL, do I need to get separate certificates for each?

Azure provides SSL cert services but I just need to know the best route to take. Thanks.

4 Answers

As far as I know, currently, there is still no built-in support for enabling SSL on Azure Container Instances refer to this.

However, you could have multiple choices for enabling SSL connections for your ACI application.

If you deploy your container group in an Azure virtual network, you can consider other options to enable an SSL endpoint for a backend container instance, including:

The standard SSL certificate maps to a unique domain name, so you need separate certificates for each domain.

You can get started to set up Nginx as an SSL provider in a sidecar container and you need an SSL certificate for the domain api.myApp.com. If you want separate secure access with domain myApp.northamerica.azurecontainer.io, you could configure extra server block in the Nginx config file. Refer to configuring HTTPS server in Nginx.

server {
    listen              443 ssl;
    server_name         www.example.com;
    ssl_certificate     www.example.com.crt;
    ssl_certificate_key www.example.com.key;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ...
}

Did a write up here: https://dev.to/kedzior_io/net-core-api-in-azure-container-instances-secured-with-https-using-caddy2-32jm

Here is the copy/paste:

  1. Create your Web API project let's call it MyApp.Image.Api and let's say it depends on another project MyApp.Core
  2. Add Dockerfile to your MyApp.Image.Api project
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
ARG ENVIRONMENT

ENV ASPNETCORE_URLS http://*:5000
ENV ENVIRONMENT_NAME "${ENVIRONMENT}"

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src

# copy project dependencies 
COPY ["src/MyApp.Core/MyApp.Core.csproj", "src/MyApp.Core/"]
COPY ["src/MyApp.Image.Api/MyApp.Image.Api.csproj", "src/MyApp.Image.Api/"]

RUN dotnet restore "src/MyApp.Image.Api/MyApp.Image.Api.csproj"
COPY . .

RUN dotnet build "src/MyApp.Image.Api/MyApp.Image.Api.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "src/MyApp.Image.Api/MyApp.Image.Api.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyApp.Image.Api.dll"]
  1. Next within the same project create directory called Proxyin it you will need 4 files:

  2. Caddyfile.development - this will create proxy when running locally with self-signed certificate

{
    email artur@isready.io
}

https://localhost {
    reverse_proxy localhost:5000
}

  1. Caddyfile.staging
{
    email artur@isready.io
}

https://myapp-image-api-staging.eastus.azurecontainer.io {
    reverse_proxy localhost:5000
}

  1. Caddyfile.production
{
    email artur@isready.io
}

https://myapp-image-api.eastus.azurecontainer.io {
    reverse_proxy localhost:5000
}

  1. Dockerfile - we will set environment prior to executing this
FROM caddy:latest
ARG ENVIRONMENT

COPY "src/MyApp.Image.Api/Proxy/Caddyfile.${ENVIRONMENT}" /etc/caddy/Caddyfile

*if you don't have multiple environments you know what to skip :-)

  1. Now you need to add docker-compose.yml to the project
version: '3.4'

services:

  proxy:
    image: myapp-image-api-proxy:${CONTAINER_VERSION}-${ENVIRONMENT}
    build:
      context: ../../
      args:
        ENVIRONMENT: ${ENVIRONMENT}
      dockerfile:  src/MyApp.Image.Api/Proxy/Dockerfile
  api:
    image:  myapp-image-api:${CONTAINER_VERSION}-${ENVIRONMENT}
    depends_on:
      - proxy
    build:
        context: ../../
        args:
          ENVIRONMENT: ${ENVIRONMENT}
        dockerfile: src/MyApp.Image.Api/Dockerfile

You should end up with this structure:

image

  1. Now you are ready to go! I use that powershell script to build my images. If you are building to locally just run this:
# set azure environment
$env:ENVIRONMENT = 'development'
$env:CONTAINER_VERSION = 'latest'

# builds images
docker-compose build

If you want to build and push images to Azure Container Registry go for commands below but before make sure you have created registry:

az provider register --namespace Microsoft.ContainerInstance`
az acr create --resource-group EastUS--name myapp --sku Basic // *enable admin user in azure portal once Azure Container Registry is created
# logs to azure
az login

# set azure environment
$env:ENVIRONMENT = 'staging'
$env:CONTAINER_VERSION = 'latest'

# builds images
docker-compose build

# logs to Azure Container Registry
az acr login --name myapp

# tag images
docker tag myapp-image-api-proxy:latest-staging myapp.azurecr.io/myapp-image-api-proxy:latest-staging
docker tag myapp-image-api:latest-staging myapp.azurecr.io/myapp-image-api:latest-staging

# push images
docker push myapp.azurecr.io/myapp-image-api-proxy:latest-staging
docker push myapp.azurecr.io/myapp-image-api:latest-staging

# clean up
docker rmi myapp-image-api-proxy:latest-staging
docker rmi myapp-image-api:latest-staging
docker rmi myapp.azurecr.io/myapp-image-api-proxy:latest-staging
docker rmi myapp.azurecr.io/myapp-image-api:latest-staging

Cool! You are ready to fire it up!##

  1. You will need two packages:

Microsoft.Azure.Management.ContainerInstance.Fluent Microsoft.Azure.Management.Fluent

  1. Let's start from connecting to Azure using C# Fluent API
private IAzure GetAzureContext()
{
    Log.Information("[Container] Getting Service Principal");
    var creds = new AzureCredentialsFactory().FromServicePrincipal(
        _config["AppSettings:ImageApi:ServicePrincipalClientId"],
        _config["AppSettings:ImageApi:ServicePrincipalSecretId"],
        _config["AppSettings:ImageApi:ServicePrincipalTenat"],
        AzureEnvironment.AzureGlobalCloud);

    Log.Information("[Container] Getting subscribtion");
    var azure = Microsoft.Azure.Management.Fluent.Azure.Authenticate(creds).WithSubscription(_config["AppSettings:ImageApi:SubscribtionId"]);

    return azure;
}

Where do you get these service principal ids?

In Azure CLI do:

az ad sp create-for-rbac --name myapp-containers --sdk-auth > my.azureauth

That file will have all service principal data needed.

  1. Now the main part:
private string CreateContainer()
{
    Log.Information("[Container] Authenticating with Azure");

    var azureContext = GetAzureContext();`
    
    // Get azure container registry for my resource group named 'EastUS' and registry 'myapp'
    var azureRegistry = azureContext.ContainerRegistries.GetByResourceGroup(
        _config["AppSettings:ImageApi:ResourceGroupName"],
        _config["AppSettings:ImageApi:ContainerRegistryName"]);

    var acrCredentials = azureRegistry.GetCredentials();

    // Get container group for my resource group named 'EastUS' and container group i.e 'myapp-image-api-staging'
    var containerGroup = azureContext.ContainerGroups.GetByResourceGroup(
        _config["AppSettings:ImageApi:ResourceGroupName"],
        _config["AppSettings:ImageApi:ContainerGroupName"]
        );

    if (containerGroup is null)
    {
        Log.Information("[Container] Creating with fluent API");

        // ContainerGroupName = 'myapp-image-api-staging'
        // ResourceGroupName = 'EastUS'
        // VolumeName = 'image-api-volume'
        // FileShare = 'containers' 
        //      # yes you need to have storage account with file share, we need it so that proxy (caddy2) can store Let's Encrypt certs in there
        //      az storage share create --name myapp-staging-containers-share --account-name myappstaging
        // 
        // StorageAccountName = 'myappstaging'
        // StorageAccountKey = 'well-that-key-here'
        // ProxyContainerName = 'image-api-proxy'
        // ProxyImageName = 'myapp.azurecr.io/myapp-image-api-proxy:latest-staging'
        // VolumeMountPath = '/data/'
        // ApiContainerName 'image-api'
        // ApiImageName 'myapp.azurecr.io/myapp-image-api:latest-staging'

        containerGroup = azureContext.ContainerGroups.Define(_config["AppSettings:ImageApi:ContainerGroupName"])
                .WithRegion(Region.USEast)
                .WithExistingResourceGroup(_config["AppSettings:ImageApi:ResourceGroupName"])
                .WithLinux()
                .WithPrivateImageRegistry(azureRegistry.LoginServerUrl, acrCredentials.Username, acrCredentials.AccessKeys[AccessKeyType.Primary])
                .DefineVolume(_config["AppSettings:ImageApi:VolumeName"])
                    .WithExistingReadWriteAzureFileShare(_config["AppSettings:ImageApi:FileShare"])
                    .WithStorageAccountName(_config["AppSettings:ImageApi:StorageAccountName"])
                    .WithStorageAccountKey(_config["AppSettings:ImageApi:StorageAccountKey"])
                    .Attach()
                .DefineContainerInstance(_config["AppSettings:ImageApi:ProxyContainerName"])
                    .WithImage(_config["AppSettings:ImageApi:ProxyImageName"])
                    .WithExternalTcpPort(443)
                    .WithExternalTcpPort(80)
                    .WithCpuCoreCount(1.0)
                    .WithMemorySizeInGB(0.5)
                    .WithVolumeMountSetting(
                        _config["AppSettings:ImageApi:VolumeName"],
                        _config["AppSettings:ImageApi:VolumeMountPath"])
                    .Attach()
                .DefineContainerInstance(_config["AppSettings:ImageApi:ApiContainerName"])
                    .WithImage(_config["AppSettings:ImageApi:ApiImageName"])
                    .WithExternalTcpPort(5000)
                    .WithCpuCoreCount(1.0)
                    .WithMemorySizeInGB(3.5)
                    .Attach()
                .WithDnsPrefix(_config["AppSettings:ImageApi:ContainerGroupName"])
                .WithRestartPolicy(ContainerGroupRestartPolicy.Always)
                .Create();
    }

    Log.Information("[Container] created {fqdn}", containerGroup.Fqdn);
    return containerGroup.Fqdn;
}

Containers are running!

It takes around 2:50 min to have them fully available.

image

Hitting fqdn given in the code above gives me my swagger index page:

image

I found the answer in this article: https://www.raeffs.dev/blog/2021/05-mai/25-autogenerate-ssl-certificates-for-aci/

I had a service that was reachable through port 80 on other-service.francecentral.azurecontainer.io.

I added the following proxy that respond to ports 80 and 443 on the URL caddy-example.francecentral.azurecontainer.io.

Here is the content of the file azure_container_caddy.yaml:

# https://www.raeffs.dev/blog/2021/05-mai/25-autogenerate-ssl-certificates-for-aci/
name: acicaddynonprod
apiVersion: '2021-10-01'
location: francecentral
properties:
    containers:
        - name: caddy
          properties:
              image: docker.io/caddy:2
              command:
                [
                    'caddy',
                    'reverse-proxy',
                    '--from',
                    'caddy-example.francecentral.azurecontainer.io',
                    '--to',
                    'other-service.francecentral.azurecontainer.io',
                ]
              ports:
                  - port: 80
                    protocol: TCP
                  - port: 443
                    protocol: TCP
              resources:
                  requests:
                      cpu: 0.5
                      memoryInGB: 0.5
    ipAddress:
        ports:
            - port: 80
              protocol: TCP
            - port: 443
              protocol: TCP
        type: Public
        dnsNameLabel: caddy-example
    osType: Linux
tags: null
type: Microsoft.ContainerInstance/containerGroups

You'll have to update the 2 instances of caddy-example and the instance of other-service, and change the location and the region in the hostnames.

Then I only had to deploy it:

az container create --resource-group [something] --file azure_container_caddy.yaml
Related