Limitations of bicep deployed function apps

Posted on Thu 01 June 2023 in devops

One of my clients was looking to deploy a function app which would be triggered by an event grid trigger, simple enough to set up and simple enough to write some bicep to deploy the resources for right? Well not quite.

The function that was being deployed was slightly more complex and made use of a few of the clients libraries and used DI (dependency injection) which is a pretty normal thing, so the function gets deployed using az publish commands through powershell. It was defined in bicep using something like

resource functionApp 'Microsoft.Web/sites@2020-06-01' = {
  name: appName
  location: location
  kind: 'functionapp'
  ........

Now normally that's all you need to do to get a function app resource ready, you just need that resource within your bicep. Bicep is ideally to be idempotent, so there should be no difference in the end result no matter how many times you run it, the outcome should be the same. You would have your azure resource to be deployed to then you can deploy your compiled code to this resource and let it run. In our case here, we want our function to be an eventgridtrigger function, which is simple enough to define within code, just something like this ....

namespace MyEventGridFunction
{
    public static class MyEventGridFunction
    {
        [FunctionName("MyEventGridFunction")]
        public static void Run([EventGridTrigger]EventGridEvent eventGridEvent, ILogger log)
        {
            log.LogInformation(eventGridEvent.Data.ToString());
        }
    }
}

If you get this in a proper project, flesh out the bicep above and deploy, it'll happily run. Where we encounter a problem is when we try to define the eventgridsubscription via bicep,

Flesh out this part - it's incomplete
resource eventGridSubscription 'Microsoft.EventGrid/eventSubscriptions@2020-06-01' = {
  name: eventGridSubscriptionName
  scope: eventGridTopic
  properties: {
    destination: {
      endpointType: 'WebHook'
      properties: {
        endpointUrl: eventGridSubscriptionUrl
      }
    }
  }
}

As you can see from the above bicep - the eventGridSubscription is linked to the function app, except when we run the bicep it responds with an error of the function must exist first! This is an issue as the if the code isn't deployed how can it exist? The eventgridsubscription is effectively looking for an endpoint which is not defined (Nor in my opinion SHOULD be defined!) from our bicep template. This clashes with our ideal of idempotency within our bicep templates, we want to be able to run our bicep and simply setup our infrastructure ready for our code. What if we can effectively define our function within our function app in our bicep template? This is a part of the job that I'm not fond of, the inaccuracy and heavily context dependency of language, in this terms a function is closer conceptually to a method, as the name function is already taken by type of the 'site' we have setup.

resource function 'Microsoft.Web/sites/functions@2020-06-01' = {
  name: '${functionApp.name}/${functionName}'
  properties: {
.......

This looks perfect, it sets up a function within the function app site we defined earlier. Except we have a problem, how do we deploy code to this? When we look into the folder structure on the azure resource we've setup, it will literally be

site\wwwroot\$functionAppName\FunctionName

It'll define it's own function.json etc but we can not deploy our code to it! This is a pretty fundamental flaw, we can do some pretty cool things with it like define simple functions within small c# files that get deployed with the bicep template or even have inline c# such as

resource function 'Microsoft.Web/sites/functions@2020-06-01' = {
  name: '${functionApp.name}/${functionName}'
  properties: {
    config: {
      disabled: false
      bindings: [
        {
          name: 'eventGridEvent'
          type: 'eventGridTrigger'
          direction: 'in'
        }
      ]
    }
    files: {
      'run.csx': '''
      #r "Microsoft.Azure.EventGrid"
      using Microsoft.Azure.EventGrid.Models;
      using Microsoft.Extensions.Logging;

      public static void Run(EventGridEvent eventGridEvent, ILogger log)
      {
          log.LogInformation(eventGridEvent.Data.ToString());
      }
      '''
    }
  }
}

But we can not setup a function like this that we deploy to from a seperate code pipeline. So we've hit a deadend with a single bicep execution to deploy all of our infrastructure.

What I ended up doing was to simplify the pipeline a little, deploy the site resource with a type of functionapp, then to deploy the function app source then have a different powershell task to deploy the eventgrid bicep.