skip to Main Content

I have an Azure Function written with .NET 8 and that uses the isolated worker model.
I want to write integration tests for this function and therefore want to spin it up in memory.

I looked into WebApplicationFactory but it seems like it’s not supporting isolated worker model.
I came across Testcontainers which sounds promising (but that also seems to not be 100% supported). But I’m giving it a try. For that to work I must dockerize my function and this is where I face issues. I had Visual Studio generate a Dockerfile. It builds, but when starting a container with the image it fails.

Before diving into the details of the error, please let me know if there is an alternative approach to solve this problem. I’m open to suggestions.

Dockerfile

It’s worth noting that I build the image from a different folder than my Dockerfile using the command: docker build -f "My Subfolder/Dockerfile" -t my-image:latest . --no-cache.

The commented-out lines are additional things I tried:

FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0 AS base
#COPY --from=mcr.microsoft.com/dotnet/core/sdk:3.1 /usr/share/dotnet /usr/share/dotnet

WORKDIR /home/site/wwwroot
EXPOSE 8080

# Install Azure Functions Core Tools
RUN apt-get update && 
    apt-get install -y curl && 
    curl -sL https://deb.nodesource.com/setup_20.x | bash - && 
    apt-get install -y nodejs && 
    npm install -g azure-functions-core-tools@4 --unsafe-perm

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY . .
RUN dotnet restore "./My Subfolder/Project.csproj"
#COPY . .
WORKDIR "/src/My Subfolder"
#RUN dotnet build "./Project.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./Project.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /home/site/wwwroot
COPY --from=publish /app/publish .

#RUN apt-get update && 
#    apt-get install -y nodejs npm && 
#    npm install -g azurite

#CMD ["ls"]

ENTRYPOINT ["func", "start", "--dotnet-isolated"]

ENV AzureWebJobsScriptRoot=/home/site/wwwroot 
    AzureFunctionsJobHost__Logging__Console__IsEnabled=true 
    WEBSITES_INCLUDE_CLOUD_CERTS=true

Issues

  1. One error seems to be that the function needs a storage account which it doesn’t have access to.
    One thought is to install Azurite in the container, but I didn’t really get this to work and as I have multiple Azure functions that is set up in a similar way, they would all have their own installation.

    Another approach is using docker compose, but that’s not supported by the .NET version of Testcontainers yet.
    So just to move on I removed the part that needs the storage (a TimerTrigger), so error is temporarily gone.

  2. Another error which is quite persistent is that it can’t seem to read my settings.json file. I use the options pattern where I have some validation, like:

    public sealed class MyOptions
    {
        [Required(ErrorMessage = "Option is required")]
        public string SomeOption { get; set; } = null!;
    }
    

    I tried CMD ["ls"] in my final stage of the Dockerfile and it does list my settings.json file. It works fine locally, so not sure what’s going on here.

    The error looks like this:

    2024-09-23 12:17:07 Azure Functions Core Tools
    2024-09-23 12:17:07 Core Tools Version:       4.0.6280 Commit hash: N/A +421f0144b42047aa289ce691dc6db4fc8b6143e6 (64-bit)
    2024-09-23 12:17:07 Function Runtime Version: 4.834.3.22875
    2024-09-23 12:17:07 
    2024-09-23 12:17:08 [2024-09-23T10:17:08.173Z] Csproj not found in /home/site/wwwroot directory tree. Skipping user secrets file configuration.
    2024-09-23 12:17:08 Skipping 'AzureWebJobsScriptRoot' from local settings as it's already defined in current environment variables.
    2024-09-23 12:17:08 Skipping 'AZURE_FUNCTIONS_ENVIRONMENT' from local settings as it's already defined in current environment variables.
    2024-09-23 12:17:09 
    2024-09-23 12:17:09 Functions:
    2024-09-23 12:17:09 
    2024-09-23 12:17:09     MyHttpFunction: [GET,POST] http://localhost:7071/api/MyHttpFunction
    2024-09-23 12:17:09 
    2024-09-23 12:17:09 For detailed output, run func with --verbose flag.
    2024-09-23 12:17:09 [2024-09-23T10:17:09.841Z] MyOptions: 
    2024-09-23 12:17:10 [2024-09-23T10:17:10.154Z] Unhandled exception. Microsoft.Extensions.Options.OptionsValidationException: DataAnnotation validation failed for 'MyOptions' members: 'SomeOption' with the error: 'Option is required'.
    2024-09-23 12:17:10 [2024-09-23T10:17:10.154Z]    at Microsoft.Extensions.Options.OptionsFactory`1.Create(String name)
    2024-09-23 12:17:10 [2024-09-23T10:17:10.154Z]    at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
    2024-09-23 12:17:10 [2024-09-23T10:17:10.154Z]    at System.Lazy`1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)
    2024-09-23 12:17:10 [2024-09-23T10:17:10.154Z]    at System.Lazy`1.CreateValue()
    2024-09-23 12:17:10 [2024-09-23T10:17:10.154Z]    at Microsoft.Extensions.Options.OptionsCache`1.GetOrAdd[TArg](String name, Func`3 createOptions, TArg factoryArgument)
    ...
    

Questions

Different Approach: Is there a different approach to solve this problem? I’m open to suggestions.

Storage Account: How can I handle the storage account requirement in a Docker container? Is installing Azurite the right approach?

Settings.json: How can it be that my settings.json file is not read correctly in the Docker container, and how can I fix this?

I’m not sure how to proceed. I’m not sure if I’m on the right track with Testcontainers and Docker. I’m open to suggestions on how to solve these problems. I’m also open to other approaches to write integration tests for my Azure Function using the isolated worder model.

2

Answers


  1. For isolated worker model functions, the AzureFunctions.TestHost library offers a local testing option without Docker.

    • You can spin up the function in memory and invoke triggers directly, similar to integration testing but faster and more lightweight than full containerization.

    Using Azurite in Docker is still one of the best solutions for simulating Azure Storage. However, to make it work smoothly, use Docker Compose to run both the function app and Azurite together,

    Docker Compose:

    version: '3.8'
    
    services:
      functionapp:
        image: my-azure-function-app:latest
        build:
          context: .
          dockerfile: Dockerfile
        ports:
          - "7071:8080"
        environment:
          AzureWebJobsStorage: "UseDevelopmentStorage=true"
        depends_on:
          - azurite
      
      azurite:
        image: mcr.microsoft.com/azure-storage/azurite
        ports:
          - "10000:10000"
    
    • While Test containers itself does not yet fully support Docker Compose with the .NET isolated worker model, using Docker Compose separately as a test environment for integration testing is a viable alternative.

    Log:

    Azure Functions Core Tools
    Core Tools Version:       4.0.6280 Commit hash: N/A +421f0144b42047aa289ce691dc6db4fc8b6143e6 (64-bit)
    Function Runtime Version: 4.834.3.22875
    
    [2024-09-23T10:17:08.173Z] Csproj not found in /home/site/wwwroot directory tree. Skipping user secrets file configuration.
    Skipping 'AzureWebJobsScriptRoot' from local settings as it's already defined in current environment variables.
    Skipping 'AZURE_FUNCTIONS_ENVIRONMENT' from local settings as it's already defined in current environment variables.
    
    Functions:
    
        MyHttpFunction: [GET,POST] http://localhost:7071/api/MyHttpFunction
    
    For detailed output, run func with --verbose flag.
    [2024-09-23T10:17:09.841Z] MyOptions: 
    [2024-09-23T10:17:10.154Z] Unhandled exception. Microsoft.Extensions.Options.OptionsValidationException: DataAnnotation validation failed for 'MyOptions' members: 'SomeOption' with the error: 'Option is required'.
       at Microsoft.Extensions.Options.OptionsFactory`1.Create(String name)
       at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
       at System.Lazy`1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)
       at System.Lazy`1.CreateValue()
       at Microsoft.Extensions.Options.OptionsCache`1.GetOrAdd[TArg](String name, Func`3 createOptions, TArg factoryArgument)
    
    Login or Signup to reply.
  2. I’ve spent a few days just whittling away at a solution for running my own self-hosted Azure Functions but I’m using Microsoft’s PowerShell image(s) without modification. If you need to run your own container, just sub in the name as required. All of the same options probably apply, so I’ll post my docker compose file for you and others to crib from.

    Obviously you’ll need a folder structure for your bind mounts.

    services:
      fa-pwsh-1:
        container_name: fa-pwsh-1
        image: mcr.microsoft.com/azure-functions/powershell:4-powershell7.4
        restart: unless-stopped
        ports:
          - 13380:80
        environment:
          - AzureFunctionsJobHost__id=fa-pwsh-1
          - AzureFunctionsJobHost__Logging__Console__IsEnabled=true
          - AzureWebJobsScriptRoot=/home/site/wwwroot
          - AzureWebJobsSecretStorageType=Files
          - AzureWebJobsStorage=DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:10000/devstoreaccount1;QueueEndpoint=http://azurite:10001/devstoreaccount1;TableEndpoint=http://azurite:10002/devstoreaccount1;
          - FUNCTIONS_SECRETS_PATH=/azure-functions-host/secrets
          - LANG=en_GB.UTF-8
          - LANGUAGE=en_GB:en
          - LC_ALL=en_GB.UTF-8
          - TZ=Europe/London
          - WEBSITES_INCLUDE_CLOUD_CERTS=true
          #- DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER=true
          #- DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_SSLPROTOCOLS=Tls12,Tls13
          #- DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_SSL_CERTIFICATE_REVOCATION_CHECK_MODE=NoCheck   
        volumes:
          - /mnt/azure-functions/fa-pwsh-1:/home/site/wwwroot
          - /mnt/azure-functions/secrets:/azure-functions-host/secrets
        security_opt:
          - no-new-privileges:true
        depends_on:
          azurite:
            condition: service_healthy
        healthcheck:
          test: ["CMD", "pidof", "dotnet"]
          interval: 30s
          timeout: 10s
          retries: 3
    
      fa-pwsh-2:
        container_name: fa-pwsh-2
        image: mcr.microsoft.com/azure-functions/powershell:4-powershell7.2
        restart: unless-stopped
        ports:
          - 13381:80
        environment:
          - AzureFunctionsJobHost__id=fa-pwsh-2
          - AzureFunctionsJobHost__Logging__Console__IsEnabled=true
          - AzureWebJobsScriptRoot=/home/site/wwwroot
          - AzureWebJobsSecretStorageType=Files
          - AzureWebJobsStorage=DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:10000/devstoreaccount1;QueueEndpoint=http://azurite:10001/devstoreaccount1;TableEndpoint=http://azurite:10002/devstoreaccount1;
          - FUNCTIONS_SECRETS_PATH=/azure-functions-host/secrets
          - LANG=en_GB.UTF-8
          - LANGUAGE=en_GB:en
          - LC_ALL=en_GB.UTF-8
          - TZ=Europe/London
          - WEBSITES_INCLUDE_CLOUD_CERTS=true
          #- DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER=true
          #- DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_SSLPROTOCOLS=Tls12,Tls13
          #- DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_SSL_CERTIFICATE_REVOCATION_CHECK_MODE=NoCheck   
        volumes:
          - /mnt/azure-functions/fa-pwsh-2:/home/site/wwwroot
          - /mnt/azure-functions/secrets:/azure-functions-host/secrets
        security_opt:
          - no-new-privileges:true
        depends_on:
          azurite:
            condition: service_healthy
        healthcheck:
          test: ["CMD", "pidof", "dotnet"]
          interval: 30s
          timeout: 10s
          retries: 3
          
      azurite:
        container_name: azurite
        image: mcr.microsoft.com/azure-storage/azurite
        volumes:
          - /mnt/azure-functions/azurite:/data
        healthcheck:
          test: nc 127.0.0.1 10000 -z
          interval: 1s
          timeout: 2s
          retries: 15
    

    Note that when using a compose file, you don’t need to expose Azurite’s ports and can instead just define the endpoints in the connection string instead since the other services (Function App containers) can just lookup azurite and Docker’s DNS will point them to the correct container.

    To be clear, the AccountName and AccountKey as defined here are well known and are what you should actually use – ie. I’m not giving away any secrets, you actually need to use those values in the connection string.

    Hopefully the bind mounts make sense without explanation but I should mention that I’m effectively sharing the secrets files (where the master key is) between both Function App instances. You may want to split that depending on your requirements.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search