azureazure-functionsazure-sql-databaseazure-keyvault

Uses SQL connection strings from Azure Keyvault in Azure functions


I am encountering an error when configuring a SQL Server connection string in my Azure Function App using KeyVault to retrieve the connection string. The error message I receive is:

Error configuring services in an external startup class.

Details: Microsoft.Azure.WebJobs.Script.ExternalStartupException: Error configuring services in an external startup class.
System.ArgumentException : Keyword not supported: '@microsoft.keyvault(secreturi'.

at Microsoft.Data.SqlClient.SqlConnectionStringBuilder.GetIndex(String keyword)
at Microsoft.Data.SqlClient.SqlConnectionStringBuilder.set_Item(String keyword,Object value)
at System.Data.Common.DbConnectionStringBuilder.set_ConnectionString(String value)
at Microsoft.Data.SqlClient.SqlConnectionStringBuilder..ctor(String connectionString)
at Serilog.Sinks.MSSqlServer.Platform.SqlClient.SqlConnectionStringBuilderWrapper..ctor(String connectionString,Boolean enlist)
at Serilog.Sinks.MSSqlServer.Dependencies.SinkDependenciesFactory.Create(String connectionString,MSSqlServerSinkOptions sinkOptions,IFormatProvider formatProvider,ColumnOptions columnOptions,ITextFormatter logEventFormatter)
at Serilog.Sinks.MSSqlServer.Configuration.Factories.MSSqlServerSinkFactory.Create(String connectionString,MSSqlServerSinkOptions sinkOptions,IFormatProvider formatProvider,ColumnOptions columnOptions,ITextFormatter logEventFormatter)
at Serilog.LoggerConfigurationMSSqlServerExtensions.MSSqlServerInternal(LoggerSinkConfiguration loggerConfiguration,String connectionString,MSSqlServerSinkOptions> sinkOptions,IConfigurationSection sinkOptionsSection,IConfiguration appConfiguration,LogEventLevel restrictedToMinimumLevel,IFormatProvider formatProvider,ColumnOptions columnOptions,IConfigurationSection columnOptionsSection,ITextFormatter logEventFormatter,IApplySystemConfiguration applySystemConfiguration,IApplyMicrosoftExtensionsConfiguration applyMicrosoftExtensionsConfiguration,IMSSqlServerSinkFactory sinkFactory,IPeriodicBatchingSinkFactory batchingSinkFactory)
at MG.TRUNK.FunctionApp.Startup.ConfigureLogging(IFunctionsHostBuilder builder,IConfiguration configuration)
at D:\a\1\s\src\MG.TRUNK.FunctionApp\Startup.cs : 46
at MG.TRUNK.FunctionApp.Startup.Configure(IFunctionsHostBuilder builder)
at D:\a\1\s\src\MG.TRUNK.FunctionApp\Startup.cs : 25
at Microsoft.Azure.Functions.Extensions.DependencyInjection.FunctionsStartup.Configure(WebJobsBuilderContext context,IWebJobsBuilder builder)
at Microsoft.Azure.WebJobs.WebJobsBuilderExtensions.ConfigureStartup(IWebJobsStartup startup,WebJobsBuilderContext context,IWebJobsBuilder builder)
at D:\a_work\1\s\src\Microsoft.Azure.WebJobs.Host\Hosting\WebJobsBuilderExtensions.cs: 162
at Microsoft.Azure.WebJobs.WebJobsBuilderExtensions.ConfigureAndLogUserConfiguredServices(IWebJobsStartup startup,WebJobsBuilderContext context,IWebJobsBuilder builder,ILoggerFactory loggerFactory)
at D:\a_work\1\s\src\Microsoft.Azure.WebJobs.Host\Hosting\WebJobsBuilderExtensions.cs: 130
at Microsoft.Azure.WebJobs.WebJobsBuilderExtensions.UseWebJobsStartup(IWebJobsBuilder builder,Type startupType,WebJobsBuilderContext context,ILoggerFactory loggerFactory)
at D:\a_work\1\s\src\Microsoft.Azure.WebJobs.Host\Hosting\WebJobsBuilderExtensions.cs: 115
at Microsoft.Azure.WebJobs.WebJobsBuilderExtensions.UseExternalStartup(IWebJobsBuilder builder,IWebJobsStartupTypeLocator startupTypeLocator,WebJobsBuilderContext context,ILoggerFactory loggerFactory)
at D:\a_work\1\s\src\Microsoft.Azure.WebJobs.Host\Hosting\WebJobsBuilderExtensions.cs: 213
at Microsoft.Azure.WebJobs.Script.ScriptHostBuilderExtensions.<>c__DisplayClass7_0.b__1(HostBuilderContext context,IWebJobsBuilder webJobsBuilder)
at /_/src/WebJobs.Script/ScriptHostBuilderExtensions.cs : 235
End of inner exception

What is weird is that the Azure functions gets the connection string correctly and I see logs in database

Azure portal warning


Solution

  • Looks like the issue is with the way you are fetching the SQL connection string from keyvault, check the keyvault reference format you are using to connect sql server with the function App.

    I have tried the same with your code and able to run the function in portal successfully.

    Follow below steps:

    1. Create a secret and store the SQL Database's connection string as its value.

    enter image description here

    1. Enable managed identity in function App.
    2. Navigate to Keyvault=>Access Role(IAM) and assign Key Vault Administrator role to Function app's managed identity.
    3. Add the keyvault reference in Azure function app.
    @Microsoft.KeyVault(SecretUri=https://rkvault11.vault.azure.net/secrets/secret2/Secret_Version)
    

    enter image description here

    Startup class:

    using Microsoft.Azure.Functions.Extensions.DependencyInjection;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Serilog;
    using Serilog.Events;
    using Serilog.Sinks.MSSqlServer;
    using System;
    using System.Collections.Generic;
    using System.Data;
    using System.Linq;
    using System.Reflection;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace FunctionApp5
    {
        public class Startup : FunctionsStartup
        {
            public override void Configure(IFunctionsHostBuilder builder)
            {
                var configuration = builder.GetContext().Configuration;
    
                builder.Services.AddSingleton(configuration);
                ConfigureLogging(builder, configuration);
            }
    
            private static void ConfigureLogging(IFunctionsHostBuilder builder, IConfiguration configuration)
            {
                string version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
    
                var columnOptions = new ColumnOptions();
                columnOptions.Store.Remove(StandardColumn.Properties);
                columnOptions.Store.Remove(StandardColumn.MessageTemplate);
                columnOptions.AdditionalColumns = new List<SqlColumn>
            {
                new SqlColumn { DataType = SqlDbType.NVarChar, ColumnName = "CorrelationID" },
                new SqlColumn { DataType = SqlDbType.NVarChar, ColumnName = "Details" }
            };
    
                var logger = new LoggerConfiguration()
                    .MinimumLevel.Information()
                    .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
                    .MinimumLevel.Override("System", LogEventLevel.Warning)
                    .MinimumLevel.Override("Microsoft.Azure.WebJobs", LogEventLevel.Error)
                    .MinimumLevel.Override("Microsoft.Azure.WebJobs.Host", LogEventLevel.Error)
                    .Enrich.FromLogContext()
                    .Enrich.WithProperty("Version", version)
                    .WriteTo.Console(outputTemplate: "[v{Version}]\t[{Timestamp:u}]\t[{Level:u3}]\t[{SourceContext}]\t{Message}{NewLine}{Exception}")
                    .WriteTo.MSSqlServer(
                        connectionString: configuration.GetSection("SqlConnectionString").Value,
                        sinkOptions: new MSSqlServerSinkOptions
                        {
                            TableName = configuration.GetSection("LogTable").Value,
                        },
                        restrictedToMinimumLevel: LogEventLevel.Information,
                        columnOptions: columnOptions)
                    .Filter.ByExcluding(logEvent =>
                    logEvent.Properties.ContainsKey("SourceContext") &&
                    (logEvent.Properties["SourceContext"].ToString().Contains("FunctionExecutor") ||
                    logEvent.Properties["SourceContext"].ToString().Contains("Host.Results") ||
                    logEvent.Properties["SourceContext"].ToString().Contains("Host.Aggregator") ||
                    logEvent.MessageTemplate.Text.Contains("Executing") || logEvent.MessageTemplate.Text.Contains("Executed")))
                    .CreateLogger();
    
                builder.Services.AddLogging(loggingBuilder =>
                {
                    loggingBuilder.AddSerilog(logger, dispose: true);
                });
            }
        }
    }
    

    Function Code Snippet:

    public static class Function1
    {
        [FunctionName("Function1")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");
    
            var connectionString = Environment.GetEnvironmentVariable("SqlConnectionString");
    
            using (SqlConnection conn = new SqlConnection(connectionString))
            {
                await conn.OpenAsync();
                log.LogInformation("Successfully connected to SQL Server.");
                using (SqlCommand command = new SqlCommand("SELECT COUNT(*) FROM Logs", conn))
                {
                    int result = (int)await command.ExecuteScalarAsync();
                    log.LogInformation($"Query result: {result}");
                }
                return new OkObjectResult("Success");
            }
        }
    }
    

    Invocation Logs:

    2024-12-12T10:31:38Z   [Information]   Executing 'Function1' (Reason='This function was programmatically called via the host APIs.', Id=9136e495-ff9a-43fa-b571-848221f13b1c)
    2024-12-12T10:31:38Z   [Information]   C# HTTP trigger function processed a request.
    2024-12-12T10:31:39Z   [Information]   Successfully connected to SQL Server.
    2024-12-12T10:31:39Z   [Information]   Query result: 1
    2024-12-12T10:31:39Z   [Information]   Executed 'Function1' (Succeeded, Id=9136e495-ff9a-43fa-b571-848221f13b1c, Duration=1243ms)