我们知道 Spring Boot 工程默认的配置文件名称为 application.properties
,SpringApplication 将从以下位置加载 application.properties
文件,并把它们添加到 Spring Environment 中:
/config
子目录/config
包如果我们运行时想指定运行哪个环境的配置文件,可以有三种方式:
application.properties
文件中配置 spring.profiles.active=dev
指定加载的环境类型--spring.profiles.active=prod
加载的环境类型--spring.config.location=target/application.properties
加载配置文件位置至于在工程中如何获取这些配置文件值,这里就不在描述了,这个不是本次演示的重点。
本次演示环境,我是在本机 MAC OS 上操作,以下是安装的软件及版本:
注意:这里 Kubernetes 集群搭建使用 Minikube 来完成,Minikube 启动的单节点 k8s Node 实例是需要运行在本机的 VM 虚拟机里面,所以需要提前安装好 VM,这里我选择 Oracle VirtualBox。k8s 运行底层使用 Docker 容器,所以本机需要安装好 Docker 环境,这里忽略 Docker、VirtualBox、Minikube、Kubectl 的安装过程,可以参考之前文章 Minikube & kubectl 升级并配置,这里结合代码着重介绍下在 K8s 集群中如何使用 ConfigMap 优雅加载 Spring Boot 工程配置文件。
首先我们使用 IDEA 创建一个 Spring Boot 项目,项目名为 demo
,为了好演示加载不同配置文件展示效果,这里添加 swagger-ui 依赖,然后新建 Controller 类 DemoController,通过读取配置文件中的 key 值并返回,代码如下:
package com.example.demo.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/demo")
@PropertySource("classpath:mysql.properties")
@Api(tags = "DemoController", description = "测试读取不同资源文件")
public class DemoController {
@Value("${env}")
private String env;
@Value("${msg}")
private String msg;
@Value("${mysql.hostname}")
private String mysl_url;
@Value("${mysql.port}")
private String mysql_port;
@ApiOperation(value = "获取配置文件变量")
@RequestMapping(value = "", method = RequestMethod.GET)
public Map getDemoKey() {
Map map = new HashMap<>();
try {
map.put("env", env);
map.put("msg", msg);
map.put("mysql_url", mysl_url);
map.put("mysql_port", mysql_port);
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
}
然后在 resources 目录下,分别新建不同环境的配置文件 application.properties
,以及其他配置文件 mysql.properties
如下:
# application-dev.properties
env=dev
msg=this is dev env properteis.
# application-prod.properties
env=prod
msg=this is prod env properteis.
# application-test.properties
env=test
msg=this is test env properteis.
# mysql.properties
mysql.hostname=127.0.0.1
mysql.port=3306
同时在 application.properties
配置文件下指定加载的环境为 dev
,毕竟本地开发,还是使用开发环境配置比较多。
# application.properties
spring.profiles.active=dev
其他代码文件这里不再贴出来了,源码已经上传到 Github 上 spring-k8s-configmap-demo 。接下来,我们本地启动一下,看下能否正确读取到 dev
环境配置文件吧!
$ mvn clean package
$ java -jar demo-0.0.1-SNAPSHOT.jar
启动完毕,本地浏览器访问 http://127.0.0.1:8080/swagger-ui.html
页面,测试一下 http://127.0.0.1:8080/demo
接口,可以看到正确加载到 application-dev.properties
和 mysql.properties
配置文件内容。
OK, 工程启动没有问题,接下来我们来创建 Dockerfile 来构建工程镜像,方便后边部署到 K8s 集群中,新建 Dockerfile 如下:
$ vim Dockerfile
FROM huwanyang168/centos7_jdk8:v1.2
MAINTAINER huwanyang168
ADD demo-0.0.1-SNAPSHOT.jar /opt
WORKDIR /opt
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "demo-0.0.1-SNAPSHOT.jar"]
简单说明一下,我们是基于 centos7_jdk8
环境,添加并启动编译后的 jar,最后构建镜像并 push 到镜像仓库(这里我推送到个人 Docker Hub 仓库)。
$ docker build -t huwanyang168/demo:0.0.1 -f Dockerfile .
$ docker push huwanyang168/demo:0.0.1
启动一下,也是妥妥没有问题的,这里就不在演示了。
接下来,我们创建一个可以在 K8s 集群中运行该镜像的资源类型 yaml 文件,该文件主要包含 Namespace
、ConfigMap
、Deployment
、Service
四种资源类型,这里我使用两种方式加载配置文件,对比下二者的好处和弊端。
yaml 文件如下:
apiVersion: v1
kind: Namespace
metadata:
name: wanyang3
---
apiVersion: v1
kind: ConfigMap
metadata:
name: demo-configmap
namespace: wanyang3
data:
application.properties: |
env=local
msg=this is local env properteis.
mysql.properteis: |
mysql.hostname=10.10.10.10
mysql.port=3333
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-hwy
namespace: wanyang3
labels:
app: demo-hwy
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: demo-hwy
template:
metadata:
labels:
app: demo-hwy
spec:
containers:
- name: demo
image: huwanyang168/demo:0.0.1
imagePullPolicy: IfNotPresent
args: ["--spring.config.location=application.properties,mysql.properties"]
ports:
- containerPort: 8080
volumeMounts:
- name: demo-config
mountPath: /opt/application.properties
subPath: application.properties
- name: demo-config
mountPath: /opt/mysql.properties
subPath: mysql.properties
volumes:
- name: demo-config
configMap:
name: demo-configmap
items:
- key: application.properties
path: application.properties
- key: mysql.properties
path: mysql.properties
---
apiVersion: v1
kind: Service
metadata:
name: demo-hwy
namespace: wanyang3
labels:
app: demo-hwy
spec:
type: NodePort
ports:
- protocol: TCP
port: 8080
targetPort: 8080
nodePort: 32123
selector:
app: demo-hwy
说明一下:
Namespace
,下边其他资源部署在该命名空间下。ConfigMap
来配置下边容器启动时需要使用到的对应环境的配置文件,一般会有多个环境配置,例如 dev
、test
、prod
,这里部署需要使用到哪个环境的配置文件,就配置哪个。Deployment
用来部署上边的 demo 镜像,开启 8080
端口并挂载 ConfigMap
指定的环境配置文件到指定位置。Service
来代理上边的 Deployment
并使用 NodePort
方式对外暴露 32123
端口来方便访问。这里有个地方需要注意:就是 K8s 中 command
、args
和 Dockerfile 中的 ENTRYPOINT
、CMD
之间的关系,下边详细介绍一下二者的关系。
K8s 中 command、args 和 Dockerfile 中的 ENTRYPOINT、CMD 之间的关系
我们知道,K8s 中有 command、args 可以指定镜像启动命令和参数,而 Dockerfile 中 ENTRYPOINT、CMD 同样可以指定镜像启动命令和参数,在 K8s 中当用户同时写了 command 和 args 的时候,是可以覆盖 Dockerfile 中 ENTRYPOINT 的命令行和 CMD 参数,但对于一些其他情况,比如仅仅写了 command 或者 args 的时候,二者的覆盖关系又是怎样呢?我们参照 这里 获得完整的情况分类如下:
Image Entrypoint | Image Cmd | Container command | Container args | Command run |
---|---|---|---|---|
[/ep-1] | [foo bar] | [ep-1 foo bar] | ||
[/ep-1] | [foo bar] | [/ep-2] | [ep-2] | |
[/ep-1] | [foo bar] | [zoo boo] | [ep-1 zoo boo] | |
[/ep-1] | [foo bar] | [/ep-2] | [zoo boo] | [ep-2 zoo boo] |
这里我们使用将 ConfigMap
配置的对应环境配置文件挂载到容器指定位置(跟 jar 包在同一目录),然后通过 1、Spring Boot 加载配置介绍 中的第三种方式,它会在当前目录自动查找指定的配置文件,从而达到启动服务时能够加载正确的配置文件的目的。这种方式好处就是,我们构建时可以不包含不同环境的配置文件(当然打包含进去也是没问题的,会覆盖),这样 jar 包就是一个纯净的不带任何配置文件的应用,该 jar 包在任何环境均可使用,只需要启动时加载包含了对应环境的配置文件的 ConfigMap
即可,其次就是如果仅仅是配置文件需要修改,那么可以在不需要重新构建镜像的情况下,直接修改 ConfigMap
即可。坏处就是每次配置 ConfigMap
时要将对应环境的配置文件配置到 yaml 文件中,稍显复杂。
最后,在集群内部署一下该 yaml,部署成功后,通过访问 http://
地址,查看下是否能够正确读取到配置吧,测试没有问题。
yaml 文件如下:
apiVersion: v1
kind: Namespace
metadata:
name: wanyang3
---
apiVersion: v1
kind: ConfigMap
metadata:
name: demo-configmap-1
namespace: wanyang3
data:
DEPLOYMENT_ENV: test
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-hwy-1
namespace: wanyang3
labels:
app: demo-hwy-1
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: demo-hwy-1
template:
metadata:
labels:
app: demo-hwy-1
spec:
containers:
- name: demo
image: huwanyang168/demo:0.0.1
imagePullPolicy: IfNotPresent
args: ["--spring.profiles.active=$(DEPLOYMENT_ENV_KEY)"]
ports:
- containerPort: 8080
env:
- name: DEPLOYMENT_ENV_KEY
valueFrom:
configMapKeyRef:
name: demo-configmap-1
key: DEPLOYMENT_ENV
---
apiVersion: v1
kind: Service
metadata:
name: demo-hwy-1
namespace: wanyang3
labels:
app: demo-hwy-1
spec:
type: NodePort
ports:
- protocol: TCP
port: 8080
targetPort: 8080
nodePort: 32124
selector:
app: demo-hwy-1
说明一下:
Namespace
,下边其他资源部署在该命名空间下。ConfigMap
用来配置一下 DEPLOYMENT_ENV: test
的 Key-Value 值,主要用来为下边启动容器时指定激活那个环境的配置,一般分为 dev
、test
、prod
等环境配置。Deployment
用来部署上边的 demo 镜像,开启 8080
端口并指定加载 ConfigMap
指定的环境配置。Service
来代理上边的 Deployment
并使用 NodePort
方式对外暴露 32124
端口来方便访问。这里有个地方需要注意:就是 Deployment
在 commond
命令中使用 ConfigMap
定义的环境变量方式。
Deployment 在 commond 命令中使用 ConfigMap 定义的环境变量
我们可以使用该方式从 ConfigMap
中获取指定的 Key 值,并设置为 env 环境变量的形式,可参考 这里 查看使用示例。
env:
- name: DEPLOYMENT_ENV_KEY
valueFrom:
configMapKeyRef:
name: demo-configmap-1
key: DEPLOYMENT_ENV
然后就可以在 command 或者 args 命令时,直接通过 $(DEPLOYMENT_ENV_KEY)
方式获取 env 的值啦!
为什么要强调这点呢,因为在 4.1、直接加载环境的配置文件 中我们通过挂载 volume 的方式将 ConfigMap
中的文件或者值挂载到容器指定位置,这里我们使用 Deployment
在 commond 命令中使用 ConfigMap
定义的环境变量,通过这种方式将要激活的环境属性传递到启动参数中,这样在启动容器时,就可以动态加载指定的环境配置文件啦(这里使用 1、Spring Boot 加载配置介绍 中的第二种方式)。对比上边那种方式,好处就是部署时不需要每次将对应环境的配置文件写到 ConfigMap
中,而是简单的指定激活的环境属性即可(前提是构建时包含所有环境的配置文件),非常的方便。坏处就是如果配置文件改变,每次都得重新构建镜像,重新走一遍部署流程。
最后,在集群内部署一下该 yaml,部署成功后,通过访问 http://
地址,查看下是否能够正确读取到配置吧,测试妥妥没有问题的。
当然,除了上边两种方式外,我们也可以直接在 Dockerfile 中指定激活的环境配置文件,这样的话,我们部署到不同环境时,需要分别构建镜像,这样 K8s 部署时就可以不需要指定 ConfigMap
了,个人认为此方式对应迭代不频繁的项目可以采用,毕竟不需要重复构建不同环境配置文件的镜像,但是对于迭代频繁的项目,建议采用 ConfigMap
方式,这样我们就可以避免重复构建不同环境的镜像啦,一个镜像搞定所有环境!
参考资料