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.