United Kingdom: +44 (0)208 088 8978

Azure Container Apps with SAFE stack and Farmer

In the third part of our series focussing on Docker, Ryan shows how you can easily deploy to the new Azure Container Apps service.

We're hiring Software Developers

Click here to find out more

If you have been following along with the previous blogs in this series, you will have seen how useful Docker can be for hosting services that your app depends on whilst developing, such as SQL Server or Azurite.

This week we take a look at another use case - bundling your application itself into an image and deploying to Azure Container Apps.

Azure Container Apps

Azure Container Apps is a new service from Microsoft which is built upon technologies such as Kubernetes Event Driven Autoscaling (KEDA) and Distributed Application Runtime (Dapr) running on the Azure Kubernetes Service (AKS). It is fully managed, allowing for simple deployments of containerised apps without needing to manage the underlying infrastructure.

Deploying a SAFE app

The SAFE template comes with out-of-the-box support for deployment to Azure App Service using Farmer to generate ARM templates.

Farmer also includes support for Azure container technologies, so it is easy to convert the app from an App Service to Container Apps deployment.

The following instructions assume you are working with a fresh SAFE stack application.

1. Create a Dockerfile

A Dockerfile simply defines the image you wish to build. In our case we will just need to copy over the bundled app and expose the appropriate http port.

Create a file at the root of your app called Dockerfile (no extension). Open it with a text editor and paste in the following:

FROM mcr.microsoft.com/dotnet/aspnet:5.0

# Copy across folders and files into the image
COPY ./deploy .

# Start the API
EXPOSE 8085
CMD dotnet ./Server.dll

This will make the app available on port 8085.

2. Update FAKE build script

Open the Build.fs FAKE script at the root of the solution.

For more info on FAKE, see the previous blog where we looked at Docker Compose

Replace the entire Azure target with the following:

let docker = createProcess "docker"

// Update these strings to whatever you like. 
// Many must be unique and have strict format rules, so if your deployment fails that will
// likely be the reason.
let registryName = "safecontainerisedreg"
let regServerKey = "registryLoginServer"
let regUserKey = "registryUsername"
let regPassKey = "registryPassword"
let resourceGroup = "safe-containerised"
let containerEnvironmentName = "safe-containerised-env"
let containerAppName = "safe-containerised-app"
let containerName = "safecontainerised"  // alphanumeric, lowercase only

Target.create "Azure" (fun _ ->

    // Every image needs a unique tag
    let tag = System.DateTime.UtcNow.ToString("yyyyMMdd-HHmmss")

    // A container registry is a repository for your images - think of it like a private nuget
    // feed
    let registry = containerRegistry {
        name registryName
        sku ContainerRegistry.Basic
        enable_admin_user
    }

    // These are needed later on to push our app to the registry and pull it out for deployment
    let registryLoginServer, registryUsername, registryPassword =
        let registryDeployment = arm {
            location Location.UKSouth
            add_resources [ registry ]
            output regServerKey registry.LoginServer
            output regUserKey registry.Username
            output regPassKey registry.Password
        }

        // Deploy the registry to Azure
        let outputs =
            registryDeployment
            |> Deploy.execute resourceGroup [ ]

        outputs.[regServerKey], outputs.[regUserKey], outputs.[regPassKey]

    // Build an image of our app using the dockerfile we create earlier, then push it to the
    // container registry
    run docker $"build -t {registryLoginServer}/{containerName}:{tag} ." "."
    run docker $"login {registryLoginServer} -u {registryUsername} -p {registryPassword}" "."
    run docker $"push {registryLoginServer}/{containerName}:{tag}" "."

    let registryCredentials =
        { Server = registryLoginServer
          Username = registryUsername
          Password = SecureParameter "container-registry-pass" } 

    let application =
        // A place to host container apps
        containerEnvironment {
            name containerEnvironmentName
            add_containers [
                // Our container app instance
                containerApp {
                    name containerAppName
                    add_containers [
                        // A container to use for the app instance
                        container {
                            name containerName
                            cpu_cores 0.25<VCores> // Granular machine specs
                            memory 0.5<Gb>
                            private_docker_image $"{registryName}.azurecr.io" containerName tag
                        }
                    ]
                    add_registry_credentials [ registryCredentials ]
                    replicas 1 5 // Min and max instances for autoscaling
                    ingress_target_port 8085us // Use the http port we exposed
                    ingress_transport ContainerApp.Auto
                }
            ]
        }

    let deployment = arm {
        location Location.UKSouth
        add_resource application
    }

    // Pass in the password from earlier as a secure parameter 
    deployment
    |> Deploy.execute resourceGroup [ "container-registry-pass", registryPassword ]
    |> ignore
)

3. Deploy the app

For this step to work you must have the Azure CLI installed and an Azure subscription set up.

At the root of your solution, run

dotnet run Azure

This will bundle and deploy your app.

If the deployment fails, you will need to look carefully at the error output in your console to find out why. By far the most common problem is that your resource names are either already taken or using invalid characters.

4. Visit the URL

Open the Azure Portal, find the resource group you just deployed and the container app within. You should see a URL for your app. Visiting it should present you with the SAFE Todo demo app.

Conclusion

As you can see, deploying your application to Container Apps is not much more effort than using App Service, especially with great tools like Farmer. This approach comes with a bunch of advantages such as the granular configuration, event-driven scalability and portability that containers provide.

There are lots of other things to talk about, such as the built in support for inter-app messaging using DAPR and end-to-end observability using Log Analytics workspaces. Stay tuned for more on those in the coming weeks!

As usual a full sample can be found on our Github.