Azure / azure-functions-core-tools

Command line tools for Azure Functions
MIT License
1.29k stars 429 forks source link

Loading settings from User Secrets doesn't work #2413

Open SeanFeldman opened 3 years ago

SeanFeldman commented 3 years ago

User Secrets are supposed to be working but they don't.

When trying to move settings from local.settings.json to User Secrets, the following error is happening:

image

Azure Functions Core Tools Core Tools Version: 3.0.3233 Commit hash: d1772f733802122a326fa696dd4c086292ec0171 Function Runtime Version: 3.0.15193.0

[2021-01-29T01:13:59.792Z] Found C:\github\djpool\mdc\Cloud\src\Endpoints\Integration\Integration.csproj. Using for user secrets file configuration. Missing value for AzureWebJobsStorage in local.settings.json and User Secrets. This is required for all triggers other than httptrigger, kafkatrigger. You can run 'func azure functionapp fetch-app-settings ' or specify a connection string in local.settings.json or User Secrets.

Context: https://twitter.com/sfeldman/status/1354567460532285442

anthonychu commented 3 years ago

Transferring to Core Tools repo.

Looks like the issue occurs when the same key is in different sources. In this case, local.settings.json contains an empty AzureWebJobsStorage setting, and user secrets contains the correct setting. Local settings is union'd with environment variables and user secrets here and the validation is looking for the first matching key, which happens to be the local.settings.json value.

The validation should ideally be reading from the configuration builder, so it sees the same values as what is passed to the host.

The workaround here is to ensure the key only exists in one of the configuration sources (in this case, the user secrets).

SeanFeldman commented 3 years ago

I've just tried to move all settings from local.settings.json to user secrets and remove the file altogether but still had the following error:

Missing value for AzureWebJobsStorage in local.settings.json. This is required for all triggers other than httptrigger, kafkatrigger. You can run 'func azure functionapp fetch-app-settings ' or specify a connection string in local.settings.json.

anthonychu commented 3 years ago

Can you check your csproj to see if there's a user secret id in there? (or run dotnet user-secrets list)

This appears to be a message that is shown when no user secrets are detected. /cc @gzuber

SeanFeldman commented 3 years ago

Can you check your csproj to see if there's a user secret id in there?

<UserSecretsId>$(MSBuildProjectName)</UserSecretsId>
PS C:\github\> dotnet user-secrets list

Values:***.Endpoints.Integration:***:ApiKey = ***
Values:***.Endpoints.Integration:***:AccessToken = ***
Values:***.Cloud.Persistence:PlatformConnection = ***
Values:***.Cloud.Billing:***:PublicKey = ***
Values:FUNCTIONS_WORKER_RUNTIME = dotnet
Values:AzureWebJobsStorage = UseDevelopmentStorage=true
Values:AzureWebJobsServiceBus = ***
Values:AzureFunctionsJobHost__logging__logLevel__default = Debug
Values:AzureFunctionsJobHost__logging__applicationInsights__samplingSettings__isEnabled = false
Values:APPINSIGHTS_INSTRUMENTATIONKEY = 00000000-0000-0000-0000-000000000000
IsEncrypted = False
SeanFeldman commented 3 years ago

@anthonychu, want to share your spike so that I can compare and contrast?

anthonychu commented 3 years ago

Do you remember how did you added it?

Mine looks like:

<UserSecretsId>233ae9e5-9f2b-42d7-b63c-732c96d44f27</UserSecretsId>

I believe Core Tools uses custom logic to get the user secret id from the csproj, and it doesn't know how to resolve that expression. You'll probably need to switch it to a GUID.

SeanFeldman commented 3 years ago

I'll try that. Saying that, $(MSBuildProjectName) is an MSBuild variable every single project type recognizes. It will be really disappointing to find out Functions is "unique".

seankearney commented 3 years ago

@SeanFeldman and @anthonychu -- Using $(MSBuildProjectName) as a variable in the .csproj is an issue here. If we replace our variable with a static Guid value then secrets are loaded.

Digging into why the variable doesn't work...

If we AddUserSecrets(assembly) in Startup, like we would normally do, then we actually have two JsonConfigurationProvider for secrets.json in our configuration root! The first one loaded has our secrets, but the second one loaded does not.

The PhysicalFileProvider for the second secrets.json is pointing at a non-existant secrets.json file. Specifically it is C:\Users\sean\AppData\Local\AzureFunctionsTools\Releases\3.18.0\cli_x64\.

Furthermore, if we don't call .AddUserSecrets(...) the wrong one is still loaded.

SeanFeldman commented 3 years ago

Thank you for sharing the finding, @seankearney.

@anthonychu, it looks like Functions is doing something wrong here.

anthonychu commented 3 years ago

We'll investigate and prioritize if it's not a difficult fix. Please use a GUID for now.

anthonychu commented 3 years ago

Core Tools doesn't have access to the dotnet toolchain needed to resolve the secrets automatically, so we had to write custom logic to locate the csproj file and extract the secrets id. This means it's not trivial for us to resolve those variables.

That said, the initial issue is worth investigating, where an empty AzureWebJobsStorage in local.settings.json hides the user secrets value. Perhaps this is working as intended, but we should see if we can provide a message when this happens. We already have some warnings when environment variables are overriding local.settings.json values. @gzuber could you please investigate?

NorthHighlandNicole commented 3 years ago

Leaving a comment here in case someone else bumps the same issue.

I experienced the issue described here issue and the last response from @anthonychu helped me resolve it. I had to make sure the AzureWebJobsStorage value in local.settings.json wasn't empty AND I had to downgrade my version of package Microsoft.Extensions.Configuration.UserSecrets from 3.1.16 to 3.1.13.

But then I ran into a problem when I tried to create a timer triggered function. After fighting with this for a while I found that it worked fine on one machine but on another machine I experienced the problem described on #1592 [Improve timer trigger error message if storage emulator is not running #1592 ]... except that my problem wasn't that my value was missing, it seemed to be some strange mix of this 2 reported issues. In the end, I was able to find that updating to the latest version of Visual Studio solved my problem.

So for timer triggered functions I found that the latest version of VS 2019 Community (>=16.10.1) using .Net Core 3.1, Microsoft.Extensions.Configuration.UserSecrets 3.1.13 and ensuring that AzureWebJobsStorage in my local.settings.json was not an empty string everything works on all my development machines.

Hope this helps!

GCoding83 commented 2 years ago

Unfortunately, the suggestion from @NorthHighlandNicole is unavailable in .NET 5, I am trying to upgrade all my projects to .NET 5, and I can't do it with Azure Functions, because of this very problem concerning User Secrets and AzureWebJobsStorage.

The problem is not just with AzureWebJobsStorage. Whatever value I put for the "Connection" field next to my trigger path, if there is a corresponding value in User Secrets but none in local.settings.json, the trigger will not work.

image

In the image above, if "MyConnection" is a key in User Secrets with a valid sotrage connection string but there is no such key in local.settings.json, then I get the following error:

"Microsoft.Azure.WebJobs.Host: Error indexing method 'Functions.BlobFunction1'. Microsoft.Azure.WebJobs.Extensions.Storage: Storage account connection string 'AzureWebJobsMyConnection' does not exist. Make sure that it is a defined App Setting."

If I do put "MyConnection" in local.settings.json, then the trigger works as expected... but that defeats the purpose of User Secrets!

Is there a way around this problem, since it prevents upgrade to .NET 5 (and Isolated Process Functions)?

Thanks.

mattchenderson commented 2 years ago

Source of error message just for reference in the discussion: https://github.com/Azure/azure-functions-core-tools/blob/5e114c97ed391449fefcae1a93767f6e9fd8f49a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs#L404-L410

This is also hit when attempting to use identity for the AzureWebJobsStorage connection (still a preview feature), where I have AzureWebJobsStorage__accountName instead of AzureWebJobsStorage in local.settings.json

As an aside, I'd also argue that the phrasing of the error implies a managed identity in a location where func is being run, which is not really a scenario. Managed identities only exist in Azure. When developing locally, identity can be used, but it will instead use the developer identity or other options that can be configured.

RCSandberg commented 2 years ago

Just to also iterate, kind of what's already been said... the problem is also present running isolated processes in .NET 6

public async Task Run(
  [BlobTrigger($"{ContainerName}/{{blobName}}", Connection = "MySpecialAppSettingConnectionString")]
...

Having MySpecialAppSettingConnectionString set correctly in local.settings.json will work just fine.

But it will not get picked up correctly from user secrets.

karpikpl commented 2 years ago

Not sure if that helps anyone, but I to get a message saying Using for user secrets file configuration..

  1. <UserSecretsId>898cd92a-e4a0-46ff-9296-2fef4b6b9a14</UserSecretsId>
  2. dotnet user-secrets list gives:
    ServiceBusConnection = Endpoint=sb://xxx
    AppConfigConnectionString = Endpoint=https://xxxx

    And app fails because none of those values are there.

I've modified startup.cs and solved the issue by adding:

public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
            if (System.Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT") == "Development")
            {
                builder.ConfigurationBuilder.AddUserSecrets<Startup>();
            }

but I was expecting default builder to do it...

jassent commented 1 year ago

I am encountering the same issue in an Azure Function v4 NET6 using isolated mode. If I move secrets from local.settings.json to secrets.json, the secrets are not loaded into Environmental variables. The azure function console output says that secrets were found "Using for user secrets file configuration" but they are not loaded into the environment.

For example, in program.cs, I need to read a connection string to use with registering Dependency Injection services: var dbconn = Environment.GetEnvironmentVariable("dbconn");

The above populates if dbconn is in local.settings.json but does not work if dbconn is in secrets.json

I can load them into a config but I don't want to do that in program.cs

PureKrome commented 1 year ago

The reason the user-secrets are not getting loaded is because the default implementation doesn't support them. To enable user-secrets, there's a few steps required.

(NOTE: i've ommited any user-secrets nuget packages, init steps, etc... they are required but assumed you know about that, already)

A Story in 3 Acts ...

Act 1: The Setup

Here's an example of what the current code looks like, using the default templates, etc.. with some changes .. but -no user-secret- changes. aka. "the before" code :)

image

(and the code for copy-pasting):

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices((appBuilder, services) =>
    {
        // Use Serilog for logging.
        // Serilog is configured via Env Vars .. which is
        //   => Localhost: local.settings.json
        //   => Azure Functions on Azure: the 'Configuration' tab/section/page
        var logger = new LoggerConfiguration()
            .ReadFrom.Configuration(appBuilder.Configuration) // <-- This is the magic, here!
            .Enrich.FromLogContext()
            .CreateLogger();
        services.AddLogging(configure => configure.AddSerilog(logger, true));

        // Setup our configuration settings.
        // Just a strongly typed class.
        services
            .AddOptions<ConnectionStringOptions>()
            .Configure<IConfiguration>((settings, configuration) =>
            {
                configuration
                    .GetSection(ConnectionStringOptions.ConfigurationSectionKey)
                    .Bind(settings);
            });

        // DapperRepository requires an instance of an IOptions<ConnectionStringOptions> class
        // in the ctor .. which is why that is registered, above.
        services.AddSingleton<IRepository, DapperRepository>(); 
    })
    .Build();

host.Run();

Act 2: The Magic

Ok, fine. so now lets tell our program that we -also- need to wire up user secrets!

image


var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureAppConfiguration(builder =>
    {
        builder.AddConfiguration(GetConfigurationBuilder());
    })
    .ConfigureServices((appBuilder, services) =>
    {
        var logger = new LoggerConfiguration()
            .ReadFrom.Configuration(appBuilder.Configuration)
            .Enrich.FromLogContext()
            .CreateLogger();
        services.AddLogging(configure => configure.AddSerilog(logger, true));

        // Setup our configuration settings.
        services
            .AddOptions<ConnectionStringOptions>()
            .Configure<IConfiguration>((settings, configuration) =>
            {
                configuration
                    .GetSection(ConnectionStringOptions.ConfigurationSectionKey)
                    .Bind(settings);
            });

        services.AddSingleton<IRepository, DapperRepository>();
    })
    .Build();

host.Run();

// Strongly based off/from: https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.host.createdefaultbuilder?view=dotnet-plat-ext-5.0
static IConfiguration GetConfigurationBuilder() =>
    new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddUserSecrets<Program>()
        .AddEnvironmentVariables()
        .Build();

So here we can see that we need to say "please also use my extra configuration information".

so builder.AddConfiguration(GetConfigurationBuilder()); will 'add' anything extra to the existing default configuration settings.

Here I have said:

And viola! you can now access your user secrets.

Act 3: The Future

So ... where does this leave us?

  1. We can just do the above and stuff works.
  2. We can update the ConfigureFunctionsWorkerDefaults to also include UserSecrets, but this means Azure Functions has yet another dependency .. which might not be wanted/a good thing?

Anyhow ... hope this helps.

sintetico82 commented 1 year ago

For me, this is not working.

If I do Environment.GetEnvironmentVariables() , I can't find any values from secrets.json.

PureKrome commented 1 year ago

@sintetico82 user-secrets (aka. secrets.json) are not the same as environmental variables. This is why you don't see the content of the user secrets in Environment.GetEnvironmentVariables(). These are two different/independent stores/providers.

Jandev commented 1 year ago

The solution of PureKrome appears to be working great.

However, it looks like the configuration is too late for the Bindings/Triggers. I can see the secrets are being added to the IConfiguration, but these values aren't used in the Connection parameter of some triggers.

When trying to set the Configuration of the IFunctionsWorkerapplicationBuilder an error occurs. Any suggestions for these triggers?

madhavireddy2006 commented 1 year ago

A simple solution is below. This is a working code for .Net 6 isolated function app. You can use the secrets present in either local.settings.json or secrets.json.

In the below sample "LocalSettingConfigKey" is present in the local.settings.json and "SecretNameHere" is present in secrets.json

Step1: Setting secrets "dotnet user-secrets set "SecretNameHere" "secretValuehere" --project C:\Work\SecretsPOC\FunctionApp1\FunctionApp2"

Step2: Make sure that your project file is updated with the SecretId as below `

net6.0
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
**<UserSecretsId>xxxxxxxx-db7d-xxxx-a5d0-xxxxxxxx</UserSecretsId>**

`

Step3: Update Program.cs to configure AppConfiguration before you configure services.

Program.cs looks like below:

var host = new HostBuilder() .ConfigureFunctionsWorkerDefaults() .ConfigureAppConfiguration(builder => { builder.AddConfiguration(new ConfigurationBuilder().AddUserSecrets().Build()); }) .ConfigureServices(service => { service.AddSingleton(sp => { var config = sp.GetService(); string secretValueFromUserSecrets = config.GetValue("SecretNameHere"); var localConfig = config.GetValue("LocalSettingConfigKey");
}); }) .Build();

host.Run();

I hope this is helpful.

Imp Note: If we have same key in both secrets.json and local.settings.json, secrets.json takes the precedence.

PureKrome commented 1 year ago

@madhavireddy2006 yep -> that's what I did in my answer, above. 👍🏻

jassent commented 1 year ago

@PureKrome & @madhavireddy2006, an issue with that approach is that it is extra code that won't work once published and put into production. Plus when you start the Azure Function it literally says in the console that Secrets were found but doesn't actually load them.

PureKrome commented 1 year ago

@jassent 👋🏻 hi!

won't work once published and put into production.

Can you please elaborate on this? Mainly, the above code is what I have in multiple production azure functions, so it's working for me/us.

Don't forget, this is targetting user secrets for LOCALHOST development. so for azure functions production (app service), there is no local secrets store ... or more to the point, there is no file that has your secrets in it. On production (using the Azure portal to control your configuration settings) ... those key/values are converted to environmental variables .. which is why new ConfigurationBuilder() default-includes adding env vars as a configuration-source.

if you wanted to use a separate KEY STORE (like Azure Key Vault) then that is missing from the example I've provided above. It's not hard to extend the sample code, above. But it's a bit more involved as it's not just a code change.

HTH

jassent commented 1 year ago

@PureKrome sorry, I was a bit terse with that comment. The sample code that was provided relies on AddUserSecrets() which will not work once put into production (because there is no such thing as secrets.json once published). The frustration is that even without AddUserSecrets() the Azure Function startup acknowledges the existence of a secrets.json file but doesn't actually read it. What I am doing instead is using a local.settings.json and telling git to ignore that file to keep secrets out of repositories. The advantage of using local.settings.json is no additional startup code needed and the Visual Studio publish function makes is easy to sync local.settings.json to the Function App's Application Settings. I am also using Azure Keyvault bindings in both the local.settings.json and within the published Application Settings - so there is a one-to-one alignment of local and published settings.

galvesribeiro commented 1 year ago

Hey folks! Just dropping here what worked for me. I'm using .Net 7 in the isolated-host mode.

@PureKrome approach was almost there but not 100% for me. I've added the UserSecretId prop and also tried the AddConfiguration call but haven't worked for me.

What did work, and the idea came from his suggestion, was to build a temporary IConfiguration at the root Program.cs and get the AAD credentials I need in order to then add the Azure AppConfig which is where all our settings lives.

Here is the gist:

var hostBuilder = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults();

var tmpConfig = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddUserSecrets<Program>(true)
    .Build();

TokenCredential cred = default!;

if (tmpConfig["AZURE_TENANT_ID"] is not null &&
    tmpConfig["AZURE_CLIENT_ID"] is not null &&
    tmpConfig["AZURE_CLIENT_SECRET"] is not null)
{
    cred = new ClientSecretCredential(
        tmpConfig["AZURE_TENANT_ID"],
        tmpConfig["AZURE_CLIENT_ID"],
        tmpConfig["AZURE_CLIENT_SECRET"]
    );
}
else
{
    cred = new DefaultAzureCredential();\
}

hostBuilder.ConfigureAppConfiguration((ctx, config) =>
{
    var region = ctx.Configuration["REGION"]?.ToString() ?? "centralus";
    var label = ctx.HostingEnvironment.IsDevelopment() ? $"dev-{region}" : region;
    var endpoint = tmpConfig["APP_CONFIG_ENDPOINT"]!;

    config.AddAzureAppConfiguration(options =>
    {
        options.Connect(new Uri(endpoint), cred)
            .Select($"{APP_NAME}:*")
            .Select($"{APP_NAME}:*", label)
            .ConfigureRefresh(refresh =>
            {
                refresh
                    .Register($"{APP_NAME}:Sentinel", true)
                    .SetCacheExpiration(new TimeSpan(0, 5, 0));
            }).ConfigureKeyVault(kv => kv.SetCredential(cred));
    });
});

The tmpConfig settings for our case, only include the Azure AppConfig URL + the Azure credentials. Everything else (including the options from the trigger attributes) comes down from Azure AppConfig.

I'm sure it will not fix everyone's case but, I hope it help.

ryansalt commented 1 year ago

Just run into this issue ourselves, and while the code listed above works for HttpTriggers, we've noticed that functions with ServiceBus Triggers appear to begin creating listeners before any startup code has run, grabbing config values from local.settings.json if it's present, but never from usersecrets.

We put breakpoints on "var HostBuilder = new HostBuilder()", and before that breakpoint is hit, we get console warnings that servicebus listeners were unable to start.

With no keys in local.settings.json, but only in secrets.json:

The listener for function 'Functions.DemoFunction' was unable to start.
[2023-09-13T09:45:56.123Z] The listener for function 'Functions.DemoFunction' was unable to start. Microsoft.Azure.WebJobs.Extensions.ServiceBus: Service Bus account connection string 'sbConnString' does not exist. Make sure that it is a defined App Setting. 

With empty values in local.settings.json, but full values in secrets.json:

[2023-09-13T09:02:59.168Z] Found C:\Users\rxxx.csproj. Using for user secrets file configuration.
[2023-09-13T09:03:00.288Z] The listener for function 'Functions.DemoFunction' was unable to start.
[2023-09-13T09:03:00.291Z] The listener for function 'Functions.DemoFunction' was unable to start. Azure.Messaging.ServiceBus: The connection string could not be parsed; either it was malformed or contains no well-known tokens.

This appears before the breakpoint is hit, so we never get a chance to tell it to use usersecrets.

We have seen a warning that application settings were modified in an external startup class and that may be an issue when scaling in production, but we've ignored that since usersecrets won't be used on production.

Krumelur commented 9 months ago

More than 2 years later I found this thread. :-) Isolated functions using NET8 and the issue still persists. Startup confirms that user secrets are present but won't be read. Would be awesome to get this bumped up on the prio list.

brainded commented 9 months ago

More than 2 years later I found this thread. :-) Isolated functions using NET8 and the issue still persists. Startup confirms that user secrets are present but won't be read. Would be awesome to get this bumped up on the prio list.

Didn't there used to be a plus reaction? +1 on this, would love a simple strategy to do secret management instead of rolling something on my own

PureKrome commented 9 months ago

@brainded

Didn't there used to be a plus reaction?

do you mean this?

image

rfphill commented 3 months ago

More than 2 years later I found this thread. :-) Isolated functions using NET8 and the issue still persists. Startup confirms that user secrets are present but won't be read. Would be awesome to get this bumped up on the prio list.

Still an issue. For me it's the appsettings.json file that contains the connection information needed for the blob trigger. I see that the function app has the setting in the collection of merged settings but when the function is getting indexed the app setting for "connection" isn't found. I see it's been opened as an issue but no love...

ADH-LukeBollam commented 1 month ago

Not sure if I'm having the same issue as everyone else (where secrets arent being found) but in .NET 8 isolated function, my secrets are not overwriting the local.settings.json values. I have to remove the key entirely from local.settings for it to use the secrets version, even though Secrets are being added last which is not in line with the docs.

var host = new HostBuilder()
    .ConfigureAppConfiguration(builder =>
    {
        builder
        .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true) //To be used in development
        .AddEnvironmentVariables()
        .AddUserSecrets(Assembly.GetExecutingAssembly(), true);
    })

image

PureKrome commented 1 month ago

@ADH-LukeBollam - are you able to debug your code and 'see' what has been registered and in what order ? Refer to my image "Act 2: The Magic", above for what to debug-look for.

When you add .AddUserSecrets as the last item in chain when building, it should be the last. But my guess is that you're adding those settings (fine) but then some other code (maybe something default?) is then kicking in and adding that other code to the end .. which ends up being re-called and overriding what you're expecting.

ADH-LukeBollam commented 1 month ago

@PureKrome you were right, the next line after was .ConfigureFunctionsWorkerDefaults() which adds the environment variables and local settings configuration sources again, stomping my secrets config. Thanks!