DEV Community

Artur Kedzior
Artur Kedzior

Posted on

.NET Core API in Azure Container Instances, secured with HTTPS using Caddy2

Here is what you need to get this running in Azure in no time:

  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"]
Enter fullscreen mode Exit fullscreen mode
  1. Next within the same project create directory called Proxyin it you will need 4 files:

    1. Caddyfile.development - this will create proxy when running locally with self-signed certificate
{
    email artur@isready.io
}

https://localhost {
    reverse_proxy localhost:5000
}

Enter fullscreen mode Exit fullscreen mode
  1. Caddyfile.staging
{
    email artur@isready.io
}

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

Enter fullscreen mode Exit fullscreen mode
  1. Caddyfile.production
{
    email artur@isready.io
}

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

Enter fullscreen mode Exit fullscreen mode
  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

Enter fullscreen mode Exit fullscreen mode

*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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)