Azure function e2e – Part-1

I probably think that you didn’t had a good experience with Azure function while configuring it and deploying via CI CD Pipeline. If you want to learn how to configure App Configuration with Azure and deploy it via CI CD Azure DevOps pipeline, then this article is for you.

Special For You! Are you getting started as an Azure .NET developer and are not aware from where to start and when to use which service? Have you already checked out my Azure Book for .NET Developers ? It’s a full basic learning book getting started with Microsoft Azure Step by Step in 7 days for .NET Developers and many bonuses included source code and access to other Azure Topics.   Click here to check it out!.

Before Diving in:

I hope you have basic understanding of Azure Function and different trigger and binding in Azure function.

Use Case

In this artcle which Part 1, we are gonna create simple Timer Trigger Azure function integrate it with

  • Configure App Configuration
  • Having env based appSettings i.e. Development, Test, PreProduction and Production
  • Azure Application Insight
  • Key Vault

High Level Diagram
azurefunction

Prerequisite:

  • Visual Studio 2019
  • Azure Function latest runtime

Lets now create a Timer Trigger Azure function in Visual Studio.

Step1

 

Step2

 

Run the App by pressing F5.

sTEP3

You can see I have an outdated version of Azure Function Core Tools. Please follow below article in order to update the same.

https://codetraveler.io/2020/02/12/visual-studio-for-mac-updating-to-azure-functions-v3/

After following the above article and making the required changes the warning should be gone.

step4

 

Configure App Configuration

Normally as per my understanding Azure function doesn’t comes up with by default integration with AppConfiguration. As App Configuration help us centralize storage and management of all your application settings separate from your code which is indeed needed in Enterprise level of application. We are gonna use AppSettings in order to centralize our management of application settings and some of them we will keep in key vault.

We will be using Azure App Configuration in order to maintain all our App configuration in appsettings.json files have our configs based on env i.e. (Dev, Test, Prod)

https://docs.microsoft.com/en-us/azure/azure-app-configuration/quickstart-azure-functions-csharp

Install Microsoft.Azure.Functions.Extensions version 1.1.0

Add a new Startup.cs and file with the following code:

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using TimerTrigger.StartupExtensions;

[assembly: FunctionsStartup(typeof(TimerTrigger.Startup))]

namespace TimerTrigger
{
    class Startup : FunctionsStartup
    {
        public IConfigurationRoot _configurationRoot; 


        public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
        {
            FunctionsHostBuilderContext context = builder.GetContext();
            string environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
            _configurationRoot = builder.ConfigurationBuilder
                                    .AddJsonFile(Path.Combine(context.ApplicationRootPath, "appsettings.json"), optional: true, reloadOnChange: false)
                                    .AddJsonFile(Path.Combine(context.ApplicationRootPath, $"appsettings.{environment}.json"), optional: true, reloadOnChange: false)
                                    .AddEnvironmentVariables()
                                    .Build();


        }



        public override void Configure(IFunctionsHostBuilder builder)
        {
            
        }
    }
}

 

Add one new file launchSettings.json inside Properties folder and add the following content on it.

{
  "profiles": {
    "TimerTrigger": {
      "commandName": "Project",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

 

Run the application just to check whether the timer trigger function is getting triggered or not?

Step5

 

💡 Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); _configurationRoot = builder.ConfigurationBuilder

You might confused around why I am using Environment Variable named ASPNETCORE_ENVIRONMENT. If you are using ASP.NET Core and deploying the web app to different environment, you would be aware that we keep all the configuration in appSettings file with env name as Development, Test, PreProduction and Production. It might differ as per different teams. 🙂

So In order to have the same deployment strategy, I am using this Environment Variable which you can change as per your requirement. For this demo I will only maintain two env Dev and Test env to demonstrate e2e CI/CD with Azure function.

We will adding ASPNETCORE_ENVIRONMENT in azure function configuration when we will deploy the Azure Function.

This is how it will look:

Step6

 

💡 Best Practice
As a best practice the timer schedule should not be hardcoded and should come from configuration or key vault.

For this function app I am gonna create one key in local.settings.json and add one key and value as shown below:

{
    "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "Schedule": "0 */5 * * * *"
  }
}

 

You can see my Schedule for the Azure Function is every 5 mins.

This is how my Function code looks like:

using CronExpressionDescriptor;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using System;

namespace TimerTrigger
{
    public static class Function1
    {
        [FunctionName("Function1")]
        public static void Run([TimerTrigger("%Schedule%")]TimerInfo myTimer, ILogger log)
        {

            log.LogInformation(ExpressionDescriptor.GetDescription("0 */5 * * * *"));
            log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
        }
    }
}

 

Integrating with Azure Application Insight

Every application that is running on cloud requires a good monitoring and tracing. Azure Application Insight help us to achieve that. I am not gonna go deep on Application Insight as that is out of scope of this article.

In order to integrate the Application Insight with Azure function I will go my devApplication insight instance and copy the APPINSIGHTS_INSTRUMENTATIONKEY as shown below:

3

 

Add appsettings.Develoment and appsettings.json for now and enable copy to output Directory as Copy if newer.

step7

We will be using appsettings.json for the configuration where keys and values will be same in all the environments.

Copy paste your Instrumentation key

{
  "APPINSIGHTS_INSTRUMENTATIONKEY": "Instrumentation Key Value",
  "KeyVaultName": "devkeyvaultfunc"
}

Install ApplicationInsights dependency from nuget

💡 Dependencies
      Microsoft.Azure.WebJobs.Logging.ApplicationInsights v 3.0.18
      Microsoft.Extensions.Logging.AzureAppServices v 3.1.3

Create one folder in solution with name StartupExtensions and add class file with name StartupExtensions.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace TimerTrigger.StartupExtensions
{
    public static class StartupExtensions
    {
      public  static void AddApplicationInsightLogging(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddLogging(logging =>
            {
                string instrumentationKey = configuration["APPINSIGHTS_INSTRUMENTATIONKEY"];
                if (!string.IsNullOrEmpty(instrumentationKey))
                {
                    logging.AddApplicationInsights(instrumentationKey).SetMinimumLevel(LogLevel.Information);
                    logging.AddApplicationInsightsWebJobs(o => o.InstrumentationKey = instrumentationKey);
                }

                logging.AddAzureWebAppDiagnostics();
            });
        }
    }
}

As you can see we are retrieving the APPINSIGHTS_INSTRUMENTATIONKEY from configuration and setting up the minimum level as Information. So that we can track down the logs and create a tree of dependency in Application Insight.

Run the Function app to check whether we are able to see any logs in application insights or not?
Inject ILogger in your function class and you Function class should look like this now:

using CronExpressionDescriptor;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace TimerTrigger
{
    public class Function1
    {
        private readonly ILogger<Function1> _logger;
        /// <summary>
        /// 
        /// </summary>
        /// <param name="logger"></param>
        public Function1(ILogger<Function1> logger)
        {
            _logger = logger ?? throw new Exception(nameof(logger));
        }

        [FunctionName("Function1")]
        public async Task Run([TimerTrigger("%Schedule%")]TimerInfo myTimer)
        {

            _logger.LogInformation(ExpressionDescriptor.GetDescription("0 */5 * * * *"));
            _logger.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
        }
    }
}

Integrating Dependency Injection in C#

We need to inject our Function as a dependency now. In order to enable DI in Azure function install which is compatible with Azure function

💡 Microsoft.Extensions.DependencyInjection V 3.1.8

Inject the dependency in Startup class.

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.IO;
using TimerTrigger.StartupExtensions;

[assembly: FunctionsStartup(typeof(TimerTrigger.Startup))]

namespace TimerTrigger
{
    class Startup : FunctionsStartup
    {
        public IConfigurationRoot _configurationRoot; 

        public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
        {
            FunctionsHostBuilderContext context = builder.GetContext();
            string environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");

            _configurationRoot = builder.ConfigurationBuilder
                                    .AddJsonFile(Path.Combine(context.ApplicationRootPath, "appsettings.json"), optional: true, reloadOnChange: false)
                                    .AddJsonFile(Path.Combine(context.ApplicationRootPath, $"appsettings.{environment}.json"), optional: true, reloadOnChange: false)
                                    .AddEnvironmentVariables()
                                    .Build();
        }

        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddScoped<Function1>();
            builder.Services.AddApplicationInsightLogging(_configurationRoot);
        }
    }
}

 

Run your Function App:

2021-03-06_12-38-16

 

Integrating Azure Function with Key Vault

In order to integrate Azure Function with Key Vault we need to install the required depdencies as mentioned below:

💡 Dependencies
Microsoft.Azure.KeyVault v 3.0.5
Microsoft.Extensions.Configuration v 5.0.0
Microsoft.Extensions.Configuration.AzureKeyVault v 3.1.8
Microsoft.Azure.Services.AppAuthentication v 1.6.1

Code snippet to integrate key vault:

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureKeyVault;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.IO;
using TimerTrigger.StartupExtensions;

[assembly: FunctionsStartup(typeof(TimerTrigger.Startup))]

namespace TimerTrigger
{
    class Startup : FunctionsStartup
    {
        public IConfigurationRoot _configurationRoot; 


        public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
        {
            FunctionsHostBuilderContext context = builder.GetContext();
            string environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");


            var azureServiceTokeProvider = new AzureServiceTokenProvider();

            var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokeProvider.KeyVaultTokenCallback));


            _configurationRoot = builder.ConfigurationBuilder
                                    .AddJsonFile(Path.Combine(context.ApplicationRootPath, "appsettings.json"), optional: true, reloadOnChange: false)
                                    .AddJsonFile(Path.Combine(context.ApplicationRootPath, $"appsettings.{environment}.json"), optional: true, reloadOnChange: false)
                                    .AddEnvironmentVariables()
                                    .Build();

            builder.ConfigurationBuilder.AddAzureKeyVault($"https://{_configurationRoot["dataconnections:azurekeyvault:keyvaultname"]}.vault.azure.net/", keyVaultClient, new DefaultKeyVaultSecretManager()).Build();

            _configurationRoot= builder.ConfigurationBuilder.Build();
        }



        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddScoped<Function1>();
            builder.Services.AddApplicationInsightLogging(_configurationRoot);
        }
    }
}

appsettings.Development.json

{
  "APPINSIGHTS_INSTRUMENTATIONKEY": "APPINSIGHTS_INSTRUMENTATIONKEY_VALUE",
  "KeyVaultName": "devkeyvaultfunc",
  "DataConnections": {
    "AzureKeyVault": {
      "KeyVaultName": "KeyVaultName"
    }
  }
}

Add a secret to the KeyVault as shown below:

2021-03-06_13-09-38

 

Code snippet to access Key Vault secret

using CronExpressionDescriptor;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace TimerTrigger
{
    public class Function1
    {

        private readonly ILogger<Function1> _logger;
        private readonly IConfiguration _configuartion;


        /// <summary>
        /// 
        /// </summary>
        /// <param name="logger"></param>
        public Function1(ILogger<Function1> logger, IConfiguration configuartion)
        {
            _logger = logger ?? throw new Exception(nameof(logger));
            _configuartion = configuartion ?? throw new Exception(nameof(logger));
        }

        [FunctionName("Function1")]
        public async Task Run([TimerTrigger("%Schedule%")]TimerInfo myTimer)
        {

            _logger.LogInformation(ExpressionDescriptor.GetDescription("0 */5 * * * *"));
            _logger.LogInformation($"Key vault value for secret secret-func-name is {_configuartion["secret-func-name"]}");
            _logger.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
        }
    }
}
sTEP8

If you want to learn Azure from start, my suggestion will be to watch this Learn Azure Step by Step video.

I hope you enjoyed the article as I did writing this up. It’s so cool to know Function Filters and how they help us share common logic across Azure Functions.
If you did, leave your thoughts in the comments below.
Also, I will love it if you share the article on your preferred social media.

Project Source Code

https://github.com/SailleshPawar/TimerTrigger

Author: sailleshpawar

My name is Saillesh Pawar, I work as a Senior Consultant. This blog and content are my own. I blog about my learning and share my information on the same. I believe in being involved in the community so that we can help other people.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.