.Net Core应用程序中appsettings.json的最佳实践 - 覆盖开发设置(或反之)?

30

寻求一种明智的方法来构建 .Net Core 中的 appsettings.json 文件。

基础的 'appsettings.json' 文件应该配置为在开发环境中运行,然后基于环境的覆盖文件,例如 appsettings.production.json 会覆盖生产环境下的特定键值吗?

还是说 appsettings.json 只应包含在所有环境下共享的配置,然后使用特定的 appsettings.development/staging.json 文件来明确设置这些环境的键值?

我的担忧是,假设一个应用程序部署到一个实际服务器上,但缺少了存储在环境变量中的密钥(例如,要覆盖连接字符串),或者拼写错误等。在这种情况下,应用程序将退回到基础的 appsettings.json 连接字符串,这将是错误的数据库连接字符串使应用程序无法正常运行。这样的情况听起来相当糟糕,特别是因为这很容易被忽视?

因此,问题真正的关键在于 - 基本的 appsettings.json 文件的内容应该作为默认的“dev”值(例如 dev 数据库、沙盒 API)而被覆盖更新为生产数据,还是反过来?


你的问题非常好,但它肯定会引发关于如何将appsettings.json作为开发活动基础的最佳方式的争论。请更多地关注你的问题,而不是询问处理appsettings.json时应该选择哪种策略。因此,在SO上一个好的问题应该避免将意见作为答案。 - Eriawan Kusumawardhono
8个回答

12
更新:我想补充一下,我不再使用Azure App Configuration,只使用KeyVault。我很失望的是,定价允许每个订阅1个免费的AppConfig,然后似乎对第一个之外的每个AppConfig每天收取超过1.20$ CDN的费用。对我来说,这意味着每月额外36美元的费用,而这个东西每天只会被使用一次(假设我让Web应用程序在晚上关闭)。价格点根本没有任何意义。我宁愿使用多个KeyVault,因为它们的成本纯粹基于使用量,并且每千次交易只需支付微不足道的费用。我不明白AppConfig的预期用例是什么,以证明其价格点的合理性。
我已经养成了将配置存储在AzureAppConfig和/或AzureKeyVault下的习惯。这为我提供了一个集中管理开发、暂存/测试、生产设置的位置,不需要我通过操作appsettings文件来复杂化部署,也不需要将它们存储在某种部署仓库中。它只在应用程序启动时从Azure中读取(我不需要在我的应用程序运行时刷新它们)。话虽如此,对于本地开发故事来说,这使得它有点有趣,因为我个人希望操作顺序是appsettings.jsonappsettings.{environment}.jsonAzureAppConfigKeyVault,最后是secrets.json。这样,无论如何,我都可以用我的本地secrets文件覆盖来自Azure的设置(即使我要覆盖的设置并不是技术上的机密)。
基本上,我最终在program.cs中编写了一些自定义代码来处理从Azure加载配置源,然后查找具有Path"secrets.json"JsonConfigurationSource,最后将其提升为IConfigurationBuilder.Sources中的最后一项。

对我来说,我的文件用法如下

  • appsettings.json - 通用设置,需要为任何环境设置,并且根据环境可能永远不会更改。 appsettings.{environment}.json - 主要是空的JSON文件,基本上只命名了AzureAppConfigAzuerKeyVault资源名称以连接到
  • AzureAppConfig - 基本上适用于在生产、暂存/测试或本地开发之间有所不同的任何内容,并且不是敏感信息。API端点地址、IP地址、各种URL、错误日志信息等。
  • AzureKeyVault - 任何敏感信息。用户名、密码、外部API的密钥(身份验证、许可证密钥、连接字符串等)。
事实是,即使您在appsettings.json中设置了一个设置,也并不意味着您不能使用appsettings.{enviroment}.json或其他地方覆盖它。我经常在根设置文件中放置一个值为NULL的设置,只是为了提醒自己它是应用程序中使用的设置。因此,更好的问题可能是,您想能够仅使用基本的appsettings.jsonsecrets.json运行应用程序(例如无错误)吗?还是始终需要来自appsettings.{enviroment}.json的内容才能成功启动?
基于您的问题,另一件要看的事情是配置验证。较新版本的Microsoft.Extensions.Options提供了各种方式来验证您的选项,以便您可以尝试并捕获留空/未定义某些内容的情况。我通常使用数据注释属性装饰我的POCO选项类,然后使用ValidateDataAnnotations()来验证它们是否正确设置。
例如:
services.AddOptions<MailOptions>().Bind(configuration.GetSection("MailSettings")).ValidateDataAnnotations();

值得注意的是,此验证仅在尝试从 DI 请求像上面我使用的 MailOptions 这样的东西时运行(因此不是在启动时)。 出于这个原因,我还创建了自己的 IStartupFilter,在应用程序启动时预先请求一个或多个我的选项类,以便在应用程序开始接受请求之前强制运行相同的验证。
public class EagerOptionsValidationStartupFilter : IStartupFilter
{
    public readonly ICollection<Type> EagerValidateTypes = new List<Type>();
    private readonly IServiceProvider serviceProvider;

    public EagerOptionsValidationStartupFilter(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }

    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        foreach (var eagerType in EagerValidateTypes)
        {
            dynamic test = serviceProvider.GetService(typeof(IOptions<>).MakeGenericType(eagerType));
            _ = test.Value;
        }

        return next;
    }
}

startup.cs

public void ConfigureServices(IServiceCollection services)
{

    services.AddTransient<IStartupFilter>(x =>
        new EagerOptionsValidationStartupFilter(x)
        {
            EagerValidateTypes = {
                typeof(MailOptions),
                typeof(OtherOptions),
                typeof(MoreImportantOptions)
            }
        });
}

嘿,Nick,存储Azure App Config端点的最佳位置/方法是什么? - joe blogs

10

我认为这个问题的答案很无聊:这取决于具体情况。但我最喜欢的方法是:

appsetting.json (base settings)
appsettings.development.json (dev with no secrets)
appsettings.production.json (production with no secrets)

Appsettings 中,仅包含基本设置中的机密值,而其他值则写在对应的 appsettings.[env].json 文件中。例如,数据库连接密钥仅存在于基本设置中,与本地数据库一起使用。这是环境的任务去替换它。

数据库连接和日志记录的示例:

appsettings.json

{
"ConnectionStrings": {
  “dbConnection: “data source=localhost” <—— only here
},
“environment”: “local”,
"Logging": {
  "LogLevel": {
    "Default": “Verbose”
  }
},
}

appsettings.development.json

{
“environment”: “development”,
"Logging": {
  "LogLevel": {
    "Default": “Warning”
  }
},
}

appsettings.production.json

{
“environment”: “production”,
"Logging": {
  "LogLevel": {
    "Default": “Information”
  }
},
}

我的担忧是:假设一个应用程序部署到了一个生产服务器上,但是存储在环境变量中的关键信息(例如覆盖连接字符串)缺失或拼写错误等。在这种情况下,该应用程序将退回到基本的appsettings.json连接字符串,这将是生产环境中不正确的数据库。这样的情况听起来相当灾难性,尤其是这可能很容易被忽视?
你总是可以这样做。但是如果你的基础设施/部署管道允许的话,应该进行一些健全性测试。进行一个简单的健康检查,如果您的基础设施/部署管道允许,则可以对数据库进行ping操作。

4
在需要保证正确性的生产环境中(例如医疗、金融等领域),与其允许回退到其他值,往往失败会更好/更安全,尤其是在配置错误时。 - Jason Weber
1
这正是我的情况 - 我觉得在任何情况下都不能“退回”到开发环境或类似的情况,这是不可接受的。 - harman_kardon
3
好的!那么你应该删除appsetting.json文件,只保留appsettings,[env].json文件。但是,你仍然不安全,因为你可能使用错误的环境变量进行构建。防止这种情况发生的最佳方法是:1)自动构建,这样你只需要正确地设置一次即可。2)确保只有生产服务可以连接到生产数据库和基础架构中的其他服务。3)不要在源代码中包含与生产服务的连接详细信息。这些应该在部署生产服务时注入。 - Moddaman
关于环境变量的问题是正确的,但它更可能会丢失而不是错误(如果缺失,则默认为生产环境)。 - harman_kardon

7
在这里有几个原则需要注意:
首先,任何损坏/丢失的项都应该出现错误而不是在某些情况下默默地工作。这很有价值,因为它能够在开发早期发现问题。只将常量或将在测试中未被覆盖时显示缺失值的基本文件中的值放入其中。这使您可以编写负面测试案例,以已知值来帮助发现更复杂配置中的错误。
其次,部署额外的内容会增加风险,因此不要多余部署任何内容。将每个环境的适当值放入特定于环境的文件中,除此之外不要有其他内容。这些值应该覆盖基本文件,使您能够在没有手动干预的情况下部署和运行。使用开箱即用的配置加载器仅加载当前环境的正确文件。
第三,有一种方法可以在不重新部署任何文件的情况下覆盖环境中的值。这里的价值取决于您的环境和情况例如安全事件,因此环境变量应优先于前两个来源。
如果您正在使用集中式配置源,则可以允许部署的文件覆盖它吗?这是一个dev-sec-ops / policy问题。您的答案将确定集中式配置应放在列表中的位置。您将其放置得越低,您的开发人员就越有可能需要在本地运行实例。
在您的项目中可能还有其他考虑因素或附加层。重要的是,您需要对所做选择有一个“为什么”,并且能够在您的上下文中以逻辑方式解释和证明它们。

7

有几种方式可以配置你的设置(这正是.NET Core的美妙之处)。我通常采用以下方式:

appsetting.json (template)
appsettings.development.json (dev with no secrets)

实际上,我没有在 appsettings.json 中设置任何设置。我将其用作必须在部署期间设置的设置的模板映射。

// appsettings.json

{
  "ConnectionStrings": {
    "dbConnection": "************************"
  },
  "environment": "************************",
  "Logging": {
    "LogLevel": {
      "Default": "************************"
    }
  },
}

这样,如果我错过了任何设置,它将在以后变得明显,因此我不必担心意外使用“滑过”层次结构的设置。因此,如果您查看其他Json文件,则它们是完整的,没有隐藏的设置。

// appsettings.Development.json

{
  "ConnectionStrings": {
    "dbConnection": "data source=localhost"
  },
  "environment": "local",
  "Logging": {
     "LogLevel": {
      "Default": "Verbose"
    }
  }
}

在小型应用程序中,共享设置似乎是一个好主意。但如果您的应用程序变得更加复杂,它实际上会带来更多问题。


1

我已经开始摆脱appsettings.json并通常使用以下方式:

appsettings.local.json
appsettings.development.json
appsettings.staging.json

现在来运行命令,我可以使用$env:ASPNETCORE_ENVIRONMENT="local"$env:ASPNETCORE_ENVIRONMENT="development"明确设置我的环境变量。

当我使用$env:ASPNETCORE_ENVIRONMENT时,我会得到一个清晰的响应,而之前,“无响应”让我不确定它将使用哪些设置 - 我也不会意外地在错误的环境中运行其他命令。

您还可以在launchsettings.json中设置环境变量以运行解决方案。

至于部署,您可以通过构建流水线处理它,或确保在web应用程序配置中设置了ASPNETCORE_ENVIRONMENT


1
我认为提交到源代码控制的appsettings.json应该配置为在本地开发环境中运行尽可能多的内容。请注意:有时可能会存在第三方依赖项,您无法在本地启动(例如,应用/服务使用的第三方API服务),在这种情况下,我将提交特定设置的dev/sandbox值,但对于其他所有内容(例如,与数据库的连接、消息代理、idp、遥测堆栈等),我将进行本地配置。我还喜欢拥有一个初始化脚本,以快速启动所有应用程序依赖项。我在工作的公司使用的微服务模板使用PowerShell和docker-compose快速轻松地启动本地容器化依赖项,以便团队成员可以尽快启动并运行。

以下是采用上述方法的原因:

  • 不假设存在持久的集中式开发/测试环境,或者团队成员能够访问这样的环境。
  • 在源代码控制中没有秘密和密码(或者至少没有生产秘密和密码)。
  • 允许团队成员克隆repo并尽快启动和运行-他们不必去获取一堆appsettings并手动更新appsettings。

另外几个要点:

  • 如果您正在使用docker,则可以使用环境变量(使用this SO answer中描述的双下划线语法)覆盖单个appsettings,但有时可能会变得有点冗长。我更喜欢使用特定于环境的覆盖文件,如下所示。请注意CONFIG_DIRASPNETCORE_ENVIRONMENT环境变量:
WebHost.CreateDefaultBuilder(args)
   .ConfigureAppConfiguration((context, builder) =>
   {
      string basePath = Environment.GetEnvironmentVariable("CONFIG_DIR") ?? Directory.GetCurrentDirectory();
      string environmentVariable = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
      Console.WriteLine("Config directory: " + basePath + Environment.NewLine + "Environment: " + environmentVariable);
      builder.SetBasePath(basePath);
      builder.AddJsonFile("appsettings.json", false, true);
      if (!string.IsNullOrEmpty(environmentVariable))
        builder.AddJsonFile("appsettings." + environmentVariable + ".json", true, true);
      builder.AddEnvironmentVariables();
   })

  • 理想情况下,您的应用程序/服务的部署和配置管理应该在一个单独的 git 仓库中,使用类似 Ansible 的工具。如果任何配置设置发生更改,此仓库应该经过与您的应用程序仓库相同类型的代码审查流程,git 历史记录中的所有内容都会被审计,并且部署是自动化的。简而言之,这可以大大减少出错的可能性。
  • 如果您部署到 Microsoft Azure 或使用 Azure 服务,则应查看 Azure App Config - 基本上是作为服务的应用程序配置(与基于文件的 appsettings 兼容)。
  • 如果要部署到 Linux,则诸如 appsettings 等配置文件应复制到 /etc/opt/[name-of-service],并且不应与二进制文件位于同一目录下的 /opt/[name-of-service] 中。这遵循 Linux 文件系统层次结构标准。前面提到的 CONFIG_DIR 环境变量就是为此而设。
  • 通常我也会在 SCM 中拥有一个 appsettings.docker.json 文件,以便在本地容器中运行我的应用程序/服务。当我想通过 docker 日志提供程序测试日志记录时,我会使用它来替代仅从 Visual Studio IDE 运行应用程序的情况。

0
  1. 为什么在部署期间环境变量会被损坏?我认为与开发过程相比,更有可能对appsettings.*.json文件进行更改,从而导致某些问题。 此外,如果您考虑将相同的设置作为后备添加到appsettings.json中,那么为什么需要env变量呢?
  2. 不仅可以测试代码,还可以为配置编写测试。这是一种比配置约定更健壮的方法。如果出了问题,无论您在多少个地方重复连接字符串,都可能出现问题。实际上... 如果您重复使用连接字符串,您将违反DRY原则,并且会引发麻烦。因为这些副本随时间而分歧。
  3. 您的任何一种方法都应该产生相同的结果。如果env出现故障
    1. 在第一种情况下,appsettings.json\dbConnection (dev)将被appsettings.production.json\dbConnection覆盖。
    2. 在第二种情况下,dbConnection将直接从appsettings.production.json\dbConnection(或从您本地机器上的appsettings.development.json\dbConnection)中获取。
    3. 在第三种情况下...?我不太明白您所说的“反之亦然”的意思是什么?但是,如果您将生产值放入appsettings.json中,它们仍将被相应文件中的值覆盖。或者不覆盖(如果没有)。无论如何。

所以,我认为唯一的问题是:在appsettings.json中是否应该有任何不同于proddev环境的设置,还是只包含通用的设置?

而合理的答案是:它应该只包含通用设置。因为这是预期的。而且更方便-如果您需要更改proddev的设置,您不需要记住在哪里找到它们。显然,在appsettings.production.json 中为 prod ,在 appsettings.development.json 中为 dev 。此外,这更可预测-有一天,即使不是您,其他人也会花费一些时间尝试弄清楚为什么数据库连接失败,如果他眼前的连接字符串正确(因为在半夜他忘记检查是否被覆盖)。


0
不要使用'Environment.GetEnvironmentVariable'来获取环境变量,我建议使用

context.HostingEnvironment.EnvironmentName

这样做可以增加更多的灵活性,例如当环境变量是 DOTNET_ENVIRONMENT 而不是 ASPNETCORE_ENVIRONMENT 时(还有更多配置)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接