Вы можете использовать Spring Cloud Kubernetes для создания приложений, работающих как внутри, так и вне кластера Kubernetes. Единственная проблема с запуском приложений вне Kubernetes состоит в том, что нет автоматически настроенного механизма регистрации.
Spring Cloud Kubernetes делегирует регистрацию платформе, что является очевидным поведением, если вы развертываете свое приложение изнутри, используя объекты Kubernetes. С внешним приложением ситуация иная. Вы должны гарантировать регистрацию самостоятельно на стороне приложения.
Вам также может понравиться: Краткое руководство по микросервисам с Kubernetes, Spring Boot 2.0 и Docker
Эта статья объясняет мотивацию добавления механизмов автоматической регистрации в проект Spring Cloud Kubernetes только для внешних приложений. Давайте рассмотрим архитектуру, в которой некоторые микросервисы работают вне кластера Kubernetes, а некоторые другие работают внутри него. Там может быть много объяснений такой ситуации. Наиболее очевидным объяснением является миграция ваших микросервисов из более старой инфраструктуры в Kubernetes.
Предполагая, что он все еще выполняется, некоторые микросервисы уже перемещены в кластер, а некоторые все еще работают в более старой инфраструктуре. Более того, вы можете решить запустить какой-то экспериментальный кластер только с несколькими вашими приложениями, пока у вас не будет больше опыта использования Kubernetes на производстве. Я думаю, что это не очень редкий случай.
Конечно, есть разные подходы к этому вопросу. Например, вы можете поддерживать две независимые архитектуры на основе микросервисов с различными источниками реестра и конфигурацией обнаружения. Но вы также можете подключить внешние микросервисы через API Kubernetes к кластеру для загрузки конфигурации из ConfigMap
или Secret
и зарегистрировать их там, чтобы обеспечить межсервисную связь с Spring Cloud Kubernetes Ribbon.
Пример исходного кода приложения доступен на GitHub в гибридном филиале в репозитории sample-spring-microservices-Kubernetes : https://github.com/piomin/sample-spring-microservices-kubernetes/tree/hybrid .
Архитектура
Мы перемещаем один из примеров микросервисов сотрудник-сервис , описанный в упомянутой статье, за пределы кластера Kubernetes. Теперь приложения, которые обмениваются данными с сотрудником-службой, должны использовать адреса вне кластера.
Кроме того, они должны иметь возможность обрабатывать номер порта, динамически генерируемый приложением во время запуска ( server.port=0
). Наши приложения по-прежнему распределены по разным пространствам имен, поэтому важно включить функции обнаружения в нескольких пространствах имен, также описанные в моей предыдущей статье. Приложение employee-service подключается к MongoDB , которая все еще развернута в Kubernetes. В этом случае интеграция осуществляется через сервис Kubernetes. Следующая картина иллюстрирует нашу текущую архитектуру.
Kubernetes PropertySource
Ситуация с распределенной конфигурацией понятна. Нам не нужно реализовывать дополнительный код, чтобы иметь возможность использовать его извне. Просто, прежде чем запускать клиентское приложение, мы должны установить переменную окружения KUBERNETES_NAMESPACE
. Так как мы установили его на внешний, нам сначала нужно создать такое пространство имен.
Затем мы можем применить некоторые источники свойств к этому пространству имен. Конфигурация состоит из Kubernetes ConfigMap
и Secret
. Мы храним там монго, учетные данные и некоторые другие свойства. Вот наша ConfigMap
декларация.
Джава
1
apiVersion: v1
2
kind: ConfigMap
3
metadata:
4
name: employee
5
data:
6
application.yaml: |-
7
logging.pattern.console: "%d{HH:mm:ss} ${LOG_LEVEL_PATTERN:-%5p} %m%n"
8
spring:
9
cloud:
10
kubernetes:
11
discovery:
12
all-namespaces: true
13
register: true
14
data:
15
mongodb:
16
database: admin
17
host: 192.168.99.100
18
port: 32612
Номер порта взят из mongodb
Service
, который развернут как NodePort
тип.
Вот наш Secret
.
Джава
xxxxxxxxxx
1
apiVersion: v1
2
kind: Secret
3
metadata:
4
name: employee
5
type: Opaque
6
data:
7
spring.data.mongodb.username: UGlvdF8xMjM=
8
spring.data.mongodb.password: cGlvdHI=
Затем мы создаем ресурсы внутри external
пространства имен.
В bootstrap.yml
файле нам нужно указать адрес сервера API Kubernetes и свойство, отвечающее за доверие к сертификату сервера. Также следует включить использование Секрета в качестве источника свойств, который по умолчанию отключен для Spring Cloud Kubernetes Config.
Джава
xxxxxxxxxx
1
spring:
2
application:
3
name: employee
4
cloud:
5
kubernetes:
6
secrets:
7
enableApi: true
8
client:
9
masterUrl: 192.168.99.100:8443
10
trustCerts: true
Реализация внешней регистрации
Ситуация с обнаружением сервиса намного сложнее. Поскольку Spring Cloud Kubernetes делегирует обнаружение платформе, что идеально подходит для внутренних приложений, отсутствие автоматической настройки регистрации является проблемой для внешних приложений. Поэтому я решил реализовать модуль для автоматической настройки регистрации Spring Cloud Kubernetes для внешнего приложения.
В настоящее время он доступен в нашем образце репозитория в виде модуля spring-cloud-Kubernetes-discovery-ext . Он реализован в соответствии с шаблоном регистрации Spring Cloud Discovery. Начнем с зависимостей. Нам просто нужно включить Spring-Cloud-Starter-Kubernetes, который содержит модули ядра и обнаружения.
Джава
xxxxxxxxxx
1
<dependency>
2
<groupId>org.springframework.cloud</groupId>
3
<artifactId>spring-cloud-starter-kubernetes</artifactId>
4
</dependency>
Вот наш регистрационный объект. Он реализует Registration
интерфейс от Spring Cloud Commons, который определяет некоторые основные методы получения. Мы должны предоставить имя хоста, порт, serviceId и т. Д.
Джава
xxxxxxxxxx
1
public class KubernetesRegistration implements Registration {
2
3
private KubernetesDiscoveryProperties properties;
4
5
private String serviceId;
6
private String instanceId;
7
private String host;
8
private int port;
9
private Map<String, String> metadata = new HashMap<>();
10
11
public KubernetesRegistration(KubernetesDiscoveryProperties properties) {
12
this.properties = properties;
13
}
14
15
16
public String getInstanceId() {
17
return instanceId;
18
}
19
20
21
public String getServiceId() {
22
return serviceId;
23
}
24
25
26
public String getHost() {
27
return host;
28
}
29
30
31
public int getPort() {
32
return port;
33
}
34
35
36
public boolean isSecure() {
37
return false;
38
}
39
40
41
public URI getUri() {
42
return null;
43
}
44
45
46
public Map<String, String> getMetadata() {
47
return metadata;
48
}
49
50
51
public String getScheme() {
52
return "http";
53
}
54
55
public void setServiceId(String serviceId) {
56
this.serviceId = serviceId;
57
}
58
59
public void setInstanceId(String instanceId) {
60
this.instanceId = instanceId;
61
}
62
63
public void setHost(String host) {
64
this.host = host;
65
}
66
67
public void setPort(int port) {
68
this.port = port;
69
}
70
71
public void setMetadata(Map<String, String> metadata) {
72
this.metadata = metadata;
73
}
74
75
}
У нас есть некоторые дополнительные свойства конфигурации по сравнению с Spring Cloud Kubernetes Discovery. Они доступны под одним и тем же префиксом spring.cloud.kubernetes.discovery
.
Джава
xxxxxxxxxx
1
"spring.cloud.kubernetes.discovery") (
2
public class KubernetesRegistrationProperties {
3
4
private String ipAddress;
5
private String hostname;
6
private boolean preferIpAddress;
7
private Integer port;
8
private boolean register;
9
10
// GETTERS AND SETTERS
11
12
}
Существует также класс, который должен расширять абстрактные AbstractAutoServiceRegistration
. Он отвечает за управление процессом регистрации. Во-первых, он включает механизм регистрации, только если приложение работает за пределами Kubernetes.
Для этого он использует PodUtils
компонент, определенный в Spring Cloud Kubernetes Core . Также реализован метод построения учетных объектов. Порт может генерироваться динамически при запуске. Остальная часть процесса выполняется внутри абстрактного подкласса.
Джава
xxxxxxxxxx
1
public class KubernetesAutoServiceRegistration extends AbstractAutoServiceRegistration<KubernetesRegistration> {
2
3
private KubernetesDiscoveryProperties properties;
4
private KubernetesRegistrationProperties registrationProperties;
5
private KubernetesRegistration registration;
6
private PodUtils podUtils;
7
8
KubernetesAutoServiceRegistration(ServiceRegistry<KubernetesRegistration> serviceRegistry,
9
AutoServiceRegistrationProperties autoServiceRegistrationProperties,
10
KubernetesRegistration registration, KubernetesDiscoveryProperties properties,
11
KubernetesRegistrationProperties registrationProperties, PodUtils podUtils) {
12
super(serviceRegistry, autoServiceRegistrationProperties);
13
this.properties = properties;
14
this.registrationProperties = registrationProperties;
15
this.registration = registration;
16
this.podUtils = podUtils;
17
}
18
19
public void setRegistration(int port) throws UnknownHostException {
20
String ip = registrationProperties.getIpAddress() != null ? registrationProperties.getIpAddress() : InetAddress.getLocalHost().getHostAddress();
21
registration.setHost(ip);
22
registration.setPort(port);
23
registration.setServiceId(getAppName(properties, getContext().getEnvironment()) + "." + getNamespace(getContext().getEnvironment()));
24
registration.getMetadata().put("namespace", getNamespace(getContext().getEnvironment()));
25
registration.getMetadata().put("name", getAppName(properties, getContext().getEnvironment()));
26
this.registration = registration;
27
}
28
29
30
protected Object getConfiguration() {
31
return properties;
32
}
33
34
35
protected boolean isEnabled() {
36
return !podUtils.isInsideKubernetes();
37
}
38
39
40
protected KubernetesRegistration getRegistration() {
41
return registration;
42
}
43
44
45
protected KubernetesRegistration getManagementRegistration() {
46
return registration;
47
}
48
49
public String getAppName(KubernetesDiscoveryProperties properties, Environment env) {
50
final String appName = properties.getServiceName();
51
if (StringUtils.hasText(appName)) {
52
return appName;
53
}
54
return env.getProperty("spring.application.name", "application");
55
}
56
57
public String getNamespace(Environment env) {
58
return env.getProperty("KUBERNETES_NAMESPACE", "external");
59
}
60
61
}
Процесс должен быть инициализирован сразу после запуска приложения. Чтобы поймать событие запуска, мы подготовим бин, который реализует SmartApplicationListener
интерфейс. Метод слушателя вызывает bean-компонент KubernetesAutoServiceRegistration
для подготовки объекта регистрации и запуска процесса.
Джава
xxxxxxxxxx
1
public class KubernetesAutoServiceRegistrationListener implements SmartApplicationListener {
2
3
private static final Logger LOGGER = LoggerFactory.getLogger(KubernetesAutoServiceRegistrationListener.class);
4
5
private final KubernetesAutoServiceRegistration autoServiceRegistration;
6
7
KubernetesAutoServiceRegistrationListener(KubernetesAutoServiceRegistration autoServiceRegistration) {
8
this.autoServiceRegistration = autoServiceRegistration;
9
}
10
11
12
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
13
return WebServerInitializedEvent.class.isAssignableFrom(eventType);
14
}
15
16
17
public boolean supportsSourceType(Class<?> sourceType) {
18
return true;
19
}
20
21
22
public int getOrder() {
23
return 0;
24
}
25
26
27
public void onApplicationEvent(ApplicationEvent applicationEvent) {
28
if (applicationEvent instanceof WebServerInitializedEvent) {
29
WebServerInitializedEvent event = (WebServerInitializedEvent) applicationEvent;
30
try {
31
autoServiceRegistration.setRegistration(event.getWebServer().getPort());
32
autoServiceRegistration.start();
33
} catch (UnknownHostException e) {
34
LOGGER.error("Error registering to kubernetes", e);
35
}
36
}
37
}
38
39
}
Вот автоконфигурация для всех ранее описанных bean-компонентов.
Джава
xxxxxxxxxx
1
2
name = "spring.cloud.kubernetes.discovery.register", havingValue = "true") (
3
AutoServiceRegistrationConfiguration.class, KubernetesServiceRegistryAutoConfiguration.class}) ({
4
public class KubernetesAutoServiceRegistrationAutoConfiguration {
5
6
7
AutoServiceRegistrationProperties autoServiceRegistrationProperties;
8
9
10
11
public KubernetesAutoServiceRegistration autoServiceRegistration(
12
"serviceRegistry") KubernetesServiceRegistry registry, (
13
AutoServiceRegistrationProperties autoServiceRegistrationProperties,
14
KubernetesDiscoveryProperties properties,
15
KubernetesRegistrationProperties registrationProperties,
16
KubernetesRegistration registration, PodUtils podUtils) {
17
return new KubernetesAutoServiceRegistration(registry,
18
autoServiceRegistrationProperties, registration, properties, registrationProperties, podUtils);
19
}
20
21
22
public KubernetesAutoServiceRegistrationListener listener(KubernetesAutoServiceRegistration registration) {
23
return new KubernetesAutoServiceRegistrationListener(registration);
24
}
25
26
27
public KubernetesRegistration registration(KubernetesDiscoveryProperties properties) throws UnknownHostException {
28
return new KubernetesRegistration(properties);
29
}
30
31
32
public KubernetesRegistrationProperties kubernetesRegistrationProperties() {
33
return new KubernetesRegistrationProperties();
34
}
35
36
}
Наконец, мы можем перейти к самому важному шагу — интеграции с API Kubernetes. Spring Cloud Kubernetes использует Fabric Kubernetes Client для связи с главным API. KubernetesClient
Боб уже автоматически настроен, так что мы можем вводить его.
register
И deregister
методы реализованы в классе , KubernetesServiceRegistry
который реализует ServiceRegistry
интерфейс. Обнаружение в Kubernetes настраивается через Endpoint API .
Каждый Endpoint
содержит список, в EndpointSubset
котором хранится список зарегистрированных IP-адресов EndpointAddress
и список прослушивающих портов внутри EndpointPort
. Вот реализация методов регистрации и отмены регистрации.
Джава
xxxxxxxxxx
1
public class KubernetesServiceRegistry implements ServiceRegistry<KubernetesRegistration> {
2
3
private static final Logger LOG = LoggerFactory.getLogger(KubernetesServiceRegistry.class);
4
5
private final KubernetesClient client;
6
private KubernetesDiscoveryProperties properties;
7
8
public KubernetesServiceRegistry(KubernetesClient client, KubernetesDiscoveryProperties properties) {
9
this.client = client;
10
this.properties = properties;
11
}
12
13
14
public void register(KubernetesRegistration registration) {
15
LOG.info("Registering service with kubernetes: " + registration.getServiceId());
16
Resource<Endpoints, DoneableEndpoints> resource = client.endpoints()
17
.inNamespace(registration.getMetadata().get("namespace"))
18
.withName(registration.getMetadata().get("name"));
19
Endpoints endpoints = resource.get();
20
if (endpoints == null) {
21
Endpoints e = client.endpoints().create(create(registration));
22
LOG.info("New endpoint: {}",e);
23
} else {
24
try {
25
Endpoints updatedEndpoints = resource.edit()
26
.editMatchingSubset(builder -> builder.hasMatchingPort(v -> v.getPort().equals(registration.getPort())))
27
.addToAddresses(new EndpointAddressBuilder().withIp(registration.getHost()).build())
28
.endSubset()
29
.done();
30
LOG.info("Endpoint updated: {}", updatedEndpoints);
31
} catch (RuntimeException e) {
32
Endpoints updatedEndpoints = resource.edit()
33
.addNewSubset()
34
.withPorts(new EndpointPortBuilder().withPort(registration.getPort()).build())
35
.withAddresses(new EndpointAddressBuilder().withIp(registration.getHost()).build())
36
.endSubset()
37
.done();
38
LOG.info("Endpoint updated: {}", updatedEndpoints);
39
}
40
}
41
42
}
43
44
45
public void deregister(KubernetesRegistration registration) {
46
LOG.info("De-registering service with kubernetes: " + registration.getInstanceId());
47
Resource<Endpoints, DoneableEndpoints> resource = client.endpoints()
48
.inNamespace(registration.getMetadata().get("namespace"))
49
.withName(registration.getMetadata().get("name"));
50
51
EndpointAddress address = new EndpointAddressBuilder().withIp(registration.getHost()).build();
52
Endpoints updatedEndpoints = resource.edit()
53
.editMatchingSubset(builder -> builder.hasMatchingPort(v -> v.getPort().equals(registration.getPort())))
54
.removeFromAddresses(address)
55
.endSubset()
56
.done();
57
LOG.info("Endpoint updated: {}", updatedEndpoints);
58
59
resource.get().getSubsets().stream()
60
.filter(subset -> subset.getAddresses().size() == 0)
61
.forEach(subset -> resource.edit()
62
.removeFromSubsets(subset)
63
.done());
64
}
65
66
private Endpoints create(KubernetesRegistration registration) {
67
EndpointAddress address = new EndpointAddressBuilder().withIp(registration.getHost()).build();
68
EndpointPort port = new EndpointPortBuilder().withPort(registration.getPort()).build();
69
EndpointSubset subset = new EndpointSubsetBuilder().withAddresses(address).withPorts(port).build();
70
ObjectMeta metadata = new ObjectMetaBuilder()
71
.withName(registration.getMetadata().get("name"))
72
.withNamespace(registration.getMetadata().get("namespace"))
73
.build();
74
Endpoints endpoints = new EndpointsBuilder().withSubsets(subset).withMetadata(metadata).build();
75
return endpoints;
76
}
77
78
}
Бины автоконфигурации регистрируются в spring.factories
файле.
Джава
xxxxxxxxxx
1
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
2
org.springframework.cloud.kubernetes.discovery.ext.KubernetesServiceRegistryAutoConfiguration,\
3
org.springframework.cloud.kubernetes.discovery.ext.KubernetesAutoServiceRegistrationAutoConfiguration
Включение регистрации
Теперь мы можем включить уже созданную библиотеку в любое приложение Spring Cloud, работающее вне Kubernetes, например, в службу сотрудников . Мы используем его вместе с Spring Cloud Kubernetes.
Джава
xxxxxxxxxx
1
<dependency>
2
<groupId>org.springframework.cloud</groupId>
3
<artifactId>spring-cloud-starter-kubernetes-all</artifactId>
4
</dependency>
5
<dependency>
6
<groupId>pl.piomin.services</groupId>
7
<artifactId>spring-cloud-kubernetes-discovery-ext</artifactId>
8
<version>1.0-SNAPSHOT</version>
9
</dependency>
Регистрации по — прежнему отключена , так как мы не будем правильно установить spring.cloud.kubernetes.discovery.register
в true
.
Джава
xxxxxxxxxx
1
spring:
2
cloud:
3
kubernetes:
4
discovery:
5
register: true
Иногда его можно использовать для настройки статического IP-адреса в конфигурации, если у вас будет несколько сетевых интерфейсов.
Джава
xxxxxxxxxx
1
spring:
2
cloud:
3
kubernetes:
4
discovery:
5
ipAddress: 192.168.99.1
Установив 192.168.99.1
статический IP-адрес, я могу легко выполнить некоторые тесты с узлом Minikube, который работает на виртуальной машине, доступной под номером 192.168.99.100.
Ручное тестирование
Давайте начнем сотрудник-сервис на местном уровне. Как вы видите на экране ниже, он успешно загрузил конфигурацию из Kubernetes и соединился с MongoDB, работающим в кластере.
После запуска приложение зарегистрировалось в Kubernetes.
Мы можем просмотреть детали
employee
конечной точки, используя
kubectl describe endpoints
команду, как показано ниже.
Наконец, мы можем выполнить несколько тестовых вызовов, например, через службу шлюза, запущенную на Minikube.
Джава
xxxxxxxxxx
1
$ curl http://192.168.99.100:31854/employee/actuator/info
Поскольку Spring Cloud Kubernetes не разрешает обнаружение во всех пространствах имен для клиентов ленты, мы должны переопределить конфигурацию ленты, используя, DiscoveryClient
как показано ниже.
Джава
xxxxxxxxxx
1
public class RibbonConfiguration {
2
3
4
private DiscoveryClient discoveryClient;
5
6
private String serviceId = "client";
7
protected static final String VALUE_NOT_SET = "__not__set__";
8
protected static final String DEFAULT_NAMESPACE = "ribbon";
9
10
public RibbonConfiguration () {
11
}
12
13
public RibbonConfiguration (String serviceId) {
14
this.serviceId = serviceId;
15
}
16
17
18
19
public ServerList<?> ribbonServerList(IClientConfig config) {
20
21
Server[] servers = discoveryClient.getInstances(config.getClientName()).stream()
22
.map(i -> new Server(i.getHost(), i.getPort()))
23
.toArray(Server[]::new);
24
25
return new StaticServerList(servers);
26
}
27
28
}
Резюме
Есть некоторые ограничения, связанные с открытием в Kubernetes. Например, нет встроенного механизма контроля пульса, поэтому мы должны позаботиться об удалении конечных точек приложения при завершении работы. Кроме того, я не рассматриваю аспекты безопасности, связанные с разрешением обнаружения во всех пространствах имен и разрешением доступа к API для внешних приложений.
Я предполагаю, что вы гарантировали необходимый уровень безопасности при создании кластера Kubernetes, особенно если вы решили разрешить внешний доступ к API. API — это всего лишь API, и мы можем его использовать. В этой статье показан пример варианта использования, который может быть полезен для вас.
Если вы сравните его с моей предыдущей статьей о Spring Cloud Kubernetes, вы увидите, что с небольшими изменениями конфигурации вы можете перемещать приложение за пределы кластера без добавления каких-либо новых компонентов для обнаружения или распределенной конфигурации.
Дальнейшее чтение
Разработка приложения Spring Boot для кластера Kubernetes: учебное пособие [Часть 1]
Разработка микросервиса Spring Boot на Кубернетесе [Видео]
Создание упругих микросервисов с Kubernetes и пружинным ботинком с нуля