Executing Time-Scheduled Tasks in Azure Container Apps with the Dapr Cron Binding
In modern cloud-native applications, scheduling tasks to run at specific times or intervals is a common requirement. Whether sending out daily reports, cleaning up stale data, or triggering background processes, time-scheduled tasks are essential for automating workflows. When working with Azure Container Apps, you have several options to implement scheduling. Still, one of the most comprehensive and developer-friendly approaches I have encountered lately is using the Dapr Cron binding. In this blog post, I explain why the Dapr scheduler was a perfect fit for me compared to all the other options available, and how it works.

In modern cloud-native applications, scheduling tasks to run at specific times or intervals is a common requirement. Whether sending out daily reports, cleaning up stale data, or triggering background processes, time-scheduled tasks are essential for automating workflows. When working with Azure Container Apps, you have several options to implement scheduling, but one of the most comprehensive and developer-friendly approaches I ran into lately is using the Dapr Cron Binding. Now let me clear out, that I was already running an Azure Container Apps application and I was already taking advantage of Dapr doing Pub/Sub and as a caching mechanism using the Dapr State Store.

I was about to introduce a new external library (like Hangfire or Quartz), but I’m, always a little picky when it comes to introducing a new external library for obvious reasons. Another obvious solution would be to create an Azure Functions project. This can be hosted separately, or even inside a container and in Azure Container Apps if you like. But now, compared to introducing a new library, I have to introduce a completely new project and maintain that. And then I ran into Dapr’s cron binding, which seemed to be a perfect fit for my situation and I did not have to introduce anything new, but some configuration, and here is why and how…

Why Use this Dapr binding?

To schedule work on a certain time interval, I used a bindings component called cron. When you configure a Dapr component of type bindings.cron, you effectively create a timer-based input binding. This means a binding that acts as input for your application. Some bindings in Dapr support input, some output, and some bindings support both. The Cron binding only supports input.

Now when you create a bindings.cron component, you need to give that component a name, for example, ‘cleanup’, or ‘generate-reports’. Then in the component’s metadata, you need to provide a schedule. This is the time interval your binding will use to ’trigger’. This can be a valid CRON schedule, like 30 * * * * * for every 30 seconds, or 0 30 3-6,20-23 * * * for every half hour between 2 and 6 AM, and 8 and 11 PM, but there are also some comprehensive shortcuts like @every 5m for every 5 minutes, or just @hourly, making sure your task runs every hour from the time the binding is initialized. Need to find the perfect timing? Here is more info about the options you have.

Calling an endpoint

Now the fun thing is, that like with Pub/Sub in Dapr, an input binding just calls an endpoint on your API. So in my case, I have a C# Web Api, and an endpoint is called that corresponds to the name of the binding. So the case of the example above, where I created a ‘cleanup’ binding, an HTTP POST request will be sent to the /cleanup route in my API. So no additional libraries, nothing fancy, I only need to create a new endpoint that accepts an HTTP POST request.

All this is designed to work across multiple environments, including Kubernetes, Azure Container Apps, and other Dapr-supported platforms and unlike traditional scheduling frameworks (e.g., Quartz.NET, Hangfire), the Dapr binding doesn’t require you to manage additional infrastructure or dependencies.

OK, but now I have an endpoint

So all is fine, you have just created an endpoint and configured a component to do some background cleanup processing, but now you introduced a new HTTP endpoint. This endpoint is required to allow anonymous calls, doesn’t this make my API vulnerable? Will, yes… but mostly no!

I think it is never a good idea to expose a container to the public internet. You really need to create (or have Kubernetes create one for you) a network and configure how your containers should behave in that network. You can do this in Kubernetes (or AKS if you like), but also in Azure Container Apps, which effectively runs Kubernetes under the hood. So a good practice would be to disable public internet traffic to your container and have a dedicated service receive internet traffic, examine incoming requests, and redirect those requests to the appropriate service (container). This means that in most production environments, you at least implemented a reverse proxy and often a firewall (or WAF) in front of that reverse proxy.

So now this means that you have control over whether or not you expose the endpoint you created for your binding to the public internet or not, because you can simply configure the reverse proxy to not respond to that route and not redirect it to your service. Because Dapr runs as a sidecar inside your (container) network, it can directly call the endpoint on your container without having to go through the reverse proxy, while public internet traffic must flow through that reverse proxy. So yes, there is an accessible endpoint on your API, but you can configure your system to not expose it to the public internet.

Show me!

So I have a .NET Aspire project where I configured all the required Dapr components in one folder. Did you know you can do that? So in your .NET Aspire AppHost project (in Program.cs) add.

builder.AddDapr();
var options = new DaprSidecarOptions
{
    ResourcesPaths = ImmutableHashSet.Create(Directory.GetCurrentDirectory() + "path-to-your/dapr/components")
};

This will make .NET Aspire spin up all the components you need in your project when you start (debug) your project. So I created a new yaml file called awesome-cron-binding.yaml and placed that in my Dapr components folder with the following content:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: some-data-processor
spec:
  type: bindings.cron
  version: v1
  metadata:
    - name: schedule
      value: "@every 5m"
    - name: route
      value: "/api/data/process"
scopes:
  - data-processor-api

So I created a new component with the name some-data-processor, which is a convenient name for me. Then I configure the component to be a bindings.cron input binding with a schedule of every 5 minutes. This means that the endpoint of my api /some-data-processor is called every 5 minutes from the moment the Dapr component is initialized. Unfortunately, the /some-data-processor endpoint is not convenient for me so I want to change the endpoint so I can neatly organize my API. That is why I added the route metadata. This makes the binding request a different route on my api, so /api/data/process instead of the default /some-data-processor. Finally, I scope the binding to the data processor API, which is the Dapr Application ID of the API I want to handle this request.

Expose an Endpoint in Your Web API

And now to expose the endpoint on my API. In my case, the APIs are fairly large so I chose the good old Controller-based API, keeping my API a bit more readable and maintainable over minimal APIs, which are a better fit if you don’t have that many endpoints, but this take is very opinionated obviously.

[ApiController]
[Route("api/[controller]")]
public class DataController : ControllerBase
{
  [HttpPost("process")]
  public IActionResult ProcessData()
  {
    // Your task logic here
    Console.WriteLine("Processing data every 5 minutes: " + DateTime.UtcNow);
    return Ok();
 }
}

This endpoint doesn’t need to be exposed to the public internet. Dapr will call it internally.

Conclusion

The Dapr Cron Binding is a powerful and comprehensive tool for executing time-scheduled tasks in Azure Container Apps. It simplifies your architecture by eliminating the need for additional scheduling frameworks or separate Azure Functions projects. With Dapr, you can easily expose an endpoint in your Web API and let the scheduler handle the rest—all without exposing your application to the public internet.


Last modified on 2025-01-28

Hi, my name is Eduard Keilholz. I'm a Microsoft developer working at 4DotNet in The Netherlands. I like to speak at conferences about all and nothing, mostly Azure (or other cloud) related topics.