.NET Core API in Azure Container Instances, secured with HTTPS using Caddy2
Tags: azure, csharp, docker, programming
Published: 2021-10-06 16:48:55

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"]
```

3. Next within the same project create directory called `Proxy`in 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
}

```

 2. `Caddyfile.staging`

```
{
    email artur@isready.io
}

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

```

 3. `Caddyfile.production`

```
{
    email artur@isready.io
}

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

```

 4. `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 :-) 

4. 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x911q4yb6m413jyzy5rt.png)

5. 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`

2. 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.

3. 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/anov70rjsqit4fl8kqvn.png)
 
Hitting `fqdn` given in the code above gives me my swagger index page:

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vh3l9m2kzebjiy430i5a.png)