【问题标题】:.Net Core appsettings.json best practices - override dev settings (or vice versa)?.Net Core appsettings.json 最佳实践 - 覆盖开发设置(反之亦然)?
【发布时间】:2020-09-17 02:07:24
【问题描述】:

寻找一种在 .Net Core 中构建 appsettings.json 文件的合理方法。

是否应将基本“appsettings.json”文件配置为在开发环境中运行,然后基于环境的覆盖(例如 appsettings.production.json)覆盖生产的特定键?

或者 appsettings.json 是否应该只包含跨所有环境共享的配置,然后是一个特定的 appsettings.development/staging.json 文件,用于为这些环境显式设置密钥?

我担心的是 - 假设应用程序部署到实时服务器,但存储在环境变量中的密钥(例如覆盖连接字符串)丢失或拼写错误等。在这种情况下,应用程序将退回到基础appsettings.json 连接字符串,这对于实时环境来说是不正确的数据库。像这样的场景听起来很糟糕,尤其是因为这很容易被忽视?

所以问题真的归结为 - 基础 appsettings.json 文件的内容是否应该是默认的“开发”值(例如开发数据库、沙盒 API),并被生产数据覆盖,反之亦然?

【问题讨论】:

  • 您的问题非常好,但它肯定会引发关于将appsettings.json 纳入开发活动的最佳方式的辩论。请更多地关注你的问题,而不是问你在处理appsettings.json时应该选择什么策略。因此,关于 SO 的好问题应避免将意见作为答案。

标签: .net asp.net-core .net-core


【解决方案1】:

我认为这个答案很无聊;这取决于。但我最喜欢的方法是:

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 数据库的位置进行简单的运行状况检查。

【讨论】:

  • 在必须正确的 PROD 环境中(例如医疗、金融等),错误配置时失败通常比允许“回退”到其他环境更好/更安全价值。
  • 这正是我的情况——我只是不觉得有这样的场景是可以接受的,你可以“退回”到开发环境或类似的东西......
  • 好的!然后你应该删除 appsetting.json 并且只有 appsettings,[env].json 文件。但是,您仍然不安全,因为您可能使用错误的环境变量进行构建。防止这种情况的最好方法是 1) 自动构建,然后你真的只需要一次正确。 2) 确保只有生产服务可以连接到生产数据库和基础设施中的其他服务。 3) 源代码中没有生产服务的连接细节。这些应该在部署生产服务时注入。
  • 关于 env var 的情况是正确的,但它更可能是丢失而不是错误(如果丢失默认为生产)。
【解决方案2】:

我已经养成了将我的配置存储在 Azure 中的 AzureAppConfig 和/或 AzureKeyVault 下的习惯。它为我提供了一个中心位置来管理我的开发、登台/测试、生产设置,并且不需要我通过操作 appsettings 文件或将它们存储在某种部署存储库中来使我的部署复杂化。它实际上只在应用程序启动时从天蓝色读取(我不需要在我的应用程序运行时刷新它们)。话虽如此,这让本地开发故事变得有点有趣,因为我个人希望操作顺序为appsettings.jsonappsettings.{environment}.jsonAzureAppConfigKeyVault,最后是secrets.json。这样一来,无论如何,我都可以使用本地机密文件覆盖 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 Options 类,然后使用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)
            }
        });
}

【讨论】:

  • 嘿尼克,存储 Azure App Config 端点的最佳位置/方法是什么?
【解决方案3】:

有几种方法可以塑造您的设置(这就是 .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"
    }
  }
}

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

【讨论】:

    【解决方案4】:

    这里有几个原则:

    首先,任何损坏/丢失的项目都应该出错而不是在某些情况下静默工作。这很有价值,因为它在开发的早期就发现了问题。仅将跨环境不变的值放入基本文件中,或者在未覆盖时显示缺失值,例如正在测试中。这使您能够将负面测试用例写入已知值,这有助于发现更复杂配置中的错误。

    其次,任何额外部署的内容都会增加风险,因此不要部署任何额外内容。将每个环境的适当值放入特定于环境的文件中,仅此而已。这些值应覆盖基本文件,使您无需手动干预即可部署和运行。使用开箱即用的配置加载器(仅)加载当前环境的正确文件。

    第三,有一种方法可以在不重新部署任何文件的情况下覆盖环境中的值,这会很有帮助。这里的值取决于您的环境和情况,例如安全事件。因此,环境变量应该覆盖前面两个来源。

    如果您使用集中式配置源,是否可以允许部署的文件覆盖它?这是一个 dev-sec-ops/policy 问题。您的答案将决定集中配置应该在列表中的哪个位置。您放得越远,您的开发人员就越有可能需要在本地运行实例。

    在您的项目中可能还有其他考虑因素或附加层。重要的是对您做出的选择有一个“为什么”,并能够在您的上下文中合理地解释和证明它们。

    【讨论】:

      【解决方案5】:
      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。而且它更具可预测性 - 有一天,如果不是你,那么其他人会花一些时间试图弄清楚如果他眼前的连接字符串 正确,为什么 db 连接会失败(这是因为在半夜他忘记检查它是否被覆盖)。

      【讨论】:

        【解决方案6】:

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

        以下是上述方法的一些原因:

        • 不对持久集中式开发/测试环境的存在或团队成员访问此类环境的能力做出任何假设。
        • 源代码控制中没有机密和密码(或至少没有生产机密和密码)。
        • 允许团队成员克隆存储库并尽快启动和运行 - 他们不必从某个地方获取大量应用设置并手动更新应用设置。

        耦合其他指针:

        • 如果您使用 docker,您可以通过使用环境变量(使用this SO answer 中描述的双下划线语法)覆盖单个应用设置,但是这有时会有点冗长。我更喜欢使用环境特定的覆盖文件,如下所示。注意 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();
           })
        
        • 理想情况下,您的应用程序/服务的部署和配置管理应该使用 Ansible 之类的工具在单独的 git 存储库中进行。如果任何配置设置发生变化,这个 repo 应该通过与您的应用程序 repo 相同的代码审查过程,所有内容都在 git 历史记录中进行审计,并且部署是自动化的。简而言之,这使得搞砸配置设置的可能性大大降低。
        • 如果您部署到 Microsoft Azure,或使用 Azure 服务;您应该查看Azure App Config - 基本上是应用配置即服务(并且与基于文件的应用设置兼容)。
        • 如果您要部署到 Linux,则应将 appsettings 等配置文件复制到 /etc/opt/[name-of-service],并且不应与 /opt/[name-of-service] 下的二进制文件位于同一目录中。这遵循Linux Filesystem Hierarchy Standard。这就是前面描述的CONFIG_DIR 环境变量的用途。
        • 当我想将我的应用程序/服务作为本地容器​​运行时,我通常在 SCM 中有一个 appsettings.docker.json 文件。当我想通过 docker logging 提供程序测试日志记录时,我使用它而不是仅从 Visual Studio IDE 运行应用程序时的一个示例。

        【讨论】:

          猜你喜欢
          • 2020-02-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-04-27
          • 2012-09-11
          • 1970-01-01
          • 1970-01-01
          • 2010-09-20
          相关资源
          最近更新 更多