应用程序的配置与代码的严格分离-Kubernetes配置

在本文中,您将了解有关 Spring Boot 配置的更多信息。我将向您展示如何在不同的环境中有效地使用它。特别是,我们更多地讨论了 Kubernetes 的配置。有很多可用的选项,包括属性、YAML 文件、环境变量和命令行参数。我们想要实现的是我们应用程序的配置与代码的严格分离。我们应该遵守十二要素应用程序的第三条规则。

如果您喜欢与 Spring Boot 配置相关的主题,您可能会对我博客上的另外两篇文章感兴趣。要阅读有关 Spring Boot 自动配置的信息,请参阅以下文章。要了解有关一般配置的更多信息,请阅读该帖子。

源代码

如果您想自己尝试一下,可以随时查看我的源代码。为此,您需要克隆我的 GitHub 存储库。之后,您应该按照我的指示进行操作。让我们开始。

加载配置级别

配置管理是 Spring Boot 中一个非常有趣的话题。总共有 14 级排序属性值和 4 级排序配置数据文件。您将在此处找到所有级别的完整列表。让我们从第一个例子开始。假设property.defaultSpring Boot中有一个属性application.yml

property.default: app

我们在 classpath 中有另一个配置文件additional.yml。它包含相同的属性,但具有不同的值。

property.default: additional

我们的 Spring Boot 应用程序additional.yml在启动时使用@PropertySource注解加载文件。那不是全部。在同一代码中,它还使用该SpringApplication.setDefaultProperties方法设置该属性的默认值。

@SpringBootApplication
@PropertySource(value = "classpath:/additional.yml", 
                ignoreResourceNotFound = true)
public class ConfigApp {

   private static final Logger LOG = 
      LoggerFactory.getLogger(ConfigApp.class);

   public static void main(String[] args) {
      SpringApplication app = new SpringApplication(ConfigApp.class);
      app.setDefaultProperties(Map.of("property.default", "default"));
      app.setAllowBeanDefinitionOverriding(true);
      app.run(args);
   }

   @Value("${property.default}")
   String propertyDefault;

   @PostConstruct
   public void printInfo() {
      LOG.info("========= Property (default): {}", propertyDefault);
   }

}

应用程序加载的属性的最终值是多少?在我们回答这个问题之前,让我们把我们的例子稍微复杂一点。我们设置环境变量PROPERTY_DEFAULT如下图。

$ export PROPERTY_DEFAULT=env

最后,我们可以运行应用程序。以下代码片段LOG.info("========= Property (default): {}", propertyDefault)将打印该值env。环境变量覆盖我们示例中使用的所有其他级别。事实上,这只是 14 的第五个配置级别。

Kubernetes 上的 Spring 配置

通常,在云平台中,我们使用环境变量来配置我们的应用程序。例如,在 Kubernetes 中,我们有两种类型的对象用于管理配置:SecretConfigMap. 我们可以在 Kubernetes 中定义我们的属性值,ConfigMap如下所示。

apiVersion: v1
kind: ConfigMap
metadata:
  name: springboot-configuration-playground
data:
  PROPERTY_DEFAULT: env

我们只需将我们的附加ConfigMap到 Kubernetes就可以将此值传递给应用程序Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: springboot-configuration-playground
spec:
  selector:
    matchLabels:
      app: springboot-configuration-playground
  template:
    metadata:
      labels:
        app: springboot-configuration-playground
    spec:
      containers:
      - name: springboot-configuration-playground
        image: piomin/springboot-configuration-playground
        ports:
        - containerPort: 8080
        envFrom:
          - configMapRef:
              name: springboot-configuration-playground

实际上,对于本地操作系统上设置的环境变量,我们得到的结果与之前相同。我们的应用程序使用该值env作为propertyDefault变量。虽然我们在 Spring Boot 中有 14 级配置,但我们可以只使用其中的 5 级来有效地管理应用程序。我会说更多——小心使用其他配置级别,因为您可能会禁用环境变量的覆盖值。当然,你可能有这样做的理由。但重要的是在开始使用更复杂的配置之前了解这些级别。kubectl logs您可以使用以下命令在部署后验证日志:

在 Kubernetes 中,我们还可以将整个配置文件传递给Deploymentvia ConfigMap,而不仅仅是单独的属性。现在,让我们假设我们只创建application.yml文件而不是设置环境变量,如下所示。

apiVersion: v1
kind: ConfigMap
metadata:
  name: springboot-configuration-playground-ext
data:
  application.yml: |
    property.default: external

我们需要重新配置应用DeploymentYAML。我们将在路径ConfigMap下附加我们的安装卷。/config由于我们使用 Jib Maven 插件作为构建镜像的工具,所以主应用程序运行目录是/. 默认情况下,Spring Boot 从位于当前目录或当前目录 /config 子目录中的文件中读取属性。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: springboot-configuration-playground
spec:
  selector:
    matchLabels:
      app: springboot-configuration-playground
  template:
    metadata:
      labels:
        app: springboot-configuration-playground
    spec:
      containers:
      - name: springboot-configuration-playground
        image: piomin/springboot-configuration-playground
        ports:
        - containerPort: 8080
        volumeMounts:
          - mountPath: /config
            name: app-vol
      volumes:
        - name: app-vol
          configMap:
            name: springboot-configuration-playground-ext

这是我们在使用最新的ConfigMap. 位于打包 jar 之外的application.yml文件会覆盖类路径中的相同文件。

将配置传递给 Spring Boot 应用程序的另一种有趣方式是通过内联 JSON 属性。当您的应用程序启动时,任何 spring.application.jsonSPRING_APPLICATION_JSON属性都将被解析并添加到 Environment. 此方法将覆盖所有先前描述的负载级别设置的值。为了在 Kubernetes 上测试它,让我们修改我们ConfigMap的环境变量,然后重新加载应用程序。现在日志中的输出应该是json.

apiVersion: v1
kind: ConfigMap
metadata:
  name: springboot-configuration-playground
data:
  PROPERTY_DEFAULT: env
  SPRING_APPLICATION_JSON: '{"property":{"default":"json"}}'

设置云平台特定配置

让我们考虑另一个与云平台相关的有趣功能。您认为是否可以根据云环境仅加载部分配置?是的!Spring Boot 能够检测我们是否在 Kubernetes 上运行我们的应用程序。我们唯一需要做的就是正确标记配置的那部分。有专门的财产spring.config.activate.on-cloud-platform。我们需要在那里设置目标平台的名称。在我们的例子中是 Kubernetes,但 Spring Boot 也支持 Heroku 或 Azure。让我们property.activationapplication.yml. 只有当我们在 Kubernetes 上运行我们的应用程序时,才会启用该属性:

property.default: app
---
spring:
  config:
    activate:
      on-cloud-platform: "kubernetes"
property.activation: "I'm on Kubernetes!"

然后我们需要更新负责在日志中打印属性的应用程序主类的一部分:

@Value("${property.default}")
String propertyDefault;
@Value("${property.activation:none}")
String propertyActivation;

@PostConstruct
public void printInfo() {
   LOG.info("========= Property (default): {}", propertyDefault);
   LOG.info("========= Property (activation): {}", propertyActivation);
}

如果您使用 Skaffold,您可以使用命令轻松地在 Kubernetes 上运行示例应用程序skaffold dev。否则,只需使用我的 Docker Hub 存储库中的映像进行部署:piomin/springboot-configuration-playground. 结果如下:

如果我们使用命令在本地运行相同的应用程序,mvn spring-boot:run它将打印默认值none

使用 Spring Boot Test 测试配置

配置属性可能会对应用程序产生巨大影响。例如,我们可以根据带有@Conditional注释的配置属性加载不同的 bean。因此,我们应该仔细测试它。假设我们有以下@Configuration包含MyBean2and MyBean3bean 的类:

public class MyConfiguration {

   @Bean
   @ConditionalOnProperty("myBean2.enabled")
   public MyBean2 myBean2() {
      return new MyBean2();
   }

   @Bean
   @ConditionalOnJava(range = ConditionalOnJava.Range.EQUAL_OR_NEWER, 
                      value = JavaVersion.EIGHTEEN)
      public MyBean3 myBean3() {
      return new MyBean3();
   }

}

仅当我们定义myBean2.enabled属性时,此 bean 才会在上下文中注册。另一方面,有一个@Configuration类覆盖了上面可见的 bean:

public class MyConfigurationOverride {

   @Bean
   public MyBean2 myBean2() {
      MyBean2 b = new MyBean2();
      b.setMe("I'm MyBean2 overridden");
      return b;
   }

}

如何使用 JUnit 测试对其进行测试?幸运的是,有一个ApplicationContextRunner类允许我们使用各种配置选项轻松测试 bean 加载。首先,我们需要将这两个类作为@Configuration测试中的类传递。默认情况下,自 2.1 版以来,Spring Boot 不允许覆盖 bean。我们需要直接使用属性来激活它spring.main.allow-bean-definition-overriding=true。最后,我们必须设置测试属性的值。

@Test
public void testOrder() {
   final ApplicationContextRunner contextRunner = 
      new ApplicationContextRunner();
   contextRunner
         .withAllowBeanDefinitionOverriding(true)
         .withUserConfiguration(MyConfiguration.class, 
                                MyConfigurationOverride.class)
         .withPropertyValues("myBean2.enabled")
         .run(context -> {
            MyBean2 myBean2 = context.getBean(MyBean2.class);
            Assertions.assertEquals("I'm MyBean2 overridden", myBean2.me());
         });
}

MyBean3然后让我们验证我们为bean使用了正确的 Java 版本。至少应该是 18。如果我使用的是早期版本的 Java,会发生什么情况?

@Test
public void testMyBean3() {
   final ApplicationContextRunner contextRunner = 
      new ApplicationContextRunner();
   Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> {
      contextRunner
            .withUserConfiguration(MyConfiguration.class)
            .run(context -> {
               MyBean3 myBean3 = context.getBean(MyBean3.class);
               Assertions.assertEquals("I'm MyBean3", myBean3.me());
            });
   });
}

最后的想法

Spring Boot 是一个非常强大的框架。它提供了许多可以在云平台上有效使用的配置选项。在本文中,您可以了解如何正确使用它们,例如在 Kubernetes 上。您还可以看到如何使用不同级别的加载属性以及如何在它们之间切换。

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章